diff --git a/.github/workflows/build-zip.yml b/.github/workflows/build-zip.yml deleted file mode 100644 index 52b291e..0000000 --- a/.github/workflows/build-zip.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Build And Upload Extension Zip Via Artifact - -on: - push: - branches: [ main ] - pull_request: - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version-file: ".nvmrc" - - - uses: actions/cache@v3 - with: - path: node_modules - key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }} - - - uses: pnpm/action-setup@v2 - - - run: pnpm install --frozen-lockfile - - - run: pnpm build - - - uses: actions/upload-artifact@v3 - with: - path: dist/* diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml deleted file mode 100644 index bdac30b..0000000 --- a/.github/workflows/greetings.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Greetings - -on: [pull_request_target, issues] - -jobs: - greeting: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/first-interaction@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: 'Thank you for your contribution. We will check and reply to you as soon as possible.' - pr-message: 'Thank you for your contribution. We will check and reply to you as soon as possible.' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index b693280..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,29 +0,0 @@ -name: Test - -on: - push: - branches: [ main ] - pull_request: - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version-file: ".nvmrc" - - - uses: actions/cache@v3 - with: - path: node_modules - key: ${{ runner.OS }}-build-${{ hashFiles('**/package-lock.json') }} - - - uses: pnpm/action-setup@v2 - - - run: pnpm install --frozen-lockfile - - - run: pnpm test diff --git a/.github/workflows/update-assignees.yml b/.github/workflows/update-assignees.yml deleted file mode 100644 index 071478a..0000000 --- a/.github/workflows/update-assignees.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Update Assignees - -on: - pull_request: - types: - - opened - - reopened - -jobs: - update-assignees: - runs-on: ubuntu-latest - if: ${{ github.actor != 'dependabot[bot]' }} - steps: - - uses: actions-ecosystem/action-add-assignees@v1 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - assignees: ${{ github.actor }} diff --git a/.gitignore b/.gitignore index 61aac8d..a98742b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ temp.txt npm-debug.log* yarn-debug.log* yarn-error.log* +temp.txt diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..87ec884 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +18.18.2 diff --git a/app/package.json b/app/package.json index b0327c6..fc7324b 100644 --- a/app/package.json +++ b/app/package.json @@ -19,12 +19,14 @@ "compile:packageJSON": "node ./bin/modules/postbuild/index.js", "prebuild": "run-s compile:app compile:packageJSON", "build": "./node_modules/.bin/electron-builder", + "build:sign": "npx ts-node ./scripts/sign.ts --esModuleInterop --resolveJsonModule", "postinstall": "run-s prebuild install:deps", "install:deps": "electron-builder install-app-deps", "make:release": "node ./bin/modules/release/index.js", "release": "electron-builder --publish always" }, "dependencies": { + "@electron/osx-sign": "^1.0.5", "body-parser": "^1.20.2", "chokidar": "^3.5.3", "cors": "^2.8.5", diff --git a/app/scripts/sign.ts b/app/scripts/sign.ts new file mode 100644 index 0000000..176803f --- /dev/null +++ b/app/scripts/sign.ts @@ -0,0 +1,23 @@ +import { exec } from 'child_process' +import { displayName, version } from '../package.json' + +const appName = displayName.replace(/\s+/g, '') // Replace spaces in the display name +const appPath = `./dist/v${version}/mac-arm64/${appName}.app` + +const signApp = () => { + const command = `electron-osx-sign "${appPath}" --identity='Developer ID Application' --no-gatekeeper-assess` + + exec(command, (error, stdout, stderr) => { + if (error) { + console.error(`Error: ${error.message}`) + return + } + if (stderr) { + console.error(`Stderr: ${stderr}`) + return + } + console.log(`Output: ${stdout}`) + }) +} + +signApp() diff --git a/app/src/shared/utils/httpFileServer.ts b/app/src/shared/utils/httpFileServer.ts index 0180da3..ecb269a 100644 --- a/app/src/shared/utils/httpFileServer.ts +++ b/app/src/shared/utils/httpFileServer.ts @@ -116,6 +116,8 @@ export const startHttpFileServer = (electronApp: Electron.App, port: number = 77 sendDebugMessage('debug - name', req.file.originalname) sendDebugMessage('debug - path', req.file.path) + sendDebugMessage('debug - upload input path', fs.readdirSync(uploadsDir).join(', ')) + // @ts-expect-error -- test token sendDebugMessage('debug - token', req?.token) sendDebugMessage('debug - file', req.file) diff --git a/extension/src/pages/content/domains/github/ui.util.ts b/extension/src/pages/content/domains/github/ui.util.ts new file mode 100644 index 0000000..27125ee --- /dev/null +++ b/extension/src/pages/content/domains/github/ui.util.ts @@ -0,0 +1,257 @@ +import { GithubUploader } from '@pages/content/GithubUploader' +import { closeNativeApp, execCommand } from '@root/src/utils/command.util' + +const MAX_FILE_SIZE_MB = 50 // Maximum file size in MB for triggering compression +const SPINNER_MARGIN = 6 // Margin for spinner placement in pixels + +type DOMEventListener = (evt: Event) => void +const domEventListeners = new Map>>() + +export const addDomEventListener = ( + targetElement: Element, + eventType: string, + eventListener: DOMEventListener, + options?: boolean | AddEventListenerOptions, +) => { + let eventTypeListeners = domEventListeners.get(targetElement) + if (!eventTypeListeners) { + eventTypeListeners = new Map>() + domEventListeners.set(targetElement, eventTypeListeners) + } + + let eventListeners = eventTypeListeners.get(eventType) + if (!eventListeners) { + eventListeners = new Set() + eventTypeListeners.set(eventType, eventListeners) + } + + if (!eventListeners.has(eventListener)) { + eventListeners.add(eventListener) + targetElement.addEventListener(eventType, eventListener, options) + } +} + +export const clearAllDomEventListeners = () => { + domEventListeners.forEach((eventTypeListeners, element) => { + eventTypeListeners.forEach((eventListeners, eventType) => { + eventListeners.forEach(listener => { + element.removeEventListener(eventType, listener) + }) + eventListeners.clear() + }) + }) +} + +// Spinner Management Functions +function isSpinnerVisible(): boolean { + return document.querySelectorAll('.gvc-spinner').length > 0 +} + +function placeSpinnerNearText(textArea: HTMLTextAreaElement, searchText: string): void { + if (isSpinnerVisible()) { + return + } + + const spinner = document.createElement('div') + spinner.classList.add('gvc-spinner') + document.body.appendChild(spinner) + + // Position Calculation + const matchIndex = textArea.value.indexOf(searchText) + if (matchIndex !== -1) { + spinner.style.display = 'block' + spinner.style.position = 'absolute' + const textAreaRect = textArea.getBoundingClientRect() + spinner.style.left = `${textAreaRect.left + window.scrollX + SPINNER_MARGIN}px` + spinner.style.top = `${textAreaRect.top + window.scrollY + SPINNER_MARGIN}px` + } +} + +function showLoadingSpinner(textArea: HTMLTextAreaElement, loadingText: string = 'Loading'): void { + const loadingIndicator = `\n\n${loadingText}...\n\n` + const currentPosition = textArea.selectionStart ?? textArea.value.length + textArea.value = + textArea.value.substring(0, currentPosition) + loadingIndicator + textArea.value.substring(currentPosition) + textArea.selectionStart = textArea.selectionEnd = currentPosition + loadingIndicator.length + textArea.focus() + + placeSpinnerNearText(textArea, loadingText) +} + +function updateSpinnerLoadingText( + textArea: HTMLTextAreaElement, + currentLoadingText: string, + newLoadingText: string, +): void { + const currentIndicator = `${currentLoadingText}...` + const newIndicator = `${newLoadingText}...` + textArea.value = textArea.value.replace(currentIndicator, newIndicator) + + placeSpinnerNearText(textArea, newLoadingText) +} + +function removeVisibleSpinners(): void { + const spinners = document.querySelectorAll('.gvc-spinner') + spinners.forEach(spinner => spinner.remove()) +} + +function insertMarkdownLink( + textArea: HTMLTextAreaElement, + linkText: string, + linkUrl: string, + placeholderText: string = 'Loading', +): void { + const markdownLink = `[${linkText}](${linkUrl})` + const loadingIndicator = `${placeholderText}...` + textArea.value = textArea.value.replace(loadingIndicator, markdownLink) + + removeVisibleSpinners() + + const insertPosition = textArea.value.indexOf(markdownLink) + markdownLink.length + textArea.selectionStart = textArea.selectionEnd = insertPosition + textArea.focus() +} + +// Helper Functions +const getRepositoryId = (): string => { + const repoId = document.querySelector('meta[name="octolytics-dimension-repository_id"]')?.getAttribute('content') + if (!repoId) { + throw new Error('Repository ID not found') + } + return repoId +} + +const getAuthenticityToken = (): string => { + const token = document.querySelector('.js-data-upload-policy-url-csrf')?.getAttribute('value') + if (!token) { + throw new Error('Authenticity token not found') + } + return token +} + +const fileSizeToMB = (fileSize: number): string => { + return (fileSize / (1024 * 1024)).toFixed(2) +} + +const uploadFileToGithub = async (textAreaElement: HTMLTextAreaElement, fileBlob: Blob, fileName: string) => { + const uploader = new GithubUploader() + + if (fileBlob) { + const file = new File([fileBlob], fileName, { type: fileBlob.type }) + console.log('Uploading file:', fileName) + + const repoId = getRepositoryId() + const csrfToken = getAuthenticityToken() + const uniqueId = Math.floor(Math.random() * 1000000000) + + const imageResponse = await uploader.startImageUpload({ + imageName: `v-${uniqueId}-${file.name}`, + imageSize: file.size, + authenticity_token: csrfToken, + content_type: file.type, + repository_id: repoId, + file, + imageUploadCompleteCallback: () => console.log('Upload complete'), + }) + + if (!imageResponse) { + throw new Error('Invalid image response') + } + + if (!textAreaElement) { + console.warn( + 'Unable to locate the textarea for file placement. Please manually copy and paste this link:\n\nLink: [${file.name}](${imageResponse.href})', + ) + throw new Error('Textarea element not found') + } + + insertMarkdownLink( + textAreaElement, + `${file.name}__${fileSizeToMB(file.size)}MB`, + imageResponse.href, + `Uploading [${fileName}]`, + ) + + removeVisibleSpinners() + } +} + +export const getTextAreaElement = (inputElement: Element): HTMLTextAreaElement | undefined => { + const textAreaId = inputElement.id.substring(3) // Assuming the format is 'fc-[textAreaId]' + return document.getElementById(textAreaId) as HTMLTextAreaElement | undefined +} + +const processFileUpload = async (file: File, textAreaElement: HTMLTextAreaElement, event: Event) => { + if (file.size > MAX_FILE_SIZE_MB * 1024 * 1024) { + event.preventDefault() + event.stopPropagation() + + try { + showLoadingSpinner(textAreaElement, `Compressing [${file.name}]`) + const { file: compressedFile } = await execCommand('compress_file', { file }) + + console.log('compressedFile.name', compressedFile.name) + console.log('file.name', file.name) + + await updateSpinnerLoadingText( + textAreaElement, + `Compressing [${file.name}]`, + `Uploading [${compressedFile.name}]`, + ) + + return await uploadFileToGithub(textAreaElement, compressedFile, compressedFile.name) + } catch (err) { + console.error('Error in file compression:', err) + } + } +} + +const handleFileChange = async (files: FileList, textAreaElement: HTMLTextAreaElement, event: Event) => { + if (!textAreaElement) { + throw new Error('Textarea element not found') + } + + if (!files) { + throw new Error('No files found') + } + + for (const file of Array.from(files)) { + if (file.type.includes('video')) { + await processFileUpload(file, textAreaElement, event) + } + } + + // Close app after all files are processed, only if there were any files to process. + if (files.length > 0) { + await closeNativeApp() + } +} + +export const handleTextAreaDrop = (event: DragEvent, textAreaElement: HTMLTextAreaElement): void => { + if (event.dataTransfer) { + const files = event.dataTransfer.files + handleFileChange(files, textAreaElement, event) + } else { + console.warn('No files specified') + } +} + +export const handleFileInputElementChange = (event: Event, textAreaElement: HTMLTextAreaElement): void => { + const target = event.target as HTMLInputElement + const files = target.files + + if (files && files?.length > 0) { + handleFileChange(files, textAreaElement, event) + } else { + console.warn('No files specified') + } +} + +export const handleEventWithTextArea = ( + targetTextArea: HTMLTextAreaElement, + eventHandler: (event: E, textAreaElement: HTMLTextAreaElement) => void, +) => { + return (event: E) => { + eventHandler(event, targetTextArea) + } +} diff --git a/extension/src/pages/content/index.ts b/extension/src/pages/content/index.ts index 8060c07..1669ab3 100644 --- a/extension/src/pages/content/index.ts +++ b/extension/src/pages/content/index.ts @@ -1,316 +1,45 @@ -const PADDING = 6 -function showSpinnerAtText(textArea: HTMLTextAreaElement, searchText: string): void { - const spinner = document.createElement('div') - spinner.classList.add('gvc-spinner') - document.body.appendChild(spinner) - - // Calculate the position - const index = textArea.value.indexOf(searchText) - if (index !== -1) { - spinner.style.display = 'block' - spinner.style.position = 'absolute' - const rect = textArea.getBoundingClientRect() - spinner.style.left = `${rect.left + window.scrollX + PADDING}px` - spinner.style.top = `${rect.top + window.scrollY + PADDING}px` - } -} - -function displayLoadingWithSpinner(textArea: HTMLTextAreaElement, loadingText: string = 'Loading'): void { - const loadingIndicator = `\n\n${loadingText}...\n\n` - const currentPos = textArea.selectionStart || textArea.value.length - textArea.value = textArea.value.substring(0, currentPos) + loadingIndicator + textArea.value.substring(currentPos) - textArea.selectionStart = textArea.selectionEnd = currentPos + loadingIndicator.length - textArea.focus() - - showSpinnerAtText(textArea, `${loadingText}...`) -} - -function updateLoadingText(textArea: HTMLTextAreaElement, currentLoadingText: string, newLoadingText: string): void { - const currentLoadingIndicator = `${currentLoadingText}...` - const newLoadingIndicator = `${newLoadingText}...` - - // Replace the current loading indicator with the new loading indicator - textArea.value = textArea.value.replace(currentLoadingIndicator, newLoadingIndicator) - - // Move the spinner to the new text - showSpinnerAtText(textArea, newLoadingIndicator) -} - -function removeSpinner(): void { - const spinner = document.querySelector('.gvc-spinner') - if (spinner) { - spinner.remove() - } -} - -function replaceLoadingWithMarkdownLink( - textArea: HTMLTextAreaElement, - name: string, - href: string, - loadingText: string = 'Loading', -): void { - const markdownLink = `[${name}](${href})` - const loadingIndicator = `${loadingText}...` - - // Replace the loading indicator with the markdown link - textArea.value = textArea.value.replace(loadingIndicator, markdownLink) - - // Remove the spinner - removeSpinner() - - // Move the cursor after the inserted markdown link - const insertPos = textArea.value.indexOf(markdownLink) + markdownLink.length - textArea.selectionStart = textArea.selectionEnd = insertPos - textArea.focus() -} - -// function injectMarkdownLink(textArea: HTMLTextAreaElement, name: string, href: string): void { -// const markdownLink = `\n\n[${name}](${href})\n\n` - -// // Check if the browser supports `selectionStart` and `selectionEnd` - -// const startPos = textArea.selectionStart -// const endPos = textArea.selectionEnd -// const beforeText = textArea.value.substring(0, startPos) -// const afterText = textArea.value.substring(endPos) - -// // Insert the markdown link where the cursor is, or at the end if no selection -// textArea.value = beforeText + markdownLink + afterText - -// // Move the cursor to the end of the new link -// textArea.selectionStart = textArea.selectionEnd = startPos + markdownLink.length - -// // Optionally, focus the textarea -// textArea.focus() -// } - -const getRepositoryId = () => { - const repository_id = document - .querySelector('meta[name="octolytics-dimension-repository_id"]') - ?.getAttribute('content') - if (!repository_id) { - throw new Error('repository_id not found') - } - - return repository_id -} - -const getAuthenticityToken = () => { - const authenticity_token = document.querySelector('.js-data-upload-policy-url-csrf')?.getAttribute('value') - if (!authenticity_token) { - throw new Error('authenticity_token not found') - } - - return authenticity_token -} - -type EventListenerFunction = (evt: Event) => void - -const addEventListenerWrapper = (() => { - const eventListenersMap = new Map>>() - - return ( - element: Element, - event: string, - listener: EventListenerFunction, - options?: boolean | AddEventListenerOptions, - ) => { - let eventsMap = eventListenersMap.get(element) - if (!eventsMap) { - eventsMap = new Map>() - eventListenersMap.set(element, eventsMap) - } - - let listeners = eventsMap.get(event) - if (!listeners) { - listeners = new Set() - eventsMap.set(event, listeners) - } - - if (!listeners.has(listener)) { - listeners.add(listener) - element.addEventListener(event, listener, options) - } - } -})() - -const fileSizeToMB = fileSize => { - return (fileSize / (1024 * 1024)).toFixed(2) -} - ;(async () => { await import('@pages/content/ui') await import('@pages/content/injected') - const { execCommand } = await import('@root/src/utils/command.util') - const { GithubUploader } = await import('@pages/content/GithubUploader') - const githubUploader = new GithubUploader() - - const uploadFile = (textAreaElement: HTMLTextAreaElement, blob: Blob, fileName: string) => { - if (blob) { - const file = new File([blob], fileName, { type: blob.type }) - console.log('fileName', fileName) - - const repository_id = getRepositoryId() - const authenticity_token = getAuthenticityToken() - const randomNumber = Math.floor(Math.random() * 1000000000) - - githubUploader - .startImageUpload({ - imageName: `v-${randomNumber}-${file.name}`, - imageSize: file.size, - authenticity_token, - content_type: file.type, - repository_id, - file, - imageUploadCompleteCallback: () => { - console.log('Upload complete') - }, - }) - .then(imageResponse => { - console.log('imageResponse', imageResponse) - if (!imageResponse) { - throw new Error('imageResponse is invalid') - } - - if (!textAreaElement) { - window.alert( - `The github compression extension does not know where to place the file. Please copy and paste the following link into the textarea instead.\n\nLink: [${file.name}](${imageResponse.href})`, - ) - throw new Error('textAreaElement not found') - } - - replaceLoadingWithMarkdownLink( - textAreaElement, - `${file.name}__${fileSizeToMB(file.size)}MB`, - imageResponse.href, - `Uploading [${fileName}]`, - ) - console.debug('success') - }) - .finally(() => { - removeSpinner() - }) - } - } - - // const addDragAndDropListeners = () => { - // const textAreaElements = document.querySelectorAll('.js-upload-markdown-image textarea') - - // textAreaElements.forEach(textAreaElement => { - // // Prevent the default behavior for dragover - // addEventListenerWrapper(textAreaElement, 'dragover', (event: DragEvent) => { - // event.preventDefault() - // }) - - // // Handle the drop event - // addEventListenerWrapper(textAreaElement, 'drop', (event: DragEvent) => { - // event.preventDefault() - - // // Ensure that dataTransfer is not null - // if (event.dataTransfer) { - // const files = event.dataTransfer.files - - // if (files) { - // // Iterate over the files - // for (let i = 0; i < files.length; i++) { - // const file = files[i] - - // // Check if the file type is a video - // if (file.type.startsWith('video/')) { - // console.log('Video file dropped:', file.name) - - // // Additional handling for the file can be done here - // } - // } - // } - // } - // }) - // }) - // } - - const TRIGGER_SIZE = 50 * 1024 * 1024 // 100Mb - const addFileUploadListeners = () => { + const { + handleFileInputElementChange, + addDomEventListener, + clearAllDomEventListeners, + getTextAreaElement, + handleEventWithTextArea, + handleTextAreaDrop, + } = await import('@root/src/pages/content/domains/github/ui.util') + + const resetAllFileUploadListeners = () => { const fileInputs = document.querySelectorAll('input[type=file]') - console.log('fileInputs', fileInputs) + clearAllDomEventListeners() fileInputs.forEach(fileInput => { - addEventListenerWrapper(fileInput, 'change', (e: Event) => { - const target = e.target as HTMLInputElement - - if (!target || target?.type !== 'file') { - return - } - - if (target && target.type === 'file') { - const textAreaElement = document.querySelector(`.js-upload-markdown-image textarea`) as HTMLTextAreaElement - - if (!textAreaElement) { - throw new Error('textAreaElement not found') - } - - const files = target.files - if (!files) { - throw new Error('files not found') - } - - // Iterate over the FileList - for (let i = 0; i < files.length; i++) { - const file = files.item(i) - - // Ignore files that are small - if (file.size < TRIGGER_SIZE) return - - if (!file) { - throw new Error('file not found') - } - - if (file.type) { - e.stopPropagation() - e.preventDefault() - - // Check if the file type includes 'video' - if (file.type.includes('video')) { - e.stopPropagation() - e.preventDefault() - - displayLoadingWithSpinner(textAreaElement, `Compressing [${file.name}]`) - execCommand('compress_file', { - file, - }) - .then(({ file: compressedFile }) => { - console.log('compressedFile', compressedFile) - console.log('finish compressed video') - if (compressedFile) { - updateLoadingText( - textAreaElement, - `Compressing [${file.name}]`, - `Uploading [${compressedFile.name}]`, - ) - - uploadFile(textAreaElement, compressedFile, compressedFile.name) - } - }) - .catch(err => { - console.log('err', err) - }) - } - } - } - } + const textAreaElement = getTextAreaElement(fileInput) + if (!textAreaElement) { + throw new Error('Could not find text area element') + } + + // File Input + addDomEventListener(fileInput, 'change', handleEventWithTextArea(textAreaElement, handleFileInputElementChange)) + + // Drag and Drop + addDomEventListener(textAreaElement, 'dragover', (event: DragEvent) => { + event.stopPropagation() + event.preventDefault() }) + addDomEventListener(textAreaElement, 'drop', handleEventWithTextArea(textAreaElement, handleTextAreaDrop)) }) } // Select the node that will be observed for mutations const targetNode = document.querySelector('.pull-discussion-timeline') - console.log('selectNode', targetNode) // Callback function to execute when mutations are observed const callback = function (mutationsList) { - for (const mutation of mutationsList) { - console.log('mutation', mutation) - addFileUploadListeners() - // addDragAndDropListeners() + if (mutationsList.length > 0) { + // When any mutations are observed, add listeners to the file inputs + resetAllFileUploadListeners() } } @@ -321,10 +50,7 @@ const fileSizeToMB = fileSize => { observer.observe(targetNode, { childList: true, subtree: true, - attributeFilter: ['data-file-attachment-for'], }) - // init - addFileUploadListeners() - // addDragAndDropListeners() + resetAllFileUploadListeners() })() diff --git a/extension/src/utils/command.util.ts b/extension/src/utils/command.util.ts index 52c1d1d..d59295f 100644 --- a/extension/src/utils/command.util.ts +++ b/extension/src/utils/command.util.ts @@ -5,6 +5,10 @@ const COMMAND_MAP = { compress_file: sendFileToServer, } +export const closeNativeApp = async () => { + return stopSession() +} + export const execCommand = async ( commandName: T, commandOptions: Omit[0], 'token'>, @@ -18,8 +22,5 @@ export const execCommand = async ( }) console.debug('result', result) - const success = await stopSession() - console.debug('stop session success', success) - return result } diff --git a/yarn.lock b/yarn.lock index 372da15..091c104 100644 --- a/yarn.lock +++ b/yarn.lock @@ -632,7 +632,7 @@ fs-extra "^9.0.1" promise-retry "^2.0.1" -"@electron/osx-sign@1.0.5": +"@electron/osx-sign@1.0.5", "@electron/osx-sign@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@electron/osx-sign/-/osx-sign-1.0.5.tgz#0af7149f2fce44d1a8215660fd25a9fb610454d8" integrity sha512-k9ZzUQtamSoweGQDV2jILiRIHUu7lYlJ3c6IEmjv1hC17rclE+eb9U+f6UFlOOETo0JzY1HNlXy4YOlCvl+Lww==