diff --git a/assets/github.svg b/assets/github.svg new file mode 100644 index 0000000..5d720c8 --- /dev/null +++ b/assets/github.svg @@ -0,0 +1,3 @@ + + + diff --git a/package.json b/package.json index 491a221..d416ebc 100644 --- a/package.json +++ b/package.json @@ -12,14 +12,17 @@ "test": "jest --passWithNoTests" }, "dependencies": { + "@octokit/rest": "^22.0.0", "acorn": "^8.14.0", "acorn-walk": "^8.3.4", "d3-array": "^3.2.4", "escodegen": "^2.1.0", + "framer-motion": "^12.23.12", "plasmo": "0.89.4", "react": "18.2.0", "react-dom": "18.2.0", - "tailwindcss": "3.4.1" + "tailwindcss": "3.4.1", + "uuid": "^11.1.0" }, "devDependencies": { "@babel/preset-env": "^7.26.9", @@ -35,6 +38,7 @@ "@types/node": "20.11.5", "@types/react": "18.2.48", "@types/react-dom": "18.2.18", + "@types/uuid": "^10.0.0", "autoprefixer": "^10.4.20", "babel-jest": "^29.7.0", "husky": "^9.1.7", diff --git a/plasmo.config.ts b/plasmo.config.ts index e812a69..8e344c6 100644 --- a/plasmo.config.ts +++ b/plasmo.config.ts @@ -4,6 +4,10 @@ export default { "storage", "cookies", "webRequest" + ], + host_permissions: [ + "https://api.github.com/*", + "https://github.com/*" ] } } \ No newline at end of file diff --git a/src/components/DialogPanel.tsx b/src/components/DialogPanel.tsx new file mode 100644 index 0000000..a0468eb --- /dev/null +++ b/src/components/DialogPanel.tsx @@ -0,0 +1,306 @@ +import React, { useCallback, useEffect, useRef, useState } from "react"; +import { motion } from "framer-motion"; +import faviconIco from "data-base64:../../assets/icon.png"; + +// Close button component +const CloseButton = ({ close }: { close: () => void }) => { + return ( + + × + + ); +}; + +// Arrow head component for resize handles +const ArrowHead = ({ direction }: { direction: 'top' | 'right' | 'bottom' | 'left' }) => { + const getArrowStyle = () => { + switch (direction) { + case 'top': + return { transform: 'rotate(0deg)' }; + case 'right': + return { transform: 'rotate(90deg)' }; + case 'bottom': + return { transform: 'rotate(180deg)' }; + case 'left': + return { transform: 'rotate(270deg)' }; + } + }; + + return ( +
+
+
+ ); +}; + +// Dialog panel component with drag and resize functionality +export const DialogPanel = ({ + children, + overlay, + close +}: { + children: React.ReactNode, + overlay?: React.ReactNode, + close?: () => void +}) => { + const [panelSize, setPanelSize] = useState<{ width: number; height: number }>({ width: 550, height: 330 }); + const resizingRef = useRef<{ + startX: number; + startY: number; + startW: number; + startH: number; + startLeft: number; + startTop: number; + viewportW: number; + viewportH: number; + edge: 'top'|'right'|'bottom'|'left'; + } | null>(null); + + const [pos, setPos] = useState<{ top: number; left: number }>(() => { + const minMargin = 4; + const bottom = 130; + const right = 80; + const top = Math.max(minMargin, window.innerHeight - bottom - 365); + const left = Math.max(minMargin, window.innerWidth - right - 550); + return { top, left }; + }); + + const draggingRef = useRef<{ startX: number; startY: number; startTop: number; startLeft: number } | null>(null); + + const onMouseMove = useCallback((e: MouseEvent) => { + if (!resizingRef.current) return; + const dx = e.clientX - resizingRef.current.startX; + const dy = e.clientY - resizingRef.current.startY; + + const minW = 320; + const minH = 200; + const maxW = Math.min(window.innerWidth * 0.92, 900); + const maxH = Math.min(window.innerHeight * 0.7, 800); + + let newW = resizingRef.current.startW; + let newH = resizingRef.current.startH; + let newLeft = pos.left; + let newTop = pos.top; + + switch (resizingRef.current.edge) { + case 'right': + newW = resizingRef.current.startW + dx; + break; + case 'left': + newW = resizingRef.current.startW - dx; + newLeft = resizingRef.current.startLeft + dx; + break; + case 'bottom': + newH = resizingRef.current.startH + dy; + break; + case 'top': + newH = resizingRef.current.startH - dy; + newTop = resizingRef.current.startTop + dy; + break; + } + + // Apply constraints + newW = Math.max(minW, Math.min(maxW, newW)); + newH = Math.max(minH, Math.min(maxH, newH)); + newLeft = Math.max(0, Math.min(window.innerWidth - newW, newLeft)); + newTop = Math.max(0, Math.min(window.innerHeight - newH, newTop)); + + setPanelSize({ width: newW, height: newH }); + if (newLeft !== pos.left || newTop !== pos.top) { + setPos({ left: newLeft, top: newTop }); + } + }, [panelSize.width, panelSize.height, pos.left, pos.top]); + + const onMouseMoveDrag = useCallback((e: MouseEvent) => { + if (!draggingRef.current) return; + const dx = e.clientX - draggingRef.current.startX; + const dy = e.clientY - draggingRef.current.startY; + + const newLeft = draggingRef.current.startLeft + dx; + const newTop = draggingRef.current.startTop + dy; + + const minMargin = 4; + const maxLeft = window.innerWidth - panelSize.width - minMargin; + const maxTop = window.innerHeight - panelSize.height - minMargin; + + setPos({ + left: Math.max(minMargin, Math.min(maxLeft, newLeft)), + top: Math.max(minMargin, Math.min(maxTop, newTop)) + }); + }, [panelSize.width, panelSize.height]); + + const endResize = useCallback(() => { + resizingRef.current = null; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + + // Remove event listeners + document.removeEventListener('mousemove', onMouseMove); + document.removeEventListener('mouseup', endResize); + }, [onMouseMove]); + + const endDrag = useCallback(() => { + draggingRef.current = null; + document.body.style.cursor = ''; + document.body.style.userSelect = ''; + + // Remove event listeners + document.removeEventListener('mousemove', onMouseMoveDrag); + document.removeEventListener('mouseup', endDrag); + }, [onMouseMoveDrag]); + + const startResize = useCallback((e: React.MouseEvent, edge: 'top'|'right'|'bottom'|'left') => { + e.preventDefault(); + resizingRef.current = { + startX: e.clientX, + startY: e.clientY, + startW: panelSize.width, + startH: panelSize.height, + startLeft: pos.left, + startTop: pos.top, + viewportW: window.innerWidth, + viewportH: window.innerHeight, + edge + }; + document.body.style.cursor = edge === 'left' || edge === 'right' ? 'ew-resize' : 'ns-resize'; + document.body.style.userSelect = 'none'; + + // Add event listeners + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', endResize); + }, [panelSize.width, panelSize.height, pos.left, pos.top, onMouseMove, endResize]); + + const startDrag = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + draggingRef.current = { + startX: e.clientX, + startY: e.clientY, + startTop: pos.top, + startLeft: pos.left + }; + document.body.style.cursor = 'grabbing'; + document.body.style.userSelect = 'none'; + + // Add event listeners + document.addEventListener('mousemove', onMouseMoveDrag); + document.addEventListener('mouseup', endDrag); + }, [pos.top, pos.left, onMouseMoveDrag, endDrag]); + + useEffect(() => { + const handleResize = () => { + const minMargin = 4; + const bottom = 130; + const right = 80; + const newTop = Math.max(minMargin, window.innerHeight - bottom - 365); + const newLeft = Math.max(minMargin, window.innerWidth - right - 550); + + if (newLeft !== pos.left || newTop !== pos.top) { + setPos({ left: newLeft, top: newTop }); + } + }; + + window.addEventListener('resize', handleResize); + return () => window.removeEventListener('resize', handleResize); + }, [panelSize.width, panelSize.height, pos.left, pos.top]); + + useEffect(() => () => endDrag(), [endDrag]); + + return ( + + {close && } +
+
+ + Mantis + + + Mantis + +
+
+ {children} +
+
+ + {/* Resize handles */} +
startResize(e, 'top')} + className="absolute top-0 left-0 right-0 h-2 cursor-n-resize group" + style={{ transform: 'translateY(-1px)' }} + title="Resize" + > + +
+
startResize(e, 'bottom')} + className="absolute bottom-0 left-0 right-0 h-2 cursor-s-resize group" + style={{ transform: 'translateY(1px)' }} + title="Resize" + > + +
+
startResize(e, 'left')} + className="absolute left-0 top-0 bottom-0 w-2 cursor-w-resize group" + style={{ transform: 'translateX(-1px)' }} + title="Resize" + > + +
+
startResize(e, 'right')} + className="absolute right-0 top-0 bottom-0 w-2 cursor-e-resize group" + style={{ transform: 'translateX(1px)' }} + title="Resize" + > + +
+ + {overlay && ( +
+ {overlay} +
+ )} +
+ ); +}; diff --git a/src/connection_manager.tsx b/src/connection_manager.tsx index 2e48d7a..164b69d 100644 --- a/src/connection_manager.tsx +++ b/src/connection_manager.tsx @@ -6,9 +6,10 @@ import { GoogleScholarConnection } from "./connections/googleScholar/connection" import { WikipediaSegmentConnection } from "./connections/wikipediaSegment/connection"; import { GmailConnection } from "./connections/Gmail/connection"; import { LinkedInConnection } from "./connections/Linkedin/connection"; +import { GitBlameConnection } from "./connections/gitblame/connection"; -export const CONNECTIONS = [GmailConnection, WikipediaSegmentConnection, WikipediaReferencesConnection, GoogleConnection, PubmedConnection, GoogleDocsConnection, GoogleScholarConnection,LinkedInConnection]; +export const CONNECTIONS = [GmailConnection, WikipediaSegmentConnection, WikipediaReferencesConnection, GoogleConnection, PubmedConnection, GoogleDocsConnection, GoogleScholarConnection, LinkedInConnection, GitBlameConnection]; export const searchConnections = (url: string, ) => { const connections = CONNECTIONS.filter(connection => connection.trigger(url)); diff --git a/src/connections/gitblame/connection.tsx b/src/connections/gitblame/connection.tsx new file mode 100644 index 0000000..9b3048c --- /dev/null +++ b/src/connections/gitblame/connection.tsx @@ -0,0 +1,638 @@ +import type { MantisConnection, injectUIType, onMessageType, registerListenersType, setProgressType, establishLogSocketType } from "../types"; +import { GenerationProgress } from "../types"; +import { Octokit } from "@octokit/rest"; +import { saveGitHubToken, getGitHubToken } from "../../github-token-manager"; + +import githubIcon from "data-base64:../../../assets/github.svg"; +import { getSpacePortal, registerAuthCookies, reqSpaceCreation } from "../../driver"; + +// Define types for better type safety +type BlameMapEntry = { + filename: string; + lineNumber: number; + commit: string; + author?: string; + date?: string; + lineContent: string; +}; + +type GitHubBlameRange = { + startingLine: number; + endingLine: number; + age: number; + commit: { + oid: string; + author: { + name: string; + date: string; + }; + }; +}; + +const trigger = (url: string) => { + return url.includes("github.com") && (url.includes("/blob/") || url.includes("/pull/")); +} + +// Check if a repository is public (no authentication required) +async function isRepositoryPublic(owner: string, repo: string): Promise { + try { + // Try to access the repository without authentication + const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`); + + // Check if we got a valid JSON response + const contentType = response.headers.get('content-type'); + if (!contentType || !contentType.includes('application/json')) { + console.warn('GitHub API returned non-JSON response, assuming private repository'); + return false; + } + + if (response.status === 200) { + try { + const data = await response.json(); + // Check if the repository is actually public + return !data.private; + } catch (jsonError) { + console.warn('Failed to parse JSON response:', jsonError); + return false; + } + } + + // If we get a 404, the repo might not exist or be private + if (response.status === 404) { + console.warn('Repository not found, assuming private'); + return false; + } + + // If we get rate limited or other errors, assume private + if (response.status === 403 || response.status === 429) { + console.warn('GitHub API rate limited or forbidden, assuming private'); + return false; + } + + return false; + } catch (error) { + console.warn('Could not determine repository visibility:', error); + return false; // Assume private if we can't determine + } +} + +// Add this function to handle token input +async function promptForGitHubToken(): Promise { + return new Promise((resolve) => { + const token = prompt( + "Please enter your GitHub Personal Access Token:\n\n" + + "This token will be stored locally in your browser and used to access GitHub's API.\n" + + "You can create a token at: https://github.com/settings/tokens\n\n" + + "Required permissions: repo (for private repos) or public_repo (for public repos only)", + "" + ); + + if (token && token.trim()) { + resolve(token.trim()); + } else { + resolve(""); + } + }); +} + +// Add this function to validate token +async function validateGitHubToken(token: string): Promise { + try { + const octokit = new Octokit({ auth: token }); + const { data } = await octokit.rest.users.getAuthenticated(); + return !!data.login; + } catch (error) { + console.warn('Invalid GitHub token:', error); + return false; + } +} + +const createSpace = async (injectUI: injectUIType, setProgress: setProgressType, onMessage: onMessageType, registerListeners: registerListenersType, establishLogSocket: establishLogSocketType) => { + setProgress(GenerationProgress.GATHERING_DATA); + + // Extract repository information from the URL more robustly + const url = new URL(window.location.href); + const pathParts = url.pathname.split('/'); + const owner = pathParts[1]; + const repo = pathParts[2]; + + // More robust branch and file path extraction + let branch = "main"; + let filePath = ""; + + // Check if this is a PR page + const isPR = pathParts.includes('pull'); + const prIndex = pathParts.indexOf('pull'); + + if (isPR && prIndex !== -1 && prIndex + 1 < pathParts.length) { + // This is a PR page - extract PR number and get the head branch + const prNumber = pathParts[prIndex + 1]; + console.log(`Processing PR #${prNumber} for ${owner}/${repo}`); + + // For PR pages, we need to get the head branch from the PR data + // We'll try to extract it from the page or use a fallback + const prBranchMeta = document.querySelector('meta[name="pr-head-branch"]'); + if (prBranchMeta) { + branch = prBranchMeta.getAttribute('content') || "main"; + } else { + // Fallback: try to get branch from the page content + const branchElement = document.querySelector('[data-testid="head-ref"]') || + document.querySelector('.head-ref') || + document.querySelector('[title*="head"]'); + if (branchElement) { + branch = branchElement.textContent?.trim() || "main"; + } + } + + // For PR pages, we need to determine the file path differently + // Check if we're viewing a specific file in the PR + const filePathMeta = document.querySelector('meta[name="file-path"]'); + if (filePathMeta) { + filePath = filePathMeta.getAttribute('content') || ""; + } else { + // Try to extract from URL if it's a file view in PR + const filesIndex = pathParts.indexOf('files'); + if (filesIndex !== -1 && filesIndex + 1 < pathParts.length) { + filePath = pathParts.slice(filesIndex + 1).join('/'); + } + } + } else { + // This is a regular blob page - use existing logic + // Look for branch name in meta tags first (more reliable) + const branchMeta = document.querySelector('meta[name="branch-name"]'); + if (branchMeta) { + branch = branchMeta.getAttribute('content') || "main"; + } else { + // Fallback to URL parsing, but handle branch names with slashes more intelligently + const blobIndex = pathParts.indexOf('blob'); + if (blobIndex !== -1 && blobIndex + 1 < pathParts.length) { + const afterBlob = pathParts.slice(blobIndex + 1); + + if (afterBlob.length >= 2) { + // For better branch detection, we need to be smarter about where the branch ends + // GitHub URLs typically have the pattern: /owner/repo/blob/branch/path/to/file + // But branch names can contain slashes, so we need to find the right split point + + // Try to find the file extension to determine where the file path starts + let filePathStartIndex = 0; + for (let i = 0; i < afterBlob.length; i++) { + const part = afterBlob[i]; + // If this part contains a file extension, it's likely part of the file path + if (part.includes('.') && !part.includes('/')) { + filePathStartIndex = i; + break; + } + // If this part looks like a commit hash (40+ hex chars), it's likely a commit, not a branch + if (/^[a-f0-9]{40,}$/.test(part)) { + filePathStartIndex = i; + break; + } + } + + if (filePathStartIndex > 0) { + // We found a likely file path start, everything before is the branch + branch = afterBlob.slice(0, filePathStartIndex).join('/'); + filePath = afterBlob.slice(filePathStartIndex).join('/'); + } else { + // Fallback: assume first part is branch, rest is file path + // This handles cases like "feature/new-feature" as branch name + branch = afterBlob[0]; + filePath = afterBlob.slice(1).join('/'); + } + } else if (afterBlob.length === 1) { + // Only one part after blob, assume it's the branch + branch = afterBlob[0]; + filePath = ""; + } + } + } + } + + // If we still don't have a file path, try to extract it from the page + if (!filePath) { + const filePathMeta = document.querySelector('meta[name="file-path"]'); + if (filePathMeta) { + filePath = filePathMeta.getAttribute('content') || ""; + } + } + + console.log(`Processing repository: ${owner}/${repo}, branch: ${branch}, file: ${filePath}`); + + // Check if repository is public first + const isPublic = await isRepositoryPublic(owner, repo); + + let githubToken: string | undefined; + let octokit: Octokit; + + if (isPublic) { + // For public repos, we can work without authentication + console.log('Repository is public, proceeding without authentication'); + octokit = new Octokit(); + + // However, some GitHub API operations (like GraphQL) may still require authentication + // We'll try without auth first, but fall back to asking for a token if needed + } else { + // For private repos, we need authentication + console.log('Repository is private, authentication required'); + + // Check if user has a GitHub token + githubToken = await getGitHubToken(); + + if (!githubToken) { + // Prompt user for token + githubToken = await promptForGitHubToken(); + + if (!githubToken) { + throw new Error('GitHub Personal Access Token is required for private repositories.'); + } + + // Validate the token + const isValid = await validateGitHubToken(githubToken); + if (!isValid) { + throw new Error('Invalid GitHub Personal Access Token. Please check your token and try again.'); + } + + // Save the valid token + await saveGitHubToken(githubToken); + } + + octokit = new Octokit({ auth: githubToken }); + } + + try { + // Get file blame information with better error handling + let blameData: BlameMapEntry[] = []; + + try { + blameData = await getFileBlame(octokit, owner, repo, filePath, branch); + } catch (blameError) { + console.warn('Failed to get blame data:', blameError); + + // If we get JSON parsing errors, it's likely an authentication issue + if (blameError.message && (blameError.message.includes('Unexpected token') || blameError.message.includes(''))) { + console.log('Detected authentication issue, prompting for token...'); + + // Ask for a token + const fallbackToken = await promptForGitHubToken(); + if (fallbackToken) { + const isValid = await validateGitHubToken(fallbackToken); + if (isValid) { + await saveGitHubToken(fallbackToken); + const authenticatedOctokit = new Octokit({ auth: fallbackToken }); + + try { + blameData = await getFileBlame(authenticatedOctokit, owner, repo, filePath, branch); + console.log('Successfully retrieved blame data with authentication'); + } catch (retryError) { + console.error('Failed to get blame data even with authentication:', retryError); + throw new Error('Unable to retrieve blame data. Please check your GitHub token permissions.'); + } + } else { + throw new Error('Invalid GitHub token. Please check your token and try again.'); + } + } else { + throw new Error('GitHub Personal Access Token is required for this repository.'); + } + } else { + throw blameError; + } + } + + // If we still have no blame data, try one more time with authentication + if (blameData.length === 0 && !githubToken) { + console.log('No blame data received, this might require authentication. Prompting for token...'); + + // Ask for a token even for public repos if GraphQL operations fail + const fallbackToken = await promptForGitHubToken(); + if (fallbackToken) { + const isValid = await validateGitHubToken(fallbackToken); + if (isValid) { + await saveGitHubToken(fallbackToken); + const authenticatedOctokit = new Octokit({ auth: fallbackToken }); + + try { + const retryBlameData = await getFileBlame(authenticatedOctokit, owner, repo, filePath, branch); + + if (retryBlameData.length > 0) { + console.log('Successfully retrieved blame data with authentication'); + blameData = retryBlameData; + } + } catch (retryError) { + console.error('Failed to get blame data even with authentication:', retryError); + throw new Error('Unable to retrieve blame data. Please check your GitHub token permissions.'); + } + } else { + throw new Error('Invalid GitHub token. Please check your token and try again.'); + } + } else { + throw new Error('GitHub Personal Access Token is required for this repository.'); + } + } + + // Get additional repository information + const repoInfo = await getRepositoryInfo(octokit, owner, repo); + + // Combine data for space creation + const extractedData = blameData.map(entry => ({ + filename: entry.filename, + lineNumber: entry.lineNumber, + commit: entry.commit, + author: entry.author, + date: entry.date, + lineContent: entry.lineContent, + repository: `${owner}/${repo}`, + branch: branch, + isPR: isPR, + prNumber: isPR ? pathParts[prIndex + 1] : undefined + })); + + // Add repository metadata + if (repoInfo) { + extractedData.push({ + filename: "repository_info", + lineNumber: 0, + commit: "metadata", + author: repoInfo.owner.login, + date: repoInfo.created_at, + lineContent: `Repository: ${repoInfo.full_name}, Description: ${repoInfo.description || 'No description'}, Language: ${repoInfo.language || 'Unknown'}`, + repository: `${owner}/${repo}`, + branch: branch, + isPR: isPR, + prNumber: isPR ? pathParts[prIndex + 1] : undefined + }); + } + + console.log(`Extracted ${extractedData.length} blame entries`); + + setProgress(GenerationProgress.CREATING_SPACE); + + const spaceData = await reqSpaceCreation(extractedData, { + "filename": "text", + "lineNumber": "number", + "commit": "text", + "author": "text", + "date": "date", + "lineContent": "semantic", + "repository": "text", + "branch": "text", + "isPR": "boolean", + "prNumber": "text" + }, establishLogSocket, `GitBlame: ${owner}/${repo}/${filePath}${isPR ? ` (PR #${pathParts[prIndex + 1]})` : ''}`); + + setProgress(GenerationProgress.INJECTING_UI); + + const spaceId = spaceData.space_id; + const createdWidget = await injectUI(spaceId, onMessage, registerListeners); + + setProgress(GenerationProgress.COMPLETED); + + return { spaceId, createdWidget }; + + } catch (error) { + console.error('Error creating GitBlame space:', error); + + // Provide more helpful error messages + if (error.message && error.message.includes('Unexpected token')) { + throw new Error('GitHub API returned an invalid response. This usually means authentication is required. Please provide a valid GitHub Personal Access Token.'); + } + + if (error.message && error.message.includes('')) { + throw new Error('GitHub API returned an HTML error page. This usually means rate limiting or authentication issues. Please try again later or provide a valid GitHub Personal Access Token.'); + } + + throw error; + } +} + +async function getFileBlame(octokit: Octokit, owner: string, repo: string, path: string, branch: string): Promise { + try { + // Use GitHub's GraphQL API for proper blame data + const query = ` + query($owner: String!, $repo: String!, $path: String!, $ref: String!) { + repository(owner: $owner, name: $repo) { + object(expression: $ref) { + ... on Commit { + blame(path: $path) { + ranges { + startingLine + endingLine + age + commit { + oid + author { + name + date + } + } + } + } + } + } + } + } + `; + + const variables = { + owner, + repo, + path, + ref: branch + }; + + // Make GraphQL request and get file content in parallel + const [response, fileContentResponse] = await Promise.all([ + octokit.graphql(query, variables), + octokit.rest.repos.getContent({ owner, repo, path, ref: branch }) + ]); + + // Check if we got HTML error pages instead of JSON + if (typeof response === 'string' && response.includes('')) { + throw new Error('GitHub API returned an HTML error page. This usually means rate limiting or authentication issues.'); + } + + // Check if the response is valid JSON + if (typeof response === 'string') { + try { + JSON.parse(response); + } catch (jsonError) { + throw new Error('GitHub API returned invalid JSON response. This usually means authentication is required.'); + } + } + + // Type the GraphQL response properly and validate it's not HTML + const typedResponse = response as any; + + // Check if we got a valid response structure + if (!typedResponse || typeof typedResponse !== 'object') { + console.warn('Invalid GraphQL response structure:', typedResponse); + return []; + } + + if (!typedResponse.repository || !typedResponse.repository.object) { + console.warn('Repository or object not found in GraphQL response'); + return []; + } + + const blameData = typedResponse.repository.object.blame?.ranges || []; + + if (Array.isArray(fileContentResponse.data)) { + // This shouldn't happen for a file path, but handle it + return []; + } + + // Check if it's a file (not a symlink or submodule) + if (fileContentResponse.data.type !== 'file') { + console.warn(`Path ${path} is not a file (type: ${fileContentResponse.data.type})`); + return []; + } + + // Check if we got HTML error pages instead of file content + if (typeof fileContentResponse.data === 'string' && (fileContentResponse.data as string).includes('')) { + throw new Error('GitHub API returned an HTML error page when fetching file content. This usually means rate limiting or authentication issues.'); + } + + // Check if the file content response is valid + if (!fileContentResponse.data || !fileContentResponse.data.content) { + throw new Error('GitHub API returned invalid file content response. This usually means authentication is required.'); + } + + const content = Buffer.from(fileContentResponse.data.content, 'base64').toString('utf-8'); + const lines = content.split('\n'); + + // Convert GraphQL response to our format + const result: BlameMapEntry[] = []; + + for (const range of blameData) { + const { startingLine, endingLine, commit } = range; + + // Add each line in the range + for (let lineNum = startingLine; lineNum <= endingLine; lineNum++) { + if (lineNum > 0 && lineNum <= lines.length) { + result.push({ + filename: path, + lineNumber: lineNum, + commit: commit.oid, + author: commit.author?.name, + date: commit.author?.date, + lineContent: lines[lineNum - 1] || '' + }); + } + } + } + + // Sort by line number + return result.sort((a, b) => a.lineNumber - b.lineNumber); + + } catch (error) { + console.warn(`Could not get blame for ${path}:`, error); + + // Check if the error is due to authentication issues + if (error.message && error.message.includes('Unexpected token')) { + console.warn('This appears to be an authentication issue. Please provide a valid GitHub token.'); + } + + if (error.message && error.message.includes('')) { + console.warn('GitHub API returned an HTML error page. This usually means rate limiting or authentication issues.'); + } + + return []; + } +} + +async function getRepositoryInfo(octokit: Octokit, owner: string, repo: string) { + try { + const { data } = await octokit.rest.repos.get({ + owner, + repo + }); + return data; + } catch (error) { + console.warn(`Could not get repository info for ${owner}/${repo}:`, error); + return null; + } +} + +const injectUI = async (space_id: string, onMessage: onMessageType, registerListeners: registerListenersType) => { + // Find the GitHub file header to inject our UI + // For PR pages, we need to look in different places + const isPR = window.location.href.includes('/pull/'); + + let fileHeader; + if (isPR) { + // For PR pages, look for the file header in the diff view + fileHeader = document.querySelector('.file-header') || + document.querySelector('.Box-header') || + document.querySelector('.d-flex.flex-column.flex-md-row') || + document.querySelector('[data-testid="file-header"]') || + document.querySelector('.js-file-header'); + } else { + // For regular blob pages, use the standard selectors + fileHeader = document.querySelector('.file-header') || + document.querySelector('.Box-header') || + document.querySelector('.d-flex.flex-column.flex-md-row'); + } + + if (!fileHeader) { + throw new Error('Could not find GitHub file header'); + } + + // Container for everything + const div = document.createElement("div"); + + // Toggle switch wrapper + const label = document.createElement("label"); + label.classList.add("inline-flex", "items-center", "cursor-pointer", "ml-4", "mr-4"); + + // Checkbox as toggle + const checkbox = document.createElement("input"); + checkbox.type = "checkbox"; + checkbox.classList.add("hidden"); + + // Text container with GitHub-style styling + const textContainer = document.createElement("span"); + textContainer.innerText = isPR ? "Mantis GitBlame (PR)" : "Mantis GitBlame"; + textContainer.classList.add("font-semibold", "text-sm"); + // Use CSS custom properties for gradient text since Tailwind doesn't support it + textContainer.style.background = "linear-gradient(90deg, #0366d6, #28a745)"; + textContainer.style.backgroundClip = "text"; + textContainer.style.webkitTextFillColor = "transparent"; + + await registerAuthCookies(); + + const iframeScalerParent = await getSpacePortal(space_id, onMessage, registerListeners); + iframeScalerParent.classList.add("hidden"); + + // Toggle behavior + checkbox.addEventListener("change", () => { + if (checkbox.checked) { + iframeScalerParent.classList.remove("hidden"); + textContainer.style.background = "linear-gradient(90deg, #28a745, #0366d6)"; + } else { + iframeScalerParent.classList.add("hidden"); + textContainer.style.background = "linear-gradient(90deg, #0366d6, #28a745)"; + } + textContainer.style.backgroundClip = "text"; + }); + + // Assemble elements + label.appendChild(textContainer); + label.appendChild(checkbox); + div.appendChild(label); + + // Insert the iframe after the file header + fileHeader.parentNode?.insertBefore(iframeScalerParent, fileHeader.nextSibling); + + // Insert into the file header + fileHeader.appendChild(div); + + return div; +} + +export const GitBlameConnection: MantisConnection = { + name: "GitBlame", + description: "Builds spaces based on Git blame information from GitHub repositories", + icon: githubIcon, + trigger: trigger, + createSpace: createSpace, + injectUI: injectUI, +} diff --git a/src/content.tsx b/src/content.tsx index 82d155f..ac4258a 100644 --- a/src/content.tsx +++ b/src/content.tsx @@ -8,6 +8,8 @@ import type { LogMessage, MantisConnection } from "./connections/types"; import { GenerationProgress, Progression } from "./connections/types"; import { addSpaceToCache, deleteSpacesWhere, getCachedSpaces } from "./persistent"; import { refetchAuthCookies } from "./driver"; +import { motion, AnimatePresence} from "framer-motion"; +import { DialogPanel } from "./components/DialogPanel"; export const config: PlasmoCSConfig = { matches: [""], @@ -41,31 +43,24 @@ const sanitizeWidget = (widget: HTMLElement, connection: MantisConnection) => { // Exits the dialog const CloseButton = ({ close }: { close: () => void }) => { - return ; -}; - -// Dialog util -const DialogHeader = ({ children }: { children: React.ReactNode }) => { return ( -
-
- {children} -
-
+ + × + ); -} +}; // Displays a navigation arrowhead const ArrowHead = ({ left, disabled }: { left: boolean, disabled: boolean }) => { return ( // Main dialog that appears when creating a space const ConnectionDialog = ({ activeConnections, close }: { activeConnections: MantisConnection[], close: () => void }) => { + const [showInitialText, setShowInitialText] = useState(true); const [state, setState] = useState(GenerationProgress.GATHERING_DATA); // Progress of creation process const [errorText, setErrorText] = useState(null); const [running, setRunning] = useState(false); // If the creation process is running @@ -92,6 +88,45 @@ const ConnectionDialog = ({ activeConnections, close }: { activeConnections: Man const [logMessages, setLogMessages] = useState([]); const logContainerRef = useRef(null); + const [showOverlay, setShowOverlay] = useState(true); + + const overlayElement = ( + + {showOverlay && ( + setShowOverlay(false)} + > + + + + Mantis + + + + )} + + ); // Check if the log scroll is at the bottom const isScrolledToBottom = () => { @@ -246,6 +281,13 @@ const ConnectionDialog = ({ activeConnections, close }: { activeConnections: Man const progressPercent = Progression.indexOf(state) / (Progression.length - 1); + useEffect(() => { + const timer = setTimeout(() => { + setShowInitialText(false); + }, 1000); + return () => clearTimeout(timer); + }, []); + // On opening useEffect(() => { // Make sure the user knows that they will be overwriting the existing space on the URL @@ -276,8 +318,7 @@ const ConnectionDialog = ({ activeConnections, close }: { activeConnections: Man if (state === GenerationProgress.COMPLETED) { return ( - - + {connectionData}

@@ -311,67 +352,107 @@ const ConnectionDialog = ({ activeConnections, close }: { activeConnections: Man

- + ); } if (state === GenerationProgress.FAILED) { return ( - - - {connectionData} -
{errorText}
- -
+ + {connectionData} +
{errorText}
+ +
); } if (running) { return ( - - {connectionData} - {state !== GenerationProgress.CREATING_SPACE ? - // Raw progress bar with no logs - (
-
-
-
- {state} -
) - - // Progress bar + logs - : (
-
-
- Progress - {state} -
-
-
-
-
- -
-
- Log Messages -
- - {WSStatus} + + {showInitialText ? ( + + + Mantis + + + ) : ( + + {connectionData} + + {state !== GenerationProgress.CREATING_SPACE ? ( +
+
+

Create New Space

+
+ + +
+
+ +
+
+
+ {activeConnections[connectionIdx].name} +
+
+

{activeConnections[connectionIdx].name}

+

{activeConnections[connectionIdx].description}

+
+
+ +
+ +
+
-
+ ) : ( +
+
+
+

Creating Space

+
+ {Progression.indexOf(state) + 1} of {Progression.length} steps +
+
{logMessages.length === 0 ? (
No log messages yet
@@ -393,16 +474,17 @@ const ConnectionDialog = ({ activeConnections, close }: { activeConnections: Man
)) )} -
-
+
+
)} - + + )} + ); } return ( - - +
{activeConnections.length > 1 && (
@@ -454,10 +536,10 @@ const ConnectionDialog = ({ activeConnections, close }: { activeConnections: Man onClick={() => runConnection(activeConnection)} disabled={!isAuthenticated} > - Create + Create Space - + ); }; @@ -521,10 +603,10 @@ const PlasmoFloatingButton = () => { return ( <> {open && ( setOpen(false)} /> diff --git a/src/github-token-manager.ts b/src/github-token-manager.ts new file mode 100644 index 0000000..c50f6a8 --- /dev/null +++ b/src/github-token-manager.ts @@ -0,0 +1,20 @@ +// Functions to manage GitHub token storage +export const GITHUB_TOKEN_KEY = 'github_personal_access_token'; + +export async function saveGitHubToken(token: string): Promise { + await chrome.storage.local.set({ [GITHUB_TOKEN_KEY]: token }); +} + +export async function getGitHubToken(): Promise { + const result = await chrome.storage.local.get([GITHUB_TOKEN_KEY]); + return result[GITHUB_TOKEN_KEY] || null; +} + +export async function hasGitHubToken(): Promise { + const token = await getGitHubToken(); + return !!token; +} + +export async function clearGitHubToken(): Promise { + await chrome.storage.local.remove([GITHUB_TOKEN_KEY]); +} diff --git a/yarn.lock b/yarn.lock index b4e5688..f989da5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1697,6 +1697,100 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" +"@octokit/auth-token@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@octokit/auth-token/-/auth-token-6.0.0.tgz#b02e9c08a2d8937df09a2a981f226ad219174c53" + integrity sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w== + +"@octokit/core@^7.0.2": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@octokit/core/-/core-7.0.3.tgz#0b5288995fed66920128d41cfeea34979d48a360" + integrity sha512-oNXsh2ywth5aowwIa7RKtawnkdH6LgU1ztfP9AIUCQCvzysB+WeU8o2kyyosDPwBZutPpjZDKPQGIzzrfTWweQ== + dependencies: + "@octokit/auth-token" "^6.0.0" + "@octokit/graphql" "^9.0.1" + "@octokit/request" "^10.0.2" + "@octokit/request-error" "^7.0.0" + "@octokit/types" "^14.0.0" + before-after-hook "^4.0.0" + universal-user-agent "^7.0.0" + +"@octokit/endpoint@^11.0.0": + version "11.0.0" + resolved "https://registry.yarnpkg.com/@octokit/endpoint/-/endpoint-11.0.0.tgz#189fcc022721b4c49d0307eea6be3de1cfb53026" + integrity sha512-hoYicJZaqISMAI3JfaDr1qMNi48OctWuOih1m80bkYow/ayPw6Jj52tqWJ6GEoFTk1gBqfanSoI1iY99Z5+ekQ== + dependencies: + "@octokit/types" "^14.0.0" + universal-user-agent "^7.0.2" + +"@octokit/graphql@^9.0.1": + version "9.0.1" + resolved "https://registry.yarnpkg.com/@octokit/graphql/-/graphql-9.0.1.tgz#eb258fc9981403d2d751720832652c385b6c1613" + integrity sha512-j1nQNU1ZxNFx2ZtKmL4sMrs4egy5h65OMDmSbVyuCzjOcwsHq6EaYjOTGXPQxgfiN8dJ4CriYHk6zF050WEULg== + dependencies: + "@octokit/request" "^10.0.2" + "@octokit/types" "^14.0.0" + universal-user-agent "^7.0.0" + +"@octokit/openapi-types@^25.1.0": + version "25.1.0" + resolved "https://registry.yarnpkg.com/@octokit/openapi-types/-/openapi-types-25.1.0.tgz#5a72a9dfaaba72b5b7db375fd05e90ca90dc9682" + integrity sha512-idsIggNXUKkk0+BExUn1dQ92sfysJrje03Q0bv0e+KPLrvyqZF8MnBpFz8UNfYDwB3Ie7Z0TByjWfzxt7vseaA== + +"@octokit/plugin-paginate-rest@^13.0.1": + version "13.1.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.1.1.tgz#ca5bb1c7b85a583691263c1f788f607e9bcb74b3" + integrity sha512-q9iQGlZlxAVNRN2jDNskJW/Cafy7/XE52wjZ5TTvyhyOD904Cvx//DNyoO3J/MXJ0ve3rPoNWKEg5iZrisQSuw== + dependencies: + "@octokit/types" "^14.1.0" + +"@octokit/plugin-request-log@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz#de1c1e557df6c08adb631bf78264fa741e01b317" + integrity sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q== + +"@octokit/plugin-rest-endpoint-methods@^16.0.0": + version "16.0.0" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-16.0.0.tgz#ba30ca387fc2ac8bd93cf9f951174736babebd97" + integrity sha512-kJVUQk6/dx/gRNLWUnAWKFs1kVPn5O5CYZyssyEoNYaFedqZxsfYs7DwI3d67hGz4qOwaJ1dpm07hOAD1BXx6g== + dependencies: + "@octokit/types" "^14.1.0" + +"@octokit/request-error@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/@octokit/request-error/-/request-error-7.0.0.tgz#48ae2cd79008315605d00e83664891a10a5ddb97" + integrity sha512-KRA7VTGdVyJlh0cP5Tf94hTiYVVqmt2f3I6mnimmaVz4UG3gQV/k4mDJlJv3X67iX6rmN7gSHCF8ssqeMnmhZg== + dependencies: + "@octokit/types" "^14.0.0" + +"@octokit/request@^10.0.2": + version "10.0.3" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-10.0.3.tgz#2ffdb88105ce20d25dcab8a592a7040ea48306c7" + integrity sha512-V6jhKokg35vk098iBqp2FBKunk3kMTXlmq+PtbV9Gl3TfskWlebSofU9uunVKhUN7xl+0+i5vt0TGTG8/p/7HA== + dependencies: + "@octokit/endpoint" "^11.0.0" + "@octokit/request-error" "^7.0.0" + "@octokit/types" "^14.0.0" + fast-content-type-parse "^3.0.0" + universal-user-agent "^7.0.2" + +"@octokit/rest@^22.0.0": + version "22.0.0" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-22.0.0.tgz#9026f47dacba9c605da3d43cce9432c4c532dc5a" + integrity sha512-z6tmTu9BTnw51jYGulxrlernpsQYXpui1RK21vmXn8yF5bp6iX16yfTtJYGK5Mh1qDkvDOmp2n8sRMcQmR8jiA== + dependencies: + "@octokit/core" "^7.0.2" + "@octokit/plugin-paginate-rest" "^13.0.1" + "@octokit/plugin-request-log" "^6.0.0" + "@octokit/plugin-rest-endpoint-methods" "^16.0.0" + +"@octokit/types@^14.0.0", "@octokit/types@^14.1.0": + version "14.1.0" + resolved "https://registry.yarnpkg.com/@octokit/types/-/types-14.1.0.tgz#3bf9b3a3e3b5270964a57cc9d98592ed44f840f2" + integrity sha512-1y6DgTy8Jomcpu33N+p5w58l6xyt55Ar2I91RPiIA0xCJBXyUAhXCcmZaDWSANiha7R9a6qJJ2CRomGPZ6f46g== + dependencies: + "@octokit/openapi-types" "^25.1.0" + "@parcel/bundler-default@2.9.3": version "2.9.3" resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.9.3.tgz#df18c4b8390a03f83ac6c89da302f9edf48c8fe2" @@ -3513,6 +3607,11 @@ resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== +"@types/uuid@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" + integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== + "@types/yargs-parser@*": version "21.0.3" resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-21.0.3.tgz#815e30b786d2e8f0dcd85fd5bcf5e1a04d008f15" @@ -3988,6 +4087,11 @@ base64-js@^1.3.1: resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== +before-after-hook@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/before-after-hook/-/before-after-hook-4.0.0.tgz#cf1447ab9160df6a40f3621da64d6ffc36050cb9" + integrity sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ== + binary-extensions@^2.0.0: version "2.3.0" resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" @@ -4157,15 +4261,10 @@ camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.3.0.tgz#5685b95eb209ac9c0c177467778c9c84df58ba9a" integrity sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA== -caniuse-lite@^1.0.30001541: - version "1.0.30001704" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001704.tgz#6644fe909d924ac3a7125e8a0ab6af95b1f32990" - integrity sha512-+L2IgBbV6gXB4ETf0keSvLr7JUrRVbIaB/lrQ1+z8mRcQiisG5k+lG6O4n6Y5q6f5EuNfaYXKgymucphlEXQew== - -caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702: - version "1.0.30001703" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001703.tgz#977cb4920598c158f491ecf4f4f2cfed9e354718" - integrity sha512-kRlAGTRWgPsOj7oARC9m1okJEXdL/8fekFVcxA8Hl7GH4r/sN4OJn/i6Flde373T50KS7Y37oFbMwlE8+F42kQ== +caniuse-lite@^1.0.30001541, caniuse-lite@^1.0.30001688, caniuse-lite@^1.0.30001702: + version "1.0.30001734" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001734.tgz" + integrity sha512-uhE1Ye5vgqju6OI71HTQqcBCZrvHugk0MjLak7Q+HfoBgoq5Bi+5YnwjP4fjDgrtYr/l8MVRBvzz9dPD4KyK0A== chai@^5.2.0: version "5.2.0" @@ -5083,6 +5182,11 @@ external-editor@^3.1.0: iconv-lite "^0.4.24" tmp "^0.0.33" +fast-content-type-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz#5590b6c807cc598be125e6740a9fde589d2b7afb" + integrity sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg== + fast-fifo@^1.2.0, fast-fifo@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/fast-fifo/-/fast-fifo-1.3.2.tgz#286e31de96eb96d38a97899815740ba2a4f3640c" @@ -5192,6 +5296,15 @@ fraction.js@^4.3.7: resolved "https://registry.yarnpkg.com/fraction.js/-/fraction.js-4.3.7.tgz#06ca0085157e42fda7f9e726e79fefc4068840f7" integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== +framer-motion@^12.23.12: + version "12.23.12" + resolved "https://registry.yarnpkg.com/framer-motion/-/framer-motion-12.23.12.tgz#80cf6fd7c111073a0c558e336c85ca36cca80d3d" + integrity sha512-6e78rdVtnBvlEVgu6eFEAgG9v3wLnYEboM8I5O5EXvfKC8gxGQB8wXJdhkMy10iVcn05jl6CNw7/HTsTCfwcWg== + dependencies: + motion-dom "^12.23.12" + motion-utils "^12.23.6" + tslib "^2.4.0" + fs-constants@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs-constants/-/fs-constants-1.0.0.tgz#6be0de9be998ce16af8afc24497b9ee9b7ccd9ad" @@ -6848,6 +6961,18 @@ mnemonic-id@3.2.7: resolved "https://registry.yarnpkg.com/mnemonic-id/-/mnemonic-id-3.2.7.tgz#f7d77d8b39e009ad068117cbafc458a6c6f8cddf" integrity sha512-kysx9gAGbvrzuFYxKkcRjnsg/NK61ovJOV4F1cHTRl9T5leg+bo6WI0pWIvOFh1Z/yDL0cjA5R3EEGPPLDv/XA== +motion-dom@^12.23.12: + version "12.23.12" + resolved "https://registry.yarnpkg.com/motion-dom/-/motion-dom-12.23.12.tgz#87974046e7e61bc4932f36d35e8eab6bb6f3e434" + integrity sha512-RcR4fvMCTESQBD/uKQe49D5RUeDOokkGRmz4ceaJKDBgHYtZtntC/s2vLvY38gqGaytinij/yi3hMcWVcEF5Kw== + dependencies: + motion-utils "^12.23.6" + +motion-utils@^12.23.6: + version "12.23.6" + resolved "https://registry.yarnpkg.com/motion-utils/-/motion-utils-12.23.6.tgz#fafef80b4ea85122dd0d6c599a0c63d72881f312" + integrity sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ== + ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" @@ -8446,7 +8571,7 @@ ts-node@^10.9.2: v8-compile-cache-lib "^3.0.1" yn "3.1.1" -tslib@^2.1.0, tslib@^2.3.0, tslib@^2.8.0: +tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0, tslib@^2.8.0: version "2.8.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== @@ -8553,6 +8678,11 @@ unique-string@^3.0.0: dependencies: crypto-random-string "^4.0.0" +universal-user-agent@^7.0.0, universal-user-agent@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/universal-user-agent/-/universal-user-agent-7.0.3.tgz#c05870a58125a2dc00431f2df815a77fe69736be" + integrity sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A== + universalify@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/universalify/-/universalify-0.2.0.tgz#6451760566fa857534745ab1dde952d1b1761be0" @@ -8589,6 +8719,11 @@ utility-types@^3.10.0: resolved "https://registry.yarnpkg.com/utility-types/-/utility-types-3.11.0.tgz#607c40edb4f258915e901ea7995607fdf319424c" integrity sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw== +uuid@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-11.1.0.tgz#9549028be1753bb934fc96e2bca09bb4105ae912" + integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A== + v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"