From fb17cdbcab420e6a7287e382a962c4a854c0648d Mon Sep 17 00:00:00 2001 From: Oscar Silva Date: Sat, 11 Apr 2026 20:30:46 -0300 Subject: [PATCH 1/4] feat(review): support multi-repo workspace reviews (#527) --- apps/hook/server/index.ts | 39 +- apps/opencode-plugin/commands.ts | 47 +- bun.lock | 6 +- packages/review-editor/App.tsx | 273 ++++++++++-- packages/review-editor/hooks/usePRContext.ts | 15 +- .../utils/buildFileTree.workspace.test.ts | 243 +++++++++++ .../utils/exportFeedback.workspace.test.ts | 122 ++++++ .../utils/reviewSearch.workspace.test.ts | 206 +++++++++ packages/server/claude-review.ts | 5 +- packages/server/codex-review.ts | 5 +- packages/server/package.json | 1 + packages/server/review-workspace.test.ts | 306 +++++++++++++ packages/server/review-workspace.ts | 404 ++++++++++++++++++ packages/server/review.ts | 348 ++++++++++++++- packages/server/shared-handlers.ts | 4 +- packages/server/vcs.test.ts | 209 +++++++++ packages/server/vcs.ts | 19 +- packages/shared/package.json | 1 + packages/shared/review-workspace.ts | 30 ++ packages/shared/types.ts | 7 + 20 files changed, 2192 insertions(+), 98 deletions(-) create mode 100644 packages/review-editor/utils/buildFileTree.workspace.test.ts create mode 100644 packages/review-editor/utils/exportFeedback.workspace.test.ts create mode 100644 packages/review-editor/utils/reviewSearch.workspace.test.ts create mode 100644 packages/server/review-workspace.test.ts create mode 100644 packages/server/review-workspace.ts create mode 100644 packages/server/vcs.test.ts create mode 100644 packages/shared/review-workspace.ts diff --git a/apps/hook/server/index.ts b/apps/hook/server/index.ts index 7572d43cf..389cb05a7 100644 --- a/apps/hook/server/index.ts +++ b/apps/hook/server/index.ts @@ -63,7 +63,7 @@ import { startAnnotateServer, handleAnnotateServerReady, } from "@plannotator/server/annotate"; -import { type DiffType, getVcsContext, runVcsDiff, gitRuntime } from "@plannotator/server/vcs"; +import { type DiffType, getVcsContext, runVcsDiff, gitRuntime, detectManagedVcs } from "@plannotator/server/vcs"; import { loadConfig, resolveDefaultDiffType } from "@plannotator/shared/config"; import { fetchRef, createWorktree, removeWorktree, ensureObjectAvailable } from "@plannotator/shared/worktree"; import { parsePRUrl, checkPRAuth, fetchPR, getCliName, getCliInstallUrl, getMRLabel, getMRNumberLabel, getDisplayRepo } from "@plannotator/server/pr"; @@ -89,6 +89,7 @@ import { } from "./cli"; import path from "path"; import { tmpdir } from "os"; +import { buildWorkspaceLocalRepos, buildWorkspacePRRepos } from "@plannotator/server/review-workspace"; // Embed the built HTML at compile time // @ts-ignore - Bun import attribute for text @@ -196,8 +197,10 @@ if (args[0] === "sessions") { const noLocalIdx = args.indexOf("--no-local"); if (noLocalIdx !== -1) args.splice(noLocalIdx, 1); - const urlArg = args[1]; - const isPRMode = urlArg?.startsWith("http://") || urlArg?.startsWith("https://"); + const urlArgs = args.slice(1).filter((arg) => arg.startsWith("http://") || arg.startsWith("https://")); + const urlArg = urlArgs[0]; + const isPRMode = urlArgs.length > 0; + const isMultiPRMode = urlArgs.length > 1; const useLocal = isPRMode && noLocalIdx === -1; let rawPatch: string; @@ -208,8 +211,13 @@ if (args[0] === "sessions") { let initialDiffType: DiffType | undefined; let agentCwd: string | undefined; let worktreeCleanup: (() => void | Promise) | undefined; + let workspaceRepos: Awaited> | undefined; - if (isPRMode) { + if (isMultiPRMode) { + workspaceRepos = await buildWorkspacePRRepos(urlArgs); + rawPatch = ""; + gitRef = "Workspace review"; + } else if (isPRMode) { // --- PR Review Mode --- const prRef = parsePRUrl(urlArg); if (!prRef) { @@ -379,12 +387,22 @@ if (args[0] === "sessions") { } } else { // --- Local Review Mode --- - gitContext = await getVcsContext(); - initialDiffType = gitContext.vcsType === "p4" ? "p4-default" : resolveDefaultDiffType(loadConfig()); - const diffResult = await runVcsDiff(initialDiffType, gitContext.defaultBranch); - rawPatch = diffResult.patch; - gitRef = diffResult.label; - diffError = diffResult.error; + const managedVcs = await detectManagedVcs(process.cwd()); + if (managedVcs) { + gitContext = await getVcsContext(); + initialDiffType = gitContext.vcsType === "p4" ? "p4-default" : resolveDefaultDiffType(loadConfig()); + const diffResult = await runVcsDiff(initialDiffType, gitContext.defaultBranch); + rawPatch = diffResult.patch; + gitRef = diffResult.label; + diffError = diffResult.error; + } else { + workspaceRepos = await buildWorkspaceLocalRepos(process.cwd()); + if (workspaceRepos.length === 0) { + throw new Error("Not in a git repo and no nested repositories were found."); + } + rawPatch = ""; + gitRef = "Workspace review"; + } } const reviewProject = (await detectProjectName()) ?? "_unknown"; @@ -398,6 +416,7 @@ if (args[0] === "sessions") { diffType: gitContext ? (initialDiffType ?? "unstaged") : undefined, gitContext, prMetadata, + workspaceRepos, agentCwd, sharingEnabled, shareBaseUrl, diff --git a/apps/opencode-plugin/commands.ts b/apps/opencode-plugin/commands.ts index 523f4723d..2871f73f4 100644 --- a/apps/opencode-plugin/commands.ts +++ b/apps/opencode-plugin/commands.ts @@ -19,9 +19,11 @@ import { handleAnnotateServerReady, } from "@plannotator/server/annotate"; import { getGitContext, runGitDiffWithContext } from "@plannotator/server/git"; +import { detectManagedVcs } from "@plannotator/server/vcs"; import { parsePRUrl, checkPRAuth, fetchPR, getCliName, getMRLabel, getMRNumberLabel, getDisplayRepo } from "@plannotator/server/pr"; import { loadConfig, resolveDefaultDiffType } from "@plannotator/shared/config"; import { resolveMarkdownFile } from "@plannotator/shared/resolve-file"; +import { buildWorkspaceLocalRepos, buildWorkspacePRRepos } from "@plannotator/server/review-workspace"; /** Shared dependencies injected by the plugin */ export interface CommandDeps { @@ -41,7 +43,9 @@ export async function handleReviewCommand( // @ts-ignore - Event properties contain arguments const urlArg: string = event.properties?.arguments || ""; - const isPRMode = urlArg?.startsWith("http://") || urlArg?.startsWith("https://"); + const urlArgs = urlArg.split(/\s+/).filter((arg: string) => arg.startsWith("http://") || arg.startsWith("https://")); + const isPRMode = urlArgs.length > 0; + const isMultiPRMode = urlArgs.length > 1; let rawPatch: string; let gitRef: string; @@ -49,11 +53,16 @@ export async function handleReviewCommand( let userDiffType: import("@plannotator/shared/config").DefaultDiffType | undefined; let gitContext: Awaited> | undefined; let prMetadata: Awaited>["metadata"] | undefined; - - if (isPRMode) { - const prRef = parsePRUrl(urlArg); + let workspaceRepos: Awaited> | undefined; + + if (isMultiPRMode) { + workspaceRepos = await buildWorkspacePRRepos(urlArgs); + rawPatch = ""; + gitRef = "Workspace review"; + } else if (isPRMode) { + const prRef = parsePRUrl(urlArgs[0]); if (!prRef) { - client.app.log({ level: "error", message: `Invalid PR/MR URL: ${urlArg}` }); + client.app.log({ level: "error", message: `Invalid PR/MR URL: ${urlArgs[0]}` }); return; } @@ -79,12 +88,27 @@ export async function handleReviewCommand( } else { client.app.log({ level: "info", message: "Opening code review UI..." }); - gitContext = await getGitContext(directory); - userDiffType = resolveDefaultDiffType(loadConfig()); - const diffResult = await runGitDiffWithContext(userDiffType, gitContext); - rawPatch = diffResult.patch; - gitRef = diffResult.label; - diffError = diffResult.error; + const managedVcs = await detectManagedVcs(directory); + if (managedVcs) { + gitContext = await getGitContext(directory); + userDiffType = resolveDefaultDiffType(loadConfig()); + const diffResult = await runGitDiffWithContext(userDiffType, gitContext); + rawPatch = diffResult.patch; + gitRef = diffResult.label; + diffError = diffResult.error; + } else { + workspaceRepos = await buildWorkspaceLocalRepos(directory || process.cwd()); + if (workspaceRepos.length === 0) { + client.app.log({ level: "error", message: "Not in a git repo and no nested repositories were found." }); + return; + } + client.app.log({ + level: "info", + message: `Workspace mode: found ${workspaceRepos.length} repos (${workspaceRepos.filter((repo) => repo.selected).length} selected with changes).`, + }); + rawPatch = ""; + gitRef = "Workspace review"; + } } const server = await startReviewServer({ @@ -95,6 +119,7 @@ export async function handleReviewCommand( diffType: isPRMode ? undefined : userDiffType, gitContext, prMetadata, + workspaceRepos, sharingEnabled: await getSharingEnabled(), shareBaseUrl: getShareBaseUrl(), htmlContent: reviewHtmlContent, diff --git a/bun.lock b/bun.lock index 786f34334..683bedaea 100644 --- a/bun.lock +++ b/bun.lock @@ -63,7 +63,7 @@ }, "apps/opencode-plugin": { "name": "@plannotator/opencode", - "version": "0.17.7", + "version": "0.17.8", "dependencies": { "@opencode-ai/plugin": "^1.1.10", }, @@ -85,7 +85,7 @@ }, "apps/pi-extension": { "name": "@plannotator/pi-extension", - "version": "0.17.7", + "version": "0.17.8", "peerDependencies": { "@mariozechner/pi-coding-agent": ">=0.53.0", }, @@ -171,7 +171,7 @@ }, "packages/server": { "name": "@plannotator/server", - "version": "0.17.7", + "version": "0.17.8", "dependencies": { "@plannotator/ai": "workspace:*", "@plannotator/shared": "workspace:*", diff --git a/packages/review-editor/App.tsx b/packages/review-editor/App.tsx index 862058445..ccafaaba9 100644 --- a/packages/review-editor/App.tsx +++ b/packages/review-editor/App.tsx @@ -58,6 +58,7 @@ import { import type { DiffFile } from './types'; import type { DiffOption, WorktreeInfo, GitContext } from '@plannotator/shared/types'; import type { PRMetadata } from '@plannotator/shared/pr-provider'; +import type { WorkspaceReviewState, WorkspaceRepoState } from '@plannotator/shared/review-workspace'; import { altKey } from '@plannotator/ui/utils/platform'; declare const __APP_VERSION__: string; @@ -167,12 +168,22 @@ const ReviewApp: React.FC = () => { const [showExitWarning, setShowExitWarning] = useState(false); const [sharingEnabled, setSharingEnabled] = useState(true); const [repoInfo, setRepoInfo] = useState<{ display: string; branch?: string } | null>(null); + const [workspace, setWorkspace] = useState(null); useEffect(() => { document.title = repoInfo ? `${repoInfo.display} · Code Review` : "Code Review"; }, [repoInfo]); - const [prMetadata, setPrMetadata] = useState(null); + useEffect(() => { + if (!workspace) return; + const selectedCount = workspace.repos.filter(repo => repo.selected).length; + setRepoInfo({ + display: selectedCount > 0 ? `${selectedCount} selected repos` : 'Workspace Review', + branch: 'Workspace', + }); + }, [workspace]); + + const [singularPrMetadata, setSingularPrMetadata] = useState(null); const [reviewDestination, setReviewDestination] = useState<'agent' | 'platform'>(() => { const stored = storage.getItem('plannotator-review-dest'); return stored === 'agent' ? 'agent' : 'platform'; // 'github' (legacy) → 'platform' @@ -180,7 +191,7 @@ const ReviewApp: React.FC = () => { const [showDestinationMenu, setShowDestinationMenu] = useState(false); const [isPlatformActioning, setIsPlatformActioning] = useState(false); const [platformActionError, setPlatformActionError] = useState(null); - const [platformUser, setPlatformUser] = useState(null); + const [singularPlatformUser, setSingularPlatformUser] = useState(null); const [platformCommentDialog, setPlatformCommentDialog] = useState<{ action: 'approve' | 'comment' } | null>(null); const [platformGeneralComment, setPlatformGeneralComment] = useState(''); const [platformOpenPR, setPlatformOpenPR] = useState(() => { @@ -197,6 +208,22 @@ const ReviewApp: React.FC = () => { }); // Derived: Platform mode is active when destination is platform AND we have PR/MR metadata + const findWorkspaceRepoForPath = useCallback((filePath?: string | null): WorkspaceRepoState | null => { + if (!workspace || !filePath) return null; + return workspace.repos.find(repo => filePath === repo.label || filePath.startsWith(`${repo.label}/`)) ?? null; + }, [workspace]); + + const activeWorkspaceRepo = useMemo(() => { + if (!workspace) return null; + return findWorkspaceRepoForPath(files[activeFileIndex]?.path) + ?? workspace.repos.find(repo => repo.selected) + ?? workspace.repos[0] + ?? null; + }, [workspace, files, activeFileIndex, findWorkspaceRepoForPath]); + + const prMetadata = workspace ? activeWorkspaceRepo?.prMetadata ?? null : singularPrMetadata; + const platformUser = workspace ? activeWorkspaceRepo?.platformUser ?? null : singularPlatformUser; + const platformMode = reviewDestination === 'platform' && !!prMetadata; // Platform-aware labels @@ -231,7 +258,16 @@ const ReviewApp: React.FC = () => { const needsInitialDiffPanel = useRef(true); // PR context (lifted from sidebar so center dock PR panels can access it) - const { prContext, isLoading: isPRContextLoading, error: prContextError, fetchContext: fetchPRContext } = usePRContext(prMetadata ?? null); + const { prContext, isLoading: isPRContextLoading, error: prContextError, fetchContext: fetchPRContext } = usePRContext(prMetadata ?? null, workspace ? activeWorkspaceRepo?.id ?? null : null); + + useEffect(() => { + if (!workspace || !activeWorkspaceRepo) return; + fetch('/api/workspace/active', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ repoId: activeWorkspaceRepo.id }), + }).catch(() => {}); + }, [workspace, activeWorkspaceRepo]); // Sync activeFileIndex from dockview's active panel (wired in handleDockReady) @@ -635,6 +671,7 @@ const ReviewApp: React.FC = () => { prMetadata?: PRMetadata; platformUser?: string; viewedFiles?: string[]; + workspace?: WorkspaceReviewState; error?: string; isWSL?: boolean; serverConfig?: { displayName?: string; gitUser?: string }; @@ -660,10 +697,17 @@ const ReviewApp: React.FC = () => { if (data.agentCwd) setAgentCwd(data.agentCwd); if (data.sharingEnabled !== undefined) setSharingEnabled(data.sharingEnabled); if (data.repoInfo) setRepoInfo(data.repoInfo); - if (data.prMetadata) setPrMetadata(data.prMetadata); - if (data.platformUser) setPlatformUser(data.platformUser); + if (data.workspace) { + setWorkspace(data.workspace); + const workspaceViewed = data.workspace.repos.flatMap(repo => repo.viewedFiles ?? []); + if (workspaceViewed.length > 0) { + setViewedFiles(new Set(workspaceViewed)); + } + } + if (data.prMetadata) setSingularPrMetadata(data.prMetadata); + if (data.platformUser) setSingularPlatformUser(data.platformUser); // Initialize viewed files from GitHub's state (set before draft restore so draft takes precedence) - if (data.viewedFiles && data.viewedFiles.length > 0) { + if (!data.workspace && data.viewedFiles && data.viewedFiles.length > 0) { setViewedFiles(new Set(data.viewedFiles)); } if (data.error) setDiffError(data.error); @@ -846,7 +890,15 @@ const ReviewApp: React.FC = () => { } // Sync viewed state to GitHub (fire and forget — best effort) // Capture willBeViewed inside the callback to ensure correctness with React batching - if (prMetadata && prMetadata.platform === 'github') { + if (workspace && activeWorkspaceRepo?.prMetadata?.platform === 'github') { + fetch('/api/pr-viewed', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ repoId: activeWorkspaceRepo.id, filePaths: [filePath], viewed: willBeViewed }), + }).catch(() => { + // Silently ignore — viewed sync is best-effort + }); + } else if (prMetadata && prMetadata.platform === 'github') { fetch('/api/pr-viewed', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -857,12 +909,14 @@ const ReviewApp: React.FC = () => { } return next; }); - }, [prMetadata]); + }, [workspace, activeWorkspaceRepo, prMetadata]); // Derive worktree path and base diff type from the composite diffType string + const effectiveDiffType = workspace ? (activeWorkspaceRepo?.diffType || 'uncommitted') : diffType; + const { activeWorktreePath, activeDiffBase } = useMemo(() => { - if (diffType.startsWith('worktree:')) { - const rest = diffType.slice('worktree:'.length); + if (effectiveDiffType.startsWith('worktree:')) { + const rest = effectiveDiffType.slice('worktree:'.length); const lastColon = rest.lastIndexOf(':'); if (lastColon !== -1) { const sub = rest.slice(lastColon + 1); @@ -872,8 +926,8 @@ const ReviewApp: React.FC = () => { } return { activeWorktreePath: rest, activeDiffBase: 'uncommitted' }; } - return { activeWorktreePath: null, activeDiffBase: diffType }; - }, [diffType]); + return { activeWorktreePath: null, activeDiffBase: effectiveDiffType }; + }, [effectiveDiffType]); // Git add/staging logic const handleFileViewedFromStage = useCallback( @@ -884,17 +938,39 @@ const ReviewApp: React.FC = () => { activeDiffBase, onFileViewed: handleFileViewedFromStage, }); - // Staging is never available in PR review mode — the server rejects it and the UI shouldn't offer it. - const canStageFiles = canStageRaw && !prMetadata; + const canStageFiles = canStageRaw && !(workspace ? activeWorkspaceRepo?.source === 'pr' : prMetadata); + + const applyServerDiffPayload = useCallback((data: { + rawPatch: string; + gitRef: string; + diffType?: string; + workspace?: WorkspaceReviewState; + error?: string; + }) => { + const nextFiles = parseDiffToFiles(data.rawPatch); + dockApi?.getPanel(REVIEW_DIFF_PANEL_ID)?.api.close(); + needsInitialDiffPanel.current = true; + setDiffData(prev => prev ? { ...prev, rawPatch: data.rawPatch, gitRef: data.gitRef, diffType: data.diffType } : prev); + setFiles(nextFiles); + if (data.diffType) setDiffType(data.diffType); + if (data.workspace) { + setWorkspace(data.workspace); + setViewedFiles(new Set(data.workspace.repos.flatMap(repo => repo.viewedFiles ?? []))); + } + setActiveFileIndex(0); + setPendingSelection(null); + setDiffError(data.error || null); + resetStagedFiles(); + }, [dockApi, resetStagedFiles]); // Shared helper: fetch a diff switch and update state - const fetchDiffSwitch = useCallback(async (fullDiffType: string) => { + const fetchDiffSwitch = useCallback(async (fullDiffType: string, repoId?: string) => { setIsLoadingDiff(true); try { const res = await fetch('/api/diff/switch', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ diffType: fullDiffType }), + body: JSON.stringify(repoId ? { repoId, diffType: fullDiffType } : { diffType: fullDiffType }), }); if (!res.ok) throw new Error('Failed to switch diff'); @@ -902,45 +978,66 @@ const ReviewApp: React.FC = () => { const data = await res.json() as { rawPatch: string; gitRef: string; - diffType: string; + diffType?: string; + workspace?: WorkspaceReviewState; error?: string; }; - const nextFiles = parseDiffToFiles(data.rawPatch); - dockApi?.getPanel(REVIEW_DIFF_PANEL_ID)?.api.close(); - needsInitialDiffPanel.current = true; - setDiffData(prev => prev ? { ...prev, rawPatch: data.rawPatch, gitRef: data.gitRef, diffType: data.diffType } : prev); - setFiles(nextFiles); - setDiffType(data.diffType); - setActiveFileIndex(0); - setPendingSelection(null); - setDiffError(data.error || null); - resetStagedFiles(); + applyServerDiffPayload(data); } catch (err) { console.error('Failed to switch diff:', err); setDiffError(err instanceof Error ? err.message : 'Failed to switch diff'); } finally { setIsLoadingDiff(false); } - }, [dockApi, resetStagedFiles]); + }, [applyServerDiffPayload]); + + const updateWorkspaceRepo = useCallback(async (repoId: string, changes: { selected?: boolean; source?: 'local' | 'pr'; prUrl?: string }) => { + setIsLoadingDiff(true); + try { + const res = await fetch('/api/workspace/repo', { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ repoId, ...changes }), + }); + if (!res.ok) throw new Error('Failed to update workspace repo'); + const data = await res.json() as { + rawPatch: string; + gitRef: string; + workspace?: WorkspaceReviewState; + error?: string; + }; + applyServerDiffPayload(data); + } catch (err) { + console.error('Failed to update workspace repo:', err); + setDiffError(err instanceof Error ? err.message : 'Failed to update workspace repo'); + } finally { + setIsLoadingDiff(false); + } + }, [applyServerDiffPayload]); // Switch diff type (uncommitted, last-commit, branch) — composes worktree prefix if active const handleDiffSwitch = useCallback(async (baseDiffType: string) => { + if (workspace && activeWorkspaceRepo) { + await fetchDiffSwitch(baseDiffType, activeWorkspaceRepo.id); + return; + } const fullDiffType = activeWorktreePath ? `worktree:${activeWorktreePath}:${baseDiffType}` : baseDiffType; if (fullDiffType === diffType) return; await fetchDiffSwitch(fullDiffType); - }, [diffType, activeWorktreePath, fetchDiffSwitch]); + }, [workspace, activeWorkspaceRepo, diffType, activeWorktreePath, fetchDiffSwitch]); // Switch worktree context (or back to main repo) const handleWorktreeSwitch = useCallback(async (worktreePath: string | null) => { + if (workspace) return; if (worktreePath === activeWorktreePath) return; const fullDiffType = worktreePath ? `worktree:${worktreePath}:uncommitted` : 'uncommitted'; await fetchDiffSwitch(fullDiffType); - }, [activeWorktreePath, fetchDiffSwitch]); + }, [workspace, activeWorktreePath, fetchDiffSwitch]); // Select annotation - switches file if needed and scrolls to it const handleSelectAnnotation = useCallback((id: string | null) => { @@ -1056,7 +1153,7 @@ const ReviewApp: React.FC = () => { return; } try { - const feedback = exportReviewFeedback(allAnnotations, prMetadata); + const feedback = exportReviewFeedback(allAnnotations, workspace ? undefined : prMetadata); await navigator.clipboard.writeText(feedback); setCopyFeedback('Feedback copied!'); setTimeout(() => setCopyFeedback(null), 2000); @@ -1065,15 +1162,15 @@ const ReviewApp: React.FC = () => { setCopyFeedback('Failed to copy'); setTimeout(() => setCopyFeedback(null), 2000); } - }, [allAnnotations, prMetadata]); + }, [workspace, allAnnotations, prMetadata]); const feedbackMarkdown = useMemo(() => { - let output = exportReviewFeedback(allAnnotations, prMetadata); + let output = exportReviewFeedback(allAnnotations, workspace ? undefined : prMetadata); if (editorAnnotations.length > 0) { output += exportEditorAnnotations(editorAnnotations); } return output; - }, [allAnnotations, prMetadata, editorAnnotations]); + }, [workspace, allAnnotations, prMetadata, editorAnnotations]); const totalAnnotationCount = allAnnotations.length + editorAnnotations.length; @@ -1155,8 +1252,18 @@ const ReviewApp: React.FC = () => { // Build the payload for /api/pr-action from current annotations const buildPRReviewPayload = useCallback((action: 'approve' | 'comment', generalComment?: string) => { - const fileAnnotations = allAnnotations.filter(a => (a.scope ?? 'line') === 'line'); - const fileScoped = allAnnotations.filter(a => a.scope === 'file'); + const repoPrefix = workspace && activeWorkspaceRepo ? `${activeWorkspaceRepo.label}/` : null; + const scopedAnnotations = repoPrefix + ? allAnnotations.filter(annotation => annotation.filePath.startsWith(repoPrefix)) + : allAnnotations; + const scopedEditorAnnotations = repoPrefix + ? editorAnnotations.filter(annotation => annotation.filePath.startsWith(repoPrefix)) + : editorAnnotations; + const scopedFiles = repoPrefix + ? files.filter(file => file.path.startsWith(repoPrefix)) + : files; + const fileAnnotations = scopedAnnotations.filter(a => (a.scope ?? 'line') === 'line'); + const fileScoped = scopedAnnotations.filter(a => a.scope === 'file'); // Top-level body: file-scoped comments const bodyParts: string[] = []; @@ -1192,8 +1299,8 @@ const ReviewApp: React.FC = () => { // Editor annotations (VS Code extension) — always on new/RIGHT side // Only include annotations targeting files in the diff to avoid GitHub API rejection - const diffPaths = new Set(files.map(f => f.path)); - for (const ea of editorAnnotations) { + const diffPaths = new Set(scopedFiles.map(f => f.path)); + for (const ea of scopedEditorAnnotations) { if (!diffPaths.has(ea.filePath)) continue; const body = ea.comment || `> ${ea.selectedText}`; if (!body.trim()) continue; @@ -1210,8 +1317,13 @@ const ReviewApp: React.FC = () => { }); } - return { action, body, fileComments }; - }, [allAnnotations, editorAnnotations, files]); + return { + ...(workspace && activeWorkspaceRepo && { repoId: activeWorkspaceRepo.id }), + action, + body, + fileComments, + }; + }, [workspace, activeWorkspaceRepo, allAnnotations, editorAnnotations, files]); // Submit a review directly to GitHub const handlePlatformAction = useCallback(async (action: 'approve' | 'comment', generalComment?: string) => { @@ -1637,6 +1749,81 @@ const ReviewApp: React.FC = () => { + {workspace && ( +
+
+ {workspace.repos.map((repo) => ( +
+
+ + {repo.error && ( + + issue + + )} +
+
+ + {repo.source === 'local' ? ( + + ) : ( + + )} +
+
+ ))} +
+
+ )} + {/* Main content */}
{/* Left sidebar stays mounted whenever it provides navigation or context. */} @@ -1653,15 +1840,15 @@ const ReviewApp: React.FC = () => { hideViewedFiles={hideViewedFiles} onToggleHideViewed={() => setHideViewedFiles(prev => !prev)} enableKeyboardNav={!showExportModal && hasSearchableFiles} - diffOptions={gitContext?.diffOptions} + diffOptions={workspace ? activeWorkspaceRepo?.gitContext?.diffOptions : gitContext?.diffOptions} activeDiffType={activeDiffBase} onSelectDiff={handleDiffSwitch} isLoadingDiff={isLoadingDiff} width={fileTreeResize.width} - worktrees={gitContext?.worktrees} + worktrees={workspace ? activeWorkspaceRepo?.gitContext?.worktrees : gitContext?.worktrees} activeWorktreePath={activeWorktreePath} onSelectWorktree={handleWorktreeSwitch} - currentBranch={gitContext?.currentBranch} + currentBranch={workspace ? activeWorkspaceRepo?.gitContext?.currentBranch : gitContext?.currentBranch} stagedFiles={stagedFiles} onCopyRawDiff={handleCopyDiff} canCopyRawDiff={!!diffData?.rawPatch} diff --git a/packages/review-editor/hooks/usePRContext.ts b/packages/review-editor/hooks/usePRContext.ts index c7fb098f3..58a26369a 100644 --- a/packages/review-editor/hooks/usePRContext.ts +++ b/packages/review-editor/hooks/usePRContext.ts @@ -1,13 +1,19 @@ -import { useState, useRef, useCallback } from 'react'; +import { useState, useRef, useCallback, useEffect } from 'react'; import type { PRContext } from '@plannotator/shared/pr-provider'; import type { PRMetadata } from '@plannotator/shared/pr-provider'; -export function usePRContext(prMetadata: PRMetadata | null) { +export function usePRContext(prMetadata: PRMetadata | null, repoId?: string | null) { const [prContext, setPRContext] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const fetched = useRef(false); + useEffect(() => { + fetched.current = false; + setPRContext(null); + setError(null); + }, [prMetadata, repoId]); + const fetchContext = useCallback(async () => { if (!prMetadata || fetched.current) return; fetched.current = true; @@ -15,7 +21,8 @@ export function usePRContext(prMetadata: PRMetadata | null) { setError(null); try { - const res = await fetch('/api/pr-context'); + const suffix = repoId ? `?repoId=${encodeURIComponent(repoId)}` : ''; + const res = await fetch(`/api/pr-context${suffix}`); if (!res.ok) { const data = await res.json().catch(() => ({})); throw new Error(data.error || `HTTP ${res.status}`); @@ -29,7 +36,7 @@ export function usePRContext(prMetadata: PRMetadata | null) { } finally { setIsLoading(false); } - }, [prMetadata]); + }, [prMetadata, repoId]); return { prContext, isLoading, error, fetchContext }; } diff --git a/packages/review-editor/utils/buildFileTree.workspace.test.ts b/packages/review-editor/utils/buildFileTree.workspace.test.ts new file mode 100644 index 000000000..c3d22c3e1 --- /dev/null +++ b/packages/review-editor/utils/buildFileTree.workspace.test.ts @@ -0,0 +1,243 @@ +import { describe, it, expect } from "bun:test"; +import { buildFileTree, getAncestorPaths, getAllFolderPaths } from "./buildFileTree"; +import type { DiffFile } from "../types"; + +const diffFile = (path: string, overrides: Partial = {}): DiffFile => ({ + path, + patch: "", + additions: 0, + deletions: 0, + ...overrides, +}); + +describe("buildFileTree - workspace mode with repo-prefixed paths", () => { + it("builds separate trees for different repo prefixes", () => { + const files = [ + diffFile("repo-a/src/index.ts"), + diffFile("repo-b/src/index.ts"), + ]; + const tree = buildFileTree(files); + + // With flat fallback: single root folder with only file children gets unwrapped + // But here we have two repos at root level, so they stay as folders + expect(tree.length).toBeGreaterThanOrEqual(2); + // After collapseSingleChild, paths like repo-a/src/index.ts become: + // folder: "repo-a/src" with file child "index.ts" + const names = tree.map(n => n.name); + expect(names).toContain("repo-a/src"); + expect(names).toContain("repo-b/src"); + }); + + it("handles same relative paths in different repos", () => { + const files = [ + diffFile("repo-a/src/utils/helper.ts", { additions: 5, deletions: 2 }), + diffFile("repo-b/src/utils/helper.ts", { additions: 3, deletions: 1 }), + ]; + const tree = buildFileTree(files); + + // After collapseSingleChild: repo-a/src/utils becomes a single folder node + const repoA = tree.find(n => n.name === "repo-a/src/utils"); + const repoB = tree.find(n => n.name === "repo-b/src/utils"); + + expect(repoA).toBeDefined(); + expect(repoB).toBeDefined(); + + // Each should have the helper.ts file as a child + const repoAFile = repoA?.children?.find(n => n.name === "helper.ts"); + const repoBFile = repoB?.children?.find(n => n.name === "helper.ts"); + + expect(repoAFile).toBeDefined(); + expect(repoBFile).toBeDefined(); + expect(repoAFile?.path).toBe("repo-a/src/utils/helper.ts"); + expect(repoBFile?.path).toBe("repo-b/src/utils/helper.ts"); + expect(repoAFile?.additions).toBe(5); + expect(repoBFile?.additions).toBe(3); + }); + + it("handles nested repo labels (longest prefix)", () => { + // Simulates repos like "apps", "apps/api", "apps/web" + const files = [ + diffFile("apps/src/main.ts"), + diffFile("apps/api/src/server.ts"), + diffFile("apps/web/src/app.ts"), + ]; + const tree = buildFileTree(files); + + // All under single "apps" root, with children for each sub-repo + // After collapseSingleChild: api/src and web/src collapse + expect(tree).toHaveLength(1); + expect(tree[0].name).toBe("apps"); + expect(tree[0].type).toBe("folder"); + + // Children: "api/src" (collapsed), "src" (from apps/src), "web/src" (collapsed) + const childNames = tree[0].children?.map(n => n.name).sort(); + expect(childNames).toEqual(["api/src", "src", "web/src"]); + }); + + it("handles deeply nested repo labels", () => { + const files = [ + diffFile("packages/shared/utils/helpers/string.ts"), + diffFile("packages/core/src/index.ts"), + ]; + const tree = buildFileTree(files); + + expect(tree).toHaveLength(1); + expect(tree[0].name).toBe("packages"); + expect(tree[0].type).toBe("folder"); + + // After collapseSingleChild, packages/core/src collapses to "core/src" + // and packages/shared/utils/helpers collapses to "shared/utils/helpers" + const children = tree[0].children?.map(n => n.name).sort(); + expect(children).toEqual(["core/src", "shared/utils/helpers"]); + }); + + it("collapses single-child folders correctly with repo prefixes", () => { + const files = [ + diffFile("repo-a/src/components/Button.tsx"), + ]; + const tree = buildFileTree(files); + + // After collapseSingleChild: repo-a/src/components collapses to single path + // Then flat fallback kicks in: single folder with only file children gets unwrapped + // Result: just the file at root level + expect(tree).toHaveLength(1); + expect(tree[0].name).toBe("Button.tsx"); + expect(tree[0].type).toBe("file"); + expect(tree[0].path).toBe("repo-a/src/components/Button.tsx"); + }); + + it("aggregates stats correctly across repo boundaries", () => { + const files = [ + diffFile("repo-a/src/index.ts", { additions: 10, deletions: 5 }), + diffFile("repo-a/src/utils.ts", { additions: 5, deletions: 2 }), + diffFile("repo-b/src/index.ts", { additions: 8, deletions: 3 }), + ]; + const tree = buildFileTree(files); + + // After collapseSingleChild: repo-a/src contains both files + const repoA = tree.find(n => n.name === "repo-a/src"); + const repoB = tree.find(n => n.name === "repo-b/src"); + + expect(repoA?.additions).toBe(15); // 10 + 5 + expect(repoA?.deletions).toBe(7); // 5 + 2 + expect(repoB?.additions).toBe(8); + expect(repoB?.deletions).toBe(3); + }); + + it("handles repo labels with special characters", () => { + const files = [ + diffFile("my-repo_2.0/src/index.ts"), + diffFile("my-repo_2.0-beta/src/app.ts"), + ]; + const tree = buildFileTree(files); + + // Two separate root-level folders after collapse + expect(tree.length).toBeGreaterThanOrEqual(2); + const names = tree.map(n => n.name); + expect(names).toContain("my-repo_2.0/src"); + expect(names).toContain("my-repo_2.0-beta/src"); + }); + + it("preserves full prefixed path in node path property", () => { + const files = [ + diffFile("owner/repo/src/index.ts"), + ]; + const tree = buildFileTree(files); + + // Collapses to "owner/repo/src" folder, then flat fallback unwraps + // Result: just the file with full path preserved + expect(tree[0].name).toBe("index.ts"); + expect(tree[0].path).toBe("owner/repo/src/index.ts"); + }); + + it("handles empty file list", () => { + const tree = buildFileTree([]); + expect(tree).toHaveLength(0); + }); + + it("handles single file in repo (flat fallback)", () => { + const files = [diffFile("repo-a/README.md")]; + const tree = buildFileTree(files); + + // Flat fallback: single root folder with only file children gets unwrapped + // But first collapseSingleChild collapses repo-a to contain README.md + // Then flat fallback sees single folder "repo-a" with file child, unwraps it + expect(tree).toHaveLength(1); + expect(tree[0].name).toBe("README.md"); + expect(tree[0].type).toBe("file"); + expect(tree[0].path).toBe("repo-a/README.md"); + }); + + it("handles multiple files in same repo subdirectories", () => { + const files = [ + diffFile("repo-a/src/index.ts"), + diffFile("repo-a/src/app.ts"), + diffFile("repo-a/lib/helpers.ts"), + ]; + const tree = buildFileTree(files); + + // repo-a has two children: src (with 2 files) and lib (with 1 file) + // So it doesn't get unwrapped by flat fallback + expect(tree).toHaveLength(1); + expect(tree[0].name).toBe("repo-a"); + expect(tree[0].type).toBe("folder"); + + const children = tree[0].children?.map(n => n.name).sort(); + expect(children).toEqual(["lib", "src"]); + }); +}); + +describe("getAncestorPaths - workspace mode", () => { + it("returns ancestor paths for repo-prefixed file", () => { + const paths = getAncestorPaths("repo-a/src/utils/helper.ts"); + expect(paths).toEqual([ + "repo-a", + "repo-a/src", + "repo-a/src/utils", + ]); + }); + + it("handles deeply nested repo labels", () => { + const paths = getAncestorPaths("packages/shared/utils/helpers/string.ts"); + expect(paths).toEqual([ + "packages", + "packages/shared", + "packages/shared/utils", + "packages/shared/utils/helpers", + ]); + }); + + it("handles flat repo structure", () => { + const paths = getAncestorPaths("repo-a/file.ts"); + expect(paths).toEqual(["repo-a"]); + }); +}); + +describe("getAllFolderPaths - workspace mode", () => { + it("collects all folder paths from repo-prefixed tree", () => { + const files = [ + diffFile("repo-a/src/index.ts"), + diffFile("repo-b/src/app.ts"), + ]; + const tree = buildFileTree(files); + const folders = getAllFolderPaths(tree); + + // After collapseSingleChild, we get "repo-a/src" and "repo-b/src" + expect(folders).toContain("repo-a/src"); + expect(folders).toContain("repo-b/src"); + }); + + it("collects nested repo label folders", () => { + const files = [ + diffFile("apps/api/src/server.ts"), + diffFile("apps/web/src/app.ts"), + ]; + const tree = buildFileTree(files); + const folders = getAllFolderPaths(tree); + + // After collapseSingleChild: apps stays, apps/api/src and apps/web/src + expect(folders).toContain("apps"); + expect(folders).toContain("apps/api/src"); + expect(folders).toContain("apps/web/src"); + }); +}); diff --git a/packages/review-editor/utils/exportFeedback.workspace.test.ts b/packages/review-editor/utils/exportFeedback.workspace.test.ts new file mode 100644 index 000000000..0a57ef16b --- /dev/null +++ b/packages/review-editor/utils/exportFeedback.workspace.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect } from "bun:test"; +import { exportReviewFeedback } from "./exportFeedback"; +import type { CodeAnnotation } from "@plannotator/ui/types"; + +const ann = (overrides: Partial = {}): CodeAnnotation => ({ + id: "1", + type: "comment", + filePath: "src/index.ts", + lineStart: 10, + lineEnd: 10, + side: "new", + text: "This looks wrong", + createdAt: Date.now(), + ...overrides, +}); + +describe("exportReviewFeedback - workspace mode", () => { + it("workspace mode: uses generic header, no PR content (same as local mode)", () => { + // In workspace mode, prMetadata is explicitly undefined even if workspace exists + const result = exportReviewFeedback([ann()], undefined); + expect(result).toStartWith("# Code Review Feedback\n\n"); + expect(result).not.toContain("PR Review"); + expect(result).not.toContain("github.com"); + expect(result).not.toContain("Branch:"); + }); + + it("groups annotations by repo-prefixed file paths", () => { + const result = exportReviewFeedback([ + ann({ filePath: "repo-a/src/index.ts", lineStart: 5, text: "first" }), + ann({ filePath: "repo-b/src/index.ts", lineStart: 1, text: "second" }), + ]); + // Different repos with same relative path should be separate groups + expect(result).toContain("## repo-a/src/index.ts"); + expect(result).toContain("## repo-b/src/index.ts"); + }); + + it("sorts annotations by line number within each repo-prefixed file", () => { + const result = exportReviewFeedback([ + ann({ filePath: "repo-a/src/index.ts", lineStart: 20, text: "later" }), + ann({ filePath: "repo-a/src/index.ts", lineStart: 5, text: "earlier" }), + ann({ filePath: "repo-b/src/index.ts", lineStart: 15, text: "middle in repo-b" }), + ]); + const earlierIdx = result.indexOf("earlier"); + const laterIdx = result.indexOf("later"); + const middleInRepoB = result.indexOf("middle in repo-b"); + expect(earlierIdx).toBeLessThan(laterIdx); + // Both repo-a annotations should come before repo-b (alphabetical by path) + expect(laterIdx).toBeLessThan(middleInRepoB); + }); + + it("handles nested repo labels with overlapping paths", () => { + // Tests the longest-prefix matching behavior from resolveWorkspaceFilePath + const result = exportReviewFeedback([ + ann({ filePath: "apps/api/src/server.ts", text: "in nested repo" }), + ann({ filePath: "apps/web/src/app.ts", text: "in sibling repo" }), + ann({ filePath: "apps/src/main.ts", text: "in parent repo" }), + ]); + expect(result).toContain("## apps/api/src/server.ts"); + expect(result).toContain("## apps/web/src/app.ts"); + expect(result).toContain("## apps/src/main.ts"); + }); + + it("handles deeply nested repo labels", () => { + const result = exportReviewFeedback([ + ann({ filePath: "packages/shared/utils/helpers/string.ts", text: "deep path" }), + ]); + expect(result).toContain("## packages/shared/utils/helpers/string.ts"); + expect(result).toContain("### Line 10 (new)"); + }); + + it("groups multiple annotations on same repo-prefixed file together", () => { + const result = exportReviewFeedback([ + ann({ filePath: "repo-a/src/index.ts", lineStart: 5, text: "first comment" }), + ann({ filePath: "repo-b/src/index.ts", lineStart: 10, text: "second comment" }), + ann({ filePath: "repo-a/src/index.ts", lineStart: 15, text: "third comment" }), + ]); + // All repo-a comments should be grouped together + const repoAHeaderIdx = result.indexOf("## repo-a/src/index.ts"); + const repoBHeaderIdx = result.indexOf("## repo-b/src/index.ts"); + const firstCommentIdx = result.indexOf("first comment"); + const thirdCommentIdx = result.indexOf("third comment"); + const secondCommentIdx = result.indexOf("second comment"); + + expect(repoAHeaderIdx).toBeLessThan(repoBHeaderIdx); + expect(firstCommentIdx).toBeLessThan(thirdCommentIdx); + expect(thirdCommentIdx).toBeLessThan(repoBHeaderIdx); + expect(repoBHeaderIdx).toBeLessThan(secondCommentIdx); + }); + + it("handles file-scoped annotations with repo-prefixed paths", () => { + const result = exportReviewFeedback([ + ann({ filePath: "repo-a/src/index.ts", scope: "file", text: "file comment" }), + ann({ filePath: "repo-a/src/index.ts", lineStart: 1, lineEnd: 1, text: "line comment" }), + ]); + expect(result).toContain("## repo-a/src/index.ts"); + expect(result).toContain("### File Comment"); + expect(result).toContain("### Line 1"); + const fileIdx = result.indexOf("File Comment"); + const lineIdx = result.indexOf("Line 1"); + expect(fileIdx).toBeLessThan(lineIdx); + }); + + it("handles repo labels with special characters in paths", () => { + const result = exportReviewFeedback([ + ann({ filePath: "my-repo_2.0/src/index.ts", text: "special chars" }), + ]); + expect(result).toContain("## my-repo_2.0/src/index.ts"); + }); + + it("empty annotations returns generic message regardless of workspace mode", () => { + expect(exportReviewFeedback([], undefined)).toBe("# Code Review\n\nNo feedback provided."); + }); + + it("contains exactly one top-level heading in workspace mode", () => { + const result = exportReviewFeedback([ + ann({ filePath: "repo-a/src/a.ts" }), + ann({ filePath: "repo-b/src/b.ts" }), + ]); + const headingMatches = result.match(/^# /gm) || []; + expect(headingMatches).toHaveLength(1); + }); +}); diff --git a/packages/review-editor/utils/reviewSearch.workspace.test.ts b/packages/review-editor/utils/reviewSearch.workspace.test.ts new file mode 100644 index 000000000..400727cc5 --- /dev/null +++ b/packages/review-editor/utils/reviewSearch.workspace.test.ts @@ -0,0 +1,206 @@ +import { describe, it, expect } from "bun:test"; +import { + buildSearchIndex, + findMatchesInIndex, + findReviewSearchMatches, + groupReviewSearchMatches, +} from "./reviewSearch"; +import type { ReviewSearchableDiffFile } from "./reviewSearch"; + +const patchFile = (path: string, patch: string): ReviewSearchableDiffFile => ({ + path, + patch, + additions: 0, + deletions: 0, +}); + +describe("reviewSearch - workspace mode with repo-prefixed paths", () => { + const samplePatch = [ + "diff --git a/src/index.ts b/src/index.ts", + "--- a/src/index.ts", + "+++ b/src/index.ts", + "@@ -1,3 +1,3 @@", + " function greet() {", + "- return 'hello';", + "+ return 'hello world';", + " }", + ].join("\n"); + + it("builds search index with repo-prefixed file paths", () => { + const files = [ + patchFile("repo-a/src/index.ts", samplePatch), + patchFile("repo-b/src/index.ts", samplePatch), + ]; + const index = buildSearchIndex(files); + + // All lines should have repo-prefixed file paths + expect(index.every(line => line.filePath.startsWith("repo-"))).toBe(true); + expect(index.some(line => line.filePath === "repo-a/src/index.ts")).toBe(true); + expect(index.some(line => line.filePath === "repo-b/src/index.ts")).toBe(true); + }); + + it("finds matches across different repos with same relative path", () => { + const files = [ + patchFile("repo-a/src/utils.ts", [ + "diff --git a/src/utils.ts b/src/utils.ts", + "@@ -1 +1 @@", + "-const x = 1;", + "+const x = 2;", + ].join("\n")), + patchFile("repo-b/src/utils.ts", [ + "diff --git a/src/utils.ts b/src/utils.ts", + "@@ -1 +1 @@", + "-const y = 1;", + "+const y = 2;", + ].join("\n")), + ]; + const matches = findReviewSearchMatches(files, "const"); + + // Should find matches in both repos + const repoAMatches = matches.filter(m => m.filePath === "repo-a/src/utils.ts"); + const repoBMatches = matches.filter(m => m.filePath === "repo-b/src/utils.ts"); + + expect(repoAMatches.length).toBeGreaterThan(0); + expect(repoBMatches.length).toBeGreaterThan(0); + }); + + it("distinguishes same content in different repos", () => { + const files = [ + patchFile("repo-a/src/index.ts", [ + "diff --git a/src/index.ts b/src/index.ts", + "@@ -1 +1 @@", + "-old content", + "+new content", + ].join("\n")), + patchFile("repo-b/src/index.ts", [ + "diff --git a/src/index.ts b/src/index.ts", + "@@ -1 +1 @@", + "-old content", + "+new content", + ].join("\n")), + ]; + const matches = findReviewSearchMatches(files, "content"); + + // Should have separate match entries for each repo + const repoAIds = matches.filter(m => m.filePath === "repo-a/src/index.ts"); + const repoBIds = matches.filter(m => m.filePath === "repo-b/src/index.ts"); + + expect(repoAIds.length).toBe(2); // "old content" and "new content" + expect(repoBIds.length).toBe(2); + + // IDs should be different + const repoAIdSet = new Set(repoAIds.map(m => m.id)); + const repoBIdSet = new Set(repoBIds.map(m => m.id)); + expect(repoAIdSet.intersection(repoBIdSet).size).toBe(0); + }); + + it("handles deeply nested repo labels in search", () => { + const files = [ + patchFile("packages/shared/utils/helpers.ts", [ + "diff --git a/utils/helpers.ts b/utils/helpers.ts", + "@@ -1 +1 @@", + "-helper function", + "+improved helper", + ].join("\n")), + ]; + const matches = findReviewSearchMatches(files, "helper"); + + expect(matches.length).toBe(2); + expect(matches.every(m => m.filePath === "packages/shared/utils/helpers.ts")).toBe(true); + }); + + it("groups matches by repo-prefixed file path", () => { + const files = [ + patchFile("repo-a/src/index.ts", samplePatch), + patchFile("repo-b/src/other.ts", [ + "diff --git a/src/other.ts b/src/other.ts", + "@@ -1 +1 @@", + "-hello there", + "+goodbye there", + ].join("\n")), + ]; + const matches = findReviewSearchMatches(files, "hello"); + const groups = groupReviewSearchMatches(files, matches); + + // "hello" appears in both files (repo-a: "hello world", repo-b: "hello there") + expect(groups).toHaveLength(2); + const paths = groups.map(g => g.filePath).sort(); + expect(paths).toEqual(["repo-a/src/index.ts", "repo-b/src/other.ts"]); + }); + + it("maintains correct file indices with repo-prefixed paths", () => { + const files = [ + patchFile("repo-a/src/a.ts", samplePatch), + patchFile("repo-b/src/b.ts", samplePatch), + patchFile("repo-c/src/c.ts", samplePatch), + ]; + const matches = findReviewSearchMatches(files, "hello"); + const groups = groupReviewSearchMatches(files, matches); + + // Each group should have correct file index + const groupA = groups.find(g => g.filePath === "repo-a/src/a.ts"); + const groupB = groups.find(g => g.filePath === "repo-b/src/b.ts"); + const groupC = groups.find(g => g.filePath === "repo-c/src/c.ts"); + + expect(groupA?.fileIndex).toBe(0); + expect(groupB?.fileIndex).toBe(1); + expect(groupC?.fileIndex).toBe(2); + }); + + it("handles search in nested repo labels (longest prefix)", () => { + const files = [ + patchFile("apps/api/src/server.ts", [ + "diff --git a/src/server.ts b/src/server.ts", + "@@ -1 +1 @@", + "-server code", + "+better server", + ].join("\n")), + patchFile("apps/web/src/app.ts", [ + "diff --git a/src/app.ts b/src/app.ts", + "@@ -1 +1 @@", + "-app code", + "+better app", + ].join("\n")), + ]; + const matches = findReviewSearchMatches(files, "code"); + + const apiMatch = matches.find(m => m.filePath === "apps/api/src/server.ts"); + const webMatch = matches.find(m => m.filePath === "apps/web/src/app.ts"); + + expect(apiMatch).toBeDefined(); + expect(webMatch).toBeDefined(); + }); + + it("returns empty results for non-matching query in workspace", () => { + const files = [ + patchFile("repo-a/src/index.ts", samplePatch), + ]; + const matches = findReviewSearchMatches(files, "nonexistent"); + + expect(matches).toHaveLength(0); + }); + + it("handles empty file list in workspace mode", () => { + const index = buildSearchIndex([]); + expect(index).toHaveLength(0); + + const matches = findMatchesInIndex(index, "query"); + expect(matches).toHaveLength(0); + }); + + it("handles multiple matches on same line in same repo", () => { + const files = [ + patchFile("repo-a/src/index.ts", [ + "diff --git a/src/index.ts b/src/index.ts", + "@@ -1 +1 @@", + "-foo bar foo", + "+foo baz foo", + ].join("\n")), + ]; + const matches = findReviewSearchMatches(files, "foo"); + + // Should find 4 matches (2 on old line, 2 on new line) + expect(matches.length).toBe(4); + expect(matches.every(m => m.filePath === "repo-a/src/index.ts")).toBe(true); + }); +}); diff --git a/packages/server/claude-review.ts b/packages/server/claude-review.ts index 55dd167fb..3b50e86d8 100644 --- a/packages/server/claude-review.ts +++ b/packages/server/claude-review.ts @@ -277,6 +277,7 @@ export function transformClaudeFindings( findings: ClaudeFinding[], source: string, cwd?: string, + pathTransform?: (path: string) => string, ): Array<{ source: string; filePath: string; @@ -294,7 +295,9 @@ export function transformClaudeFindings( .filter(f => f.file && typeof f.line === "number") .map(f => ({ source, - filePath: toRelativePath(f.file, cwd), + filePath: pathTransform + ? pathTransform(toRelativePath(f.file, cwd)) + : toRelativePath(f.file, cwd), lineStart: f.line, lineEnd: f.end_line ?? f.line, type: "comment", diff --git a/packages/server/codex-review.ts b/packages/server/codex-review.ts index 62abef7e8..8dcf7ee47 100644 --- a/packages/server/codex-review.ts +++ b/packages/server/codex-review.ts @@ -347,6 +347,7 @@ export function transformReviewFindings( source: string, cwd?: string, author?: string, + pathTransform?: (path: string) => string, ): ReviewAnnotationInput[] { const annotations = findings .filter((f) => @@ -356,7 +357,9 @@ export function transformReviewFindings( ) .map((f) => ({ source, - filePath: toRelativePath(f.code_location.absolute_file_path, cwd), + filePath: pathTransform + ? pathTransform(toRelativePath(f.code_location.absolute_file_path, cwd)) + : toRelativePath(f.code_location.absolute_file_path, cwd), lineStart: f.code_location.line_range.start, lineEnd: f.code_location.line_range.end, type: "comment", diff --git a/packages/server/package.json b/packages/server/package.json index b3c8c0566..0a7f628d8 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -16,6 +16,7 @@ "./p4": "./p4.ts", "./vcs": "./vcs.ts", "./repo": "./repo.ts", + "./review-workspace": "./review-workspace.ts", "./share-url": "./share-url.ts", "./sessions": "./sessions.ts", "./project": "./project.ts", diff --git a/packages/server/review-workspace.test.ts b/packages/server/review-workspace.test.ts new file mode 100644 index 000000000..c21ed18c3 --- /dev/null +++ b/packages/server/review-workspace.test.ts @@ -0,0 +1,306 @@ +/** + * Workspace Review Tests + * + * Tests for workspace repo discovery, label building, and path resolution. + * Run: bun test packages/server/review-workspace.test.ts + */ + +import { afterEach, describe, expect, it } from "bun:test"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; + +import { + prefixPatchPaths, + resolveWorkspaceFilePath, + discoverWorkspaceRepoPaths, + type WorkspaceRepoRuntimeState, +} from "./review-workspace"; + +const tempDirs: string[] = []; + +function makeTempDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} + +function git(cwd: string, args: string[]): string { + const result = spawnSync("git", args, { cwd, encoding: "utf-8" }); + if (result.status !== 0) { + throw new Error(result.stderr || `git ${args.join(" ")} failed`); + } + return result.stdout.trim(); +} + +function initRepo(dir: string, initialBranch = "main"): void { + git(dir, ["init"]); + git(dir, ["branch", "-M", initialBranch]); + git(dir, ["config", "user.email", "test@example.com"]); + git(dir, ["config", "user.name", "Test User"]); + writeFileSync(join(dir, "README.md"), "# Test\n", "utf-8"); + git(dir, ["add", "README.md"]); + git(dir, ["commit", "-m", "initial"]); +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +describe("review-workspace", () => { + describe("prefixPatchPaths", () => { + it("prefixes diff headers with the repo label", () => { + const patch = [ + "diff --git a/src/index.ts b/src/index.ts", + "--- a/src/index.ts", + "+++ b/src/index.ts", + "@@ -1 +1 @@", + "-old", + "+new", + ].join("\n"); + + const result = prefixPatchPaths(patch, "repo-a"); + + expect(result).toContain("diff --git a/repo-a/src/index.ts b/repo-a/src/index.ts"); + expect(result).toContain("--- a/repo-a/src/index.ts"); + expect(result).toContain("+++ b/repo-a/src/index.ts"); + }); + + it("handles /dev/null paths correctly", () => { + const patch = [ + "diff --git a/src/index.ts b/src/index.ts", + "--- a/src/index.ts", + "+++ /dev/null", + "@@ -1 +0,0 @@", + "-content", + ].join("\n"); + + const result = prefixPatchPaths(patch, "repo-a"); + + expect(result).toContain("+++ /dev/null"); + expect(result).not.toContain("+++ b/repo-a/dev/null"); + }); + + it("handles empty patches", () => { + expect(prefixPatchPaths("", "repo-a")).toBe(""); + expect(prefixPatchPaths(" ", "repo-a")).toBe(" "); + }); + + it("handles nested paths correctly", () => { + const patch = [ + "diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts", + "--- a/packages/ui/src/index.ts", + "+++ b/packages/ui/src/index.ts", + ].join("\n"); + + const result = prefixPatchPaths(patch, "frontend"); + + expect(result).toContain("diff --git a/frontend/packages/ui/src/index.ts b/frontend/packages/ui/src/index.ts"); + }); + }); + + describe("resolveWorkspaceFilePath", () => { + it("resolves the longest matching repo label first", () => { + const repos = [ + { id: "1", label: "apps", cwd: "/tmp/apps", selected: true, source: "local", rawPatch: "", gitRef: "" }, + { id: "2", label: "apps/api", cwd: "/tmp/apps-api", selected: true, source: "local", rawPatch: "", gitRef: "" }, + ] as WorkspaceRepoRuntimeState[]; + + const resolved = resolveWorkspaceFilePath(repos, "apps/api/src/index.ts"); + + expect(resolved?.repo.id).toBe("2"); + expect(resolved?.repoRelativePath).toBe("src/index.ts"); + }); + + it("returns null when no repo matches", () => { + const repos = [ + { id: "1", label: "frontend", cwd: "/tmp/frontend", selected: true, source: "local", rawPatch: "", gitRef: "" }, + ] as WorkspaceRepoRuntimeState[]; + + const resolved = resolveWorkspaceFilePath(repos, "backend/src/index.ts"); + + expect(resolved).toBeNull(); + }); + + it("handles exact label matches", () => { + const repos = [ + { id: "1", label: "repo-a", cwd: "/tmp/repo-a", selected: true, source: "local", rawPatch: "", gitRef: "" }, + ] as WorkspaceRepoRuntimeState[]; + + const resolved = resolveWorkspaceFilePath(repos, "repo-a/file.ts"); + + expect(resolved?.repo.id).toBe("1"); + expect(resolved?.repoRelativePath).toBe("file.ts"); + }); + + it("validates file paths for directory traversal attacks", () => { + const repos = [ + { id: "1", label: "repo", cwd: "/tmp/repo", selected: true, source: "local", rawPatch: "", gitRef: "" }, + ] as WorkspaceRepoRuntimeState[]; + + expect(() => resolveWorkspaceFilePath(repos, "repo/../../../etc/passwd")).toThrow(); + }); + }); + + describe("discoverWorkspaceRepoPaths", () => { + it("excludes the root itself even if it is a git repo", () => { + // The function is designed to discover repos WITHIN a workspace root, + // not the root itself. This allows the workspace root to be a git repo + // (e.g., a meta-repo) while still discovering nested repos. + const root = makeTempDir("plannotator-workspace-root-repo-"); + initRepo(root); + + const repos = discoverWorkspaceRepoPaths(root); + + // Root itself is excluded even though it's a git repo + expect(repos).toHaveLength(0); + expect(repos).not.toContain(root); + }); + + it("discovers multiple nested git repos", () => { + const root = makeTempDir("plannotator-workspace-multi-"); + + // Create nested repos + const frontend = join(root, "frontend"); + const backend = join(root, "backend"); + const backendApi = join(backend, "api"); + + mkdirSync(frontend, { recursive: true }); + mkdirSync(backendApi, { recursive: true }); + + initRepo(frontend); + initRepo(backendApi); + + const repos = discoverWorkspaceRepoPaths(root); + + expect(repos).toHaveLength(2); + expect(repos).toContain(frontend); + expect(repos).toContain(backendApi); + expect(repos).not.toContain(root); + expect(repos).not.toContain(backend); // backend itself is not a repo + }); + + it("stops recursion at git repo boundaries (does not discover nested repos inside other repos)", () => { + const root = makeTempDir("plannotator-workspace-boundary-"); + + // Create a repo with a nested directory that would be a repo + const parentRepo = join(root, "parent"); + const childDir = join(parentRepo, "child"); + + mkdirSync(childDir, { recursive: true }); + initRepo(parentRepo); + + // Create a git repo inside the child (should NOT be discovered separately + // because parent repo stops recursion - we don't traverse into git repos) + const grandchildRepo = join(childDir, "grandchild"); + mkdirSync(grandchildRepo, { recursive: true }); + initRepo(grandchildRepo); + + const repos = discoverWorkspaceRepoPaths(root); + + // Only the parent should be discovered - grandchild is inside a git repo + expect(repos).toHaveLength(1); + expect(repos).toContain(parentRepo); + expect(repos).not.toContain(grandchildRepo); + }); + + it("skips ignored directories", () => { + const root = makeTempDir("plannotator-workspace-skip-"); + + // Create node_modules with a fake .git (should be skipped) + const nodeModules = join(root, "node_modules", "some-pkg"); + mkdirSync(nodeModules, { recursive: true }); + mkdirSync(join(nodeModules, ".git"), { recursive: true }); + + // Create a real repo + const realRepo = join(root, "src"); + mkdirSync(realRepo, { recursive: true }); + initRepo(realRepo); + + const repos = discoverWorkspaceRepoPaths(root); + + expect(repos).toHaveLength(1); + expect(repos[0]).toBe(realRepo); + }); + + it("returns empty array when root has no git repos", () => { + const root = makeTempDir("plannotator-workspace-empty-"); + + // Create some non-git directories + mkdirSync(join(root, "src"), { recursive: true }); + mkdirSync(join(root, "docs"), { recursive: true }); + writeFileSync(join(root, "README.md"), "# Project\n", "utf-8"); + + const repos = discoverWorkspaceRepoPaths(root); + + expect(repos).toHaveLength(0); + }); + + it("sorts results alphabetically", () => { + const root = makeTempDir("plannotator-workspace-sort-"); + + const zebra = join(root, "zebra"); + const alpha = join(root, "alpha"); + const beta = join(root, "beta"); + + mkdirSync(zebra, { recursive: true }); + mkdirSync(alpha, { recursive: true }); + mkdirSync(beta, { recursive: true }); + + initRepo(zebra); + initRepo(alpha); + initRepo(beta); + + const repos = discoverWorkspaceRepoPaths(root); + + expect(repos).toEqual([alpha, beta, zebra]); + }); + + it("handles deeply nested repos", () => { + const root = makeTempDir("plannotator-workspace-deep-"); + + const deepRepo = join(root, "a", "b", "c", "d", "repo"); + mkdirSync(deepRepo, { recursive: true }); + initRepo(deepRepo); + + const repos = discoverWorkspaceRepoPaths(root); + + expect(repos).toHaveLength(1); + expect(repos[0]).toBe(deepRepo); + }); + }); + + describe("buildRepoLabel (via discoverWorkspaceRepoPaths integration)", () => { + it("uses relative path as label when possible", () => { + // This is tested indirectly through the full workspace flow + // The label building logic is internal, but we verify it works + // through resolveWorkspaceFilePath tests with realistic labels + const repos = [ + { id: "1", label: "packages/frontend", cwd: "/tmp/packages/frontend", selected: true, source: "local", rawPatch: "", gitRef: "" }, + { id: "2", label: "packages/backend", cwd: "/tmp/packages/backend", selected: true, source: "local", rawPatch: "", gitRef: "" }, + ] as WorkspaceRepoRuntimeState[]; + + const resolved1 = resolveWorkspaceFilePath(repos, "packages/frontend/src/index.ts"); + const resolved2 = resolveWorkspaceFilePath(repos, "packages/backend/api.ts"); + + expect(resolved1?.repo.id).toBe("1"); + expect(resolved2?.repo.id).toBe("2"); + }); + + it("handles duplicate basename fallback", () => { + // When two repos have the same basename but different paths, + // the second should get a numbered suffix + const repos = [ + { id: "1", label: "api", cwd: "/tmp/apps/api", selected: true, source: "local", rawPatch: "", gitRef: "" }, + { id: "2", label: "api-2", cwd: "/tmp/services/api", selected: true, source: "local", rawPatch: "", gitRef: "" }, + ] as WorkspaceRepoRuntimeState[]; + + const resolved = resolveWorkspaceFilePath(repos, "api-2/src/index.ts"); + + expect(resolved?.repo.id).toBe("2"); + }); + }); +}); diff --git a/packages/server/review-workspace.ts b/packages/server/review-workspace.ts new file mode 100644 index 000000000..4ad49c82d --- /dev/null +++ b/packages/server/review-workspace.ts @@ -0,0 +1,404 @@ +import { existsSync, readdirSync, statSync } from "node:fs"; +import { basename, relative, resolve } from "node:path"; + +import type { DiffType, GitContext } from "./vcs"; +import { getVcsContext, runVcsDiff, validateFilePath } from "./vcs"; +import { fetchPR, parsePRUrl, type PRMetadata } from "./pr"; +import type { + WorkspacePRCandidate, + WorkspaceRepoSource, + WorkspaceRepoState, +} from "@plannotator/shared/review-workspace"; +import { resolveDefaultDiffType, loadConfig } from "@plannotator/shared/config"; +import { parseRemoteUrl } from "@plannotator/shared/repo"; + +const SKIP_DIRS = new Set([ + ".git", + "node_modules", + ".turbo", + ".next", + "dist", + "build", + "coverage", +]); + +export interface WorkspaceRepoRuntimeState extends WorkspaceRepoState { + rawPatch: string; + gitRef: string; +} + +function normalizePath(path: string): string { + return path.replace(/\\/g, "/").replace(/^\/+/, ""); +} + +function prefixRepoPath(label: string, filePath: string): string { + const normalizedFilePath = normalizePath(filePath); + if (normalizedFilePath === "/dev/null") return normalizedFilePath; + return `${normalizePath(label)}/${normalizedFilePath}`; +} + +function rewritePatchLine(line: string, label: string): string { + if (line.startsWith("diff --git a/")) { + const match = line.match(/^diff --git a\/(.+) b\/(.+)$/); + if (!match) return line; + return `diff --git a/${prefixRepoPath(label, match[1])} b/${prefixRepoPath(label, match[2])}`; + } + + if (line.startsWith("--- ")) { + const path = line.slice(4); + if (path === "/dev/null") return line; + if (path.startsWith("a/")) return `--- a/${prefixRepoPath(label, path.slice(2))}`; + return line; + } + + if (line.startsWith("+++ ")) { + const path = line.slice(4); + if (path === "/dev/null") return line; + if (path.startsWith("b/")) return `+++ b/${prefixRepoPath(label, path.slice(2))}`; + return line; + } + + return line; +} + +export function prefixPatchPaths(rawPatch: string, label: string): string { + if (!rawPatch.trim()) return rawPatch; + return rawPatch + .split("\n") + .map((line) => rewritePatchLine(line, label)) + .join("\n"); +} + +export function resolveWorkspaceFilePath( + repos: WorkspaceRepoRuntimeState[], + prefixedPath: string, +): { repo: WorkspaceRepoRuntimeState; repoRelativePath: string } | null { + validateFilePath(prefixedPath); + + for (const repo of [...repos].sort((a, b) => b.label.length - a.label.length)) { + const prefix = `${normalizePath(repo.label)}/`; + if (prefixedPath.startsWith(prefix)) { + return { + repo, + repoRelativePath: prefixedPath.slice(prefix.length), + }; + } + } + + return null; +} + +function hasGitMarker(dirPath: string): boolean { + return existsSync(resolve(dirPath, ".git")); +} + +function collectGitRepos(root: string, current: string, results: string[]): void { + let entries: ReturnType; + try { + entries = readdirSync(current, { withFileTypes: true }); + } catch { + return; + } + + if (current !== root && hasGitMarker(current)) { + results.push(current); + return; + } + + for (const entry of entries) { + if (!entry.isDirectory()) continue; + if (SKIP_DIRS.has(entry.name)) continue; + collectGitRepos(root, resolve(current, entry.name), results); + } +} + +export function discoverWorkspaceRepoPaths(root: string): string[] { + const resolvedRoot = resolve(root); + const results: string[] = []; + collectGitRepos(resolvedRoot, resolvedRoot, results); + return results.sort(); +} + +function buildRepoLabel(root: string, cwd: string, used = new Set()): string { + const rel = normalizePath(relative(root, cwd)); + const preferred = rel && rel !== "" ? rel : basename(cwd); + if (!used.has(preferred)) { + used.add(preferred); + return preferred; + } + + const fallback = normalizePath(basename(cwd)); + if (!used.has(fallback)) { + used.add(fallback); + return fallback; + } + + let counter = 2; + let next = `${fallback}-${counter}`; + while (used.has(next)) { + counter += 1; + next = `${fallback}-${counter}`; + } + used.add(next); + return next; +} + +function buildUniqueLabel(preferred: string, used = new Set()): string { + const normalized = normalizePath(preferred); + if (!used.has(normalized)) { + used.add(normalized); + return normalized; + } + + let counter = 2; + let next = `${normalized}-${counter}`; + while (used.has(next)) { + counter += 1; + next = `${normalized}-${counter}`; + } + used.add(next); + return next; +} + +async function discoverGitHubPRCandidate(cwd: string, gitContext: GitContext): Promise { + const branch = gitContext.currentBranch; + if (!branch || branch === "HEAD") return null; + + const remoteProc = Bun.spawn(["git", "remote", "get-url", "origin"], { + cwd, + stdout: "pipe", + stderr: "pipe", + }); + const [remoteUrl, remoteCode] = await Promise.all([ + new Response(remoteProc.stdout).text(), + remoteProc.exited, + ]); + if (remoteCode !== 0) return null; + + const repo = parseRemoteUrl(remoteUrl.trim()); + if (!repo) return null; + + const hostMatch = remoteUrl.trim().match(/^[^@]+@([^:]+):/)?.[1]; + const httpsHost = (() => { + try { + return new URL(remoteUrl.trim()).hostname; + } catch { + return null; + } + })(); + const host = hostMatch || httpsHost || "github.com"; + + const ghArgs = [ + "pr", + "list", + "--state", + "open", + "--head", + branch, + "--json", + "url", + "--limit", + "1", + ]; + const env = host === "github.com" ? process.env : { ...process.env, GH_HOST: host }; + let proc: ReturnType; + try { + proc = Bun.spawn(["gh", ...ghArgs], { + cwd, + env, + stdout: "pipe", + stderr: "ignore", + }); + } catch { + return null; + } + const [stdout, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + proc.exited, + ]); + if (exitCode !== 0) return null; + + let entries: Array<{ url?: string }>; + try { + entries = JSON.parse(stdout); + } catch { + return null; + } + + const url = entries[0]?.url; + if (!url) return null; + const ref = parsePRUrl(url); + if (!ref) return null; + + try { + const pr = await fetchPR(ref); + return { url, metadata: pr.metadata }; + } catch { + return null; + } +} + +async function discoverGitLabPRCandidate(cwd: string, gitContext: GitContext): Promise { + const branch = gitContext.currentBranch; + if (!branch || branch === "HEAD") return null; + + let proc: ReturnType; + try { + proc = Bun.spawn([ + "glab", + "mr", + "list", + "--source-branch", + branch, + "--state", + "opened", + "--output", + "json", + ], { + cwd, + stdout: "pipe", + stderr: "ignore", + }); + } catch { + return null; + } + const [stdout, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + proc.exited, + ]); + if (exitCode !== 0) return null; + + let entries: Array<{ web_url?: string }>; + try { + entries = JSON.parse(stdout); + } catch { + return null; + } + + const url = entries[0]?.web_url; + if (!url) return null; + const ref = parsePRUrl(url); + if (!ref) return null; + + try { + const pr = await fetchPR(ref); + return { url, metadata: pr.metadata }; + } catch { + return null; + } +} + +export async function discoverPRCandidates(cwd: string, gitContext: GitContext): Promise { + const candidates = await Promise.all([ + discoverGitHubPRCandidate(cwd, gitContext), + discoverGitLabPRCandidate(cwd, gitContext), + ]); + + return candidates.filter((candidate): candidate is WorkspacePRCandidate => !!candidate); +} + +export async function buildWorkspaceLocalRepos(root: string): Promise { + const repoPaths = discoverWorkspaceRepoPaths(root); + const defaultDiffType = resolveDefaultDiffType(loadConfig()); + const usedLabels = new Set(); + + const repos = await Promise.all(repoPaths.map(async (cwd, index) => { + const label = buildRepoLabel(root, cwd, usedLabels); + try { + const gitContext = await getVcsContext(cwd); + const diffType = gitContext.vcsType === "p4" ? "p4-default" : defaultDiffType; + const diffResult = await runVcsDiff(diffType, gitContext.defaultBranch, cwd); + const discoveredPRs = await discoverPRCandidates(cwd, gitContext); + return { + id: `repo-${index + 1}`, + label, + cwd, + selected: !!diffResult.patch.trim(), + source: "local" as WorkspaceRepoSource, + diffType, + gitContext, + diffOptions: gitContext.diffOptions, + discoveredPRs, + rawPatch: prefixPatchPaths(diffResult.patch, label), + gitRef: diffResult.label, + error: diffResult.error, + } satisfies WorkspaceRepoRuntimeState; + } catch (error) { + return { + id: `repo-${index + 1}`, + label, + cwd, + selected: false, + source: "local" as WorkspaceRepoSource, + rawPatch: "", + gitRef: "", + error: error instanceof Error ? error.message : String(error), + } satisfies WorkspaceRepoRuntimeState; + } + })); + + return repos; +} + +export async function buildWorkspacePRRepos(urls: string[]): Promise { + const usedLabels = new Set(); + const repos = await Promise.all(urls.map(async (url, index) => { + const ref = parsePRUrl(url); + if (!ref) { + return { + id: `repo-${index + 1}`, + label: `invalid-${index + 1}`, + cwd: process.cwd(), + selected: false, + source: "pr" as WorkspaceRepoSource, + rawPatch: "", + gitRef: "", + error: `Invalid PR/MR URL: ${url}`, + } satisfies WorkspaceRepoRuntimeState; + } + + try { + const pr = await fetchPR(ref); + const baseLabel = pr.metadata.platform === "github" + ? `${pr.metadata.owner}/${pr.metadata.repo}` + : pr.metadata.projectPath; + const label = buildUniqueLabel(baseLabel, usedLabels); + return { + id: `repo-${index + 1}`, + label, + cwd: process.cwd(), + selected: true, + source: "pr" as WorkspaceRepoSource, + prMetadata: pr.metadata, + rawPatch: prefixPatchPaths(pr.rawPatch, label), + gitRef: pr.metadata.url, + } satisfies WorkspaceRepoRuntimeState; + } catch (error) { + return { + id: `repo-${index + 1}`, + label: `pr-${index + 1}`, + cwd: process.cwd(), + selected: false, + source: "pr" as WorkspaceRepoSource, + rawPatch: "", + gitRef: "", + error: error instanceof Error ? error.message : String(error), + } satisfies WorkspaceRepoRuntimeState; + } + })); + + return repos; +} + +export function aggregateWorkspacePatch(repos: WorkspaceRepoRuntimeState[]): { + rawPatch: string; + gitRef: string; + errors: string[]; +} { + const selected = repos.filter((repo) => repo.selected); + return { + rawPatch: selected.map((repo) => repo.rawPatch.trim()).filter(Boolean).join("\n"), + gitRef: selected.map((repo) => repo.gitRef || repo.label).filter(Boolean).join(" | ") || "Workspace review", + errors: selected.flatMap((repo) => repo.error ? [`${repo.label}: ${repo.error}`] : []), + }; +} diff --git a/packages/server/review.ts b/packages/server/review.ts index 4c785c8fc..a9719333b 100644 --- a/packages/server/review.ts +++ b/packages/server/review.ts @@ -36,6 +36,8 @@ import { saveConfig, detectGitUser, getServerConfig } from "./config"; import { type PRMetadata, type PRReviewFileComment, fetchPRFileContent, fetchPRContext, submitPRReview, fetchPRViewedFiles, markPRFilesViewed, getPRUser, prRefFromMetadata, getDisplayRepo, getMRLabel, getMRNumberLabel } from "./pr"; import { createAIEndpoints, ProviderRegistry, SessionManager, createProvider, type AIEndpoints, type PiSDKConfig } from "@plannotator/ai"; import { isWSL } from "./browser"; +import { aggregateWorkspacePatch, prefixPatchPaths, resolveWorkspaceFilePath, type WorkspaceRepoRuntimeState } from "./review-workspace"; +import type { WorkspaceReviewState } from "@plannotator/shared/review-workspace"; // Re-export utilities export { isRemoteSession, getServerPort } from "./remote"; @@ -75,6 +77,8 @@ export interface ReviewServerOptions { agentCwd?: string; /** Cleanup callback invoked when server stops (e.g., remove temp worktree) */ onCleanup?: () => void | Promise; + /** Workspace-mode repo state for multi-repo review */ + workspaceRepos?: WorkspaceRepoRuntimeState[]; } export interface ReviewServerResult { @@ -112,32 +116,66 @@ const RETRY_DELAY_MS = 500; export async function startReviewServer( options: ReviewServerOptions ): Promise { - const { htmlContent, origin, gitContext, sharingEnabled = true, shareBaseUrl, onReady, prMetadata } = options; + const { htmlContent, origin, gitContext, sharingEnabled = true, shareBaseUrl, onReady, prMetadata, workspaceRepos } = options; const isPRMode = !!prMetadata; + const isWorkspaceMode = !!workspaceRepos?.length; const hasLocalAccess = !!gitContext; const draftKey = contentHash(options.rawPatch); const editorAnnotations = createEditorAnnotationHandler(); const externalAnnotations = createExternalAnnotationHandler("review"); // Mutable state for diff switching - let currentPatch = options.rawPatch; - let currentGitRef = options.gitRef; + const initialWorkspaceSnapshot = workspaceRepos ? aggregateWorkspacePatch(workspaceRepos) : null; + let currentPatch = initialWorkspaceSnapshot?.rawPatch ?? options.rawPatch; + let currentGitRef = initialWorkspaceSnapshot?.gitRef ?? options.gitRef; let currentDiffType: DiffType = options.diffType || "uncommitted"; - let currentError = options.error; + let currentError = initialWorkspaceSnapshot?.errors.join("\n") || options.error; + let currentActiveRepoId = workspaceRepos?.find((repo) => repo.selected)?.id ?? workspaceRepos?.[0]?.id ?? null; + + const getWorkspaceState = (): WorkspaceReviewState | null => { + if (!workspaceRepos) return null; + return { + mode: "workspace", + repos: workspaceRepos.map(({ rawPatch: _rawPatch, gitRef: _gitRef, ...repo }) => repo), + }; + }; + + const refreshWorkspaceAggregate = () => { + if (!workspaceRepos) return; + const snapshot = aggregateWorkspacePatch(workspaceRepos); + currentPatch = snapshot.rawPatch; + currentGitRef = snapshot.gitRef; + currentError = snapshot.errors.length > 0 ? snapshot.errors.join("\n") : undefined; + }; + + const getActiveRepo = (): WorkspaceRepoRuntimeState | null => { + if (!workspaceRepos?.length) return null; + return workspaceRepos.find((repo) => repo.id === currentActiveRepoId) + ?? workspaceRepos.find((repo) => repo.selected) + ?? workspaceRepos[0] + ?? null; + }; + + const getRepoForFilePath = (filePath: string): { repo: WorkspaceRepoRuntimeState; repoRelativePath: string } | null => { + if (!workspaceRepos) return null; + return resolveWorkspaceFilePath(workspaceRepos, filePath); + }; // Agent jobs — background process manager (late-binds serverUrl via getter) let serverUrl = ""; const agentJobs = createAgentJobHandler({ mode: "review", getServerUrl: () => serverUrl, - getCwd: () => { - if (options.agentCwd) return options.agentCwd; - return resolveVcsCwd(currentDiffType, gitContext?.cwd) ?? process.cwd(); - }, + getCwd: () => { + const activeRepo = getActiveRepo(); + if (activeRepo) return activeRepo.cwd; + if (options.agentCwd) return options.agentCwd; + return resolveVcsCwd(currentDiffType, gitContext?.cwd) ?? process.cwd(); + }, async buildCommand(provider) { - const cwd = options.agentCwd ?? resolveVcsCwd(currentDiffType, gitContext?.cwd) ?? process.cwd(); + const cwd = getActiveRepo()?.cwd ?? options.agentCwd ?? resolveVcsCwd(currentDiffType, gitContext?.cwd) ?? process.cwd(); const hasAgentLocalAccess = !!options.agentCwd || !!gitContext; const userMessage = buildCodexReviewUserMessage( currentPatch, @@ -180,7 +218,14 @@ export async function startReviewServer( }; if (output.findings.length > 0) { - const annotations = transformReviewFindings(output.findings, job.source, cwd, "Codex"); + const activeRepo = getActiveRepo(); + const annotations = transformReviewFindings( + output.findings, + job.source, + cwd, + "Codex", + activeRepo ? (filePath) => `${activeRepo.label}/${filePath}` : undefined, + ); const result = externalAnnotations.addAnnotations({ annotations }); if ("error" in result) console.error(`[codex-review] addAnnotations error:`, result.error); } @@ -203,7 +248,13 @@ export async function startReviewServer( }; if (output.findings.length > 0) { - const annotations = transformClaudeFindings(output.findings, job.source, cwd); + const activeRepo = getActiveRepo(); + const annotations = transformClaudeFindings( + output.findings, + job.source, + cwd, + activeRepo ? (filePath) => `${activeRepo.label}/${filePath}` : undefined, + ); const result = externalAnnotations.addAnnotations({ annotations }); if ("error" in result) console.error(`[claude-review] addAnnotations error:`, result.error); } @@ -289,10 +340,12 @@ export async function startReviewServer( aiEndpoints = createAIEndpoints({ registry: aiRegistry, sessionManager: aiSessionManager, - getCwd: () => { - if (options.agentCwd) return options.agentCwd; - return resolveVcsCwd(currentDiffType, gitContext?.cwd) ?? process.cwd(); - }, + getCwd: () => { + const activeRepo = getActiveRepo(); + if (activeRepo) return activeRepo.cwd; + if (options.agentCwd) return options.agentCwd; + return resolveVcsCwd(currentDiffType, gitContext?.cwd) ?? process.cwd(); + }, }); } @@ -303,7 +356,9 @@ export async function startReviewServer( // Detect repo info (cached for this session) // In PR mode, derive from metadata instead of local git - const repoInfo = isPRMode + const repoInfo = isWorkspaceMode + ? { display: `${workspaceRepos?.filter((repo) => repo.selected).length ?? 0} repos`, branch: "Workspace Review" } + : isPRMode ? { display: getDisplayRepo(prMetadata), branch: `${getMRLabel(prMetadata)} ${getMRNumberLabel(prMetadata)}` } : await getRepoInfo(); @@ -324,6 +379,24 @@ export async function startReviewServer( } } + if (workspaceRepos?.length) { + await Promise.all(workspaceRepos.map(async (repo) => { + if (!repo.prMetadata) return; + const repoPrRef = prRefFromMetadata(repo.prMetadata); + repo.platformUser = await getPRUser(repoPrRef); + if (repo.prMetadata.platform === "github") { + try { + const viewedMap = await fetchPRViewedFiles(repoPrRef); + repo.viewedFiles = Object.entries(viewedMap) + .filter(([, isViewed]) => isViewed) + .map(([path]) => `${repo.label}/${path}`); + } catch { + // Best effort + } + } + })); + } + // Decision promise let resolveDecision: (result: { approved: boolean; @@ -359,11 +432,12 @@ export async function startReviewServer( rawPatch: currentPatch, gitRef: currentGitRef, origin, - diffType: hasLocalAccess ? currentDiffType : undefined, - gitContext: hasLocalAccess ? gitContext : undefined, + diffType: hasLocalAccess && !isWorkspaceMode ? currentDiffType : undefined, + gitContext: hasLocalAccess && !isWorkspaceMode ? gitContext : undefined, sharingEnabled, shareBaseUrl, repoInfo, + workspace: getWorkspaceState(), isWSL: wslFlag, ...(options.agentCwd && { agentCwd: options.agentCwd }), ...(isPRMode && { prMetadata, platformUser }), @@ -375,6 +449,37 @@ export async function startReviewServer( // API: Switch diff type (requires local file access) if (url.pathname === "/api/diff/switch" && req.method === "POST") { + if (isWorkspaceMode) { + try { + const body = (await req.json()) as { repoId: string; diffType: DiffType }; + const repo = workspaceRepos?.find((candidate) => candidate.id === body.repoId); + if (!repo) { + return Response.json({ error: "Unknown repo" }, { status: 404 }); + } + if (!repo.gitContext) { + return Response.json({ error: "Diff switching unavailable for this repo" }, { status: 400 }); + } + + const nextDiffType = body.diffType || (repo.diffType as DiffType | undefined) || "uncommitted"; + const result = await runVcsDiff(nextDiffType, repo.gitContext.defaultBranch, repo.cwd); + repo.diffType = nextDiffType; + repo.rawPatch = prefixPatchPaths(result.patch, repo.label); + repo.gitRef = result.label; + repo.error = result.error; + refreshWorkspaceAggregate(); + + return Response.json({ + rawPatch: currentPatch, + gitRef: currentGitRef, + workspace: getWorkspaceState(), + ...(currentError && { error: currentError }), + }); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to switch workspace diff"; + return Response.json({ error: message }, { status: 500 }); + } + } + if (!hasLocalAccess) { return Response.json( { error: "Not available without local file access" }, @@ -417,8 +522,100 @@ export async function startReviewServer( } } + if (url.pathname === "/api/workspace/repo" && req.method === "PATCH") { + if (!isWorkspaceMode || !workspaceRepos) { + return Response.json({ error: "Not in workspace mode" }, { status: 400 }); + } + try { + const body = (await req.json()) as { + repoId: string; + selected?: boolean; + source?: "local" | "pr"; + prUrl?: string; + }; + const repo = workspaceRepos.find((candidate) => candidate.id === body.repoId); + if (!repo) return Response.json({ error: "Unknown repo" }, { status: 404 }); + + if (typeof body.selected === "boolean") repo.selected = body.selected; + if (body.source) repo.source = body.source; + + if (repo.source === "pr") { + const nextUrl = body.prUrl || repo.prMetadata?.url || repo.discoveredPRs?.[0]?.url; + if (!nextUrl) { + return Response.json({ error: "No PR/MR available for this repo" }, { status: 400 }); + } + const ref = parsePRUrl(nextUrl); + if (!ref) return Response.json({ error: "Invalid PR/MR URL" }, { status: 400 }); + const pr = await fetchPR(ref); + repo.prMetadata = pr.metadata; + repo.rawPatch = prefixPatchPaths(pr.rawPatch, repo.label); + repo.gitRef = pr.metadata.url; + repo.platformUser = await getPRUser(prRefFromMetadata(pr.metadata)); + if (pr.metadata.platform === "github") { + try { + const viewedMap = await fetchPRViewedFiles(prRefFromMetadata(pr.metadata)); + repo.viewedFiles = Object.entries(viewedMap) + .filter(([, isViewed]) => isViewed) + .map(([path]) => `${repo.label}/${path}`); + } catch { + repo.viewedFiles = []; + } + } + repo.error = undefined; + } else if (repo.gitContext) { + const nextDiffType = (repo.diffType as DiffType | undefined) || "uncommitted"; + const result = await runVcsDiff(nextDiffType, repo.gitContext.defaultBranch, repo.cwd); + repo.rawPatch = prefixPatchPaths(result.patch, repo.label); + repo.gitRef = result.label; + repo.prMetadata = undefined; + repo.platformUser = null; + repo.viewedFiles = []; + repo.error = result.error; + } + + refreshWorkspaceAggregate(); + return Response.json({ + rawPatch: currentPatch, + gitRef: currentGitRef, + workspace: getWorkspaceState(), + ...(currentError && { error: currentError }), + }); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to update workspace repo"; + return Response.json({ error: message }, { status: 500 }); + } + } + + if (url.pathname === "/api/workspace/active" && req.method === "POST") { + if (!isWorkspaceMode) { + return Response.json({ error: "Not in workspace mode" }, { status: 400 }); + } + try { + const body = (await req.json()) as { repoId?: string }; + if (body.repoId) currentActiveRepoId = body.repoId; + return Response.json({ ok: true }); + } catch { + return Response.json({ error: "Invalid request" }, { status: 400 }); + } + } + // API: Fetch PR context (comments, checks, merge status) — PR mode only if (url.pathname === "/api/pr-context" && req.method === "GET") { + if (isWorkspaceMode && workspaceRepos) { + const repoId = url.searchParams.get("repoId"); + const repo = repoId ? workspaceRepos.find((candidate) => candidate.id === repoId) : getActiveRepo(); + if (!repo?.prMetadata) { + return Response.json({ error: "Repo is not in PR mode" }, { status: 400 }); + } + try { + const context = await fetchPRContext(prRefFromMetadata(repo.prMetadata)); + return Response.json(context); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to fetch PR context"; + return Response.json({ error: message }, { status: 500 }); + } + } + if (!isPRMode) { return Response.json( { error: "Not in PR mode" }, @@ -451,6 +648,35 @@ export async function startReviewServer( } } + if (isWorkspaceMode && workspaceRepos) { + const resolved = getRepoForFilePath(filePath); + if (!resolved) { + return Response.json({ error: "Unknown repo path" }, { status: 404 }); + } + const resolvedOld = oldPath ? getRepoForFilePath(oldPath) : null; + + if (resolved.repo.source === "pr" && resolved.repo.prMetadata) { + const ref = prRefFromMetadata(resolved.repo.prMetadata); + const oldSha = resolved.repo.prMetadata.mergeBaseSha ?? resolved.repo.prMetadata.baseSha; + const [oldContent, newContent] = await Promise.all([ + fetchPRFileContent(ref, oldSha, resolvedOld?.repoRelativePath || resolved.repoRelativePath), + fetchPRFileContent(ref, resolved.repo.prMetadata.headSha, resolved.repoRelativePath), + ]); + return Response.json({ oldContent, newContent }); + } + + if (resolved.repo.gitContext) { + const result = await getVcsFileContentsForDiff( + (resolved.repo.diffType as DiffType) || "uncommitted", + resolved.repo.gitContext.defaultBranch, + resolved.repoRelativePath, + resolvedOld?.repoRelativePath, + resolved.repo.cwd, + ); + return Response.json(result); + } + } + // Local review: read file contents from local git if (hasLocalAccess) { const defaultBranch = gitContext?.defaultBranch || "main"; @@ -482,6 +708,31 @@ export async function startReviewServer( // API: Stage / unstage a file (disabled when VCS doesn't support it) if (url.pathname === "/api/git-add" && req.method === "POST") { + if (isWorkspaceMode && workspaceRepos) { + try { + const body = (await req.json()) as { filePath: string; undo?: boolean }; + const resolved = getRepoForFilePath(body.filePath); + if (!resolved) { + return Response.json({ error: "Unknown repo path" }, { status: 404 }); + } + if (resolved.repo.source !== "local" || !resolved.repo.gitContext) { + return Response.json({ error: "Staging not available" }, { status: 400 }); + } + + const repoDiffType = (resolved.repo.diffType as DiffType) || "uncommitted"; + const cwd = resolveVcsCwd(repoDiffType, resolved.repo.cwd); + if (body.undo) { + await unstageFile(repoDiffType, resolved.repoRelativePath, cwd); + } else { + await stageFile(repoDiffType, resolved.repoRelativePath, cwd); + } + return Response.json({ ok: true }); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to stage file"; + return Response.json({ error: message }, { status: 500 }); + } + } + if (isPRMode || !canStageFiles(currentDiffType)) { return Response.json( { error: "Staging not available" }, @@ -598,6 +849,36 @@ export async function startReviewServer( // API: Submit PR review directly to GitHub (PR mode only) if (url.pathname === "/api/pr-action" && req.method === "POST") { + if (isWorkspaceMode && workspaceRepos) { + const body = (await req.json()) as { + repoId: string; + action: "approve" | "comment"; + body: string; + fileComments: PRReviewFileComment[]; + }; + const repo = workspaceRepos.find((candidate) => candidate.id === body.repoId); + if (!repo?.prMetadata) { + return Response.json({ error: "Repo is not in PR mode" }, { status: 400 }); + } + try { + const prRefForRepo = prRefFromMetadata(repo.prMetadata); + await submitPRReview( + prRefForRepo, + repo.prMetadata.headSha, + body.action, + body.body, + body.fileComments.map((comment) => ({ + ...comment, + path: resolveWorkspaceFilePath(workspaceRepos, comment.path)?.repoRelativePath ?? comment.path, + })), + ); + return Response.json({ ok: true, prUrl: repo.prMetadata.url }); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to submit PR review"; + return Response.json({ error: message }, { status: 500 }); + } + } + if (!isPRMode || !prMetadata) { return Response.json({ error: "Not in PR mode" }, { status: 400 }); } @@ -630,6 +911,30 @@ export async function startReviewServer( // API: Mark/unmark PR files as viewed on GitHub (PR mode, GitHub only) if (url.pathname === "/api/pr-viewed" && req.method === "POST") { + if (isWorkspaceMode && workspaceRepos) { + const body = (await req.json()) as { + repoId: string; + filePaths: string[]; + viewed: boolean; + }; + const repo = workspaceRepos.find((candidate) => candidate.id === body.repoId); + if (!repo?.prMetadata || repo.prMetadata.platform !== "github" || !repo.prMetadata.prNodeId) { + return Response.json({ error: "Viewed sync not available for this repo" }, { status: 400 }); + } + try { + await markPRFilesViewed( + prRefFromMetadata(repo.prMetadata), + repo.prMetadata.prNodeId, + body.filePaths.map((filePath) => resolveWorkspaceFilePath(workspaceRepos, filePath)?.repoRelativePath ?? filePath), + body.viewed, + ); + return Response.json({ ok: true }); + } catch (err) { + const message = err instanceof Error ? err.message : "Failed to update viewed state"; + return Response.json({ error: message }, { status: 500 }); + } + } + if (!isPRMode || !prMetadata) { return Response.json({ error: "Not in PR mode" }, { status: 400 }); } @@ -667,7 +972,12 @@ export async function startReviewServer( // Serve embedded HTML for all other routes (SPA) return new Response(htmlContent, { - headers: { "Content-Type": "text/html" }, + headers: { + "Content-Type": "text/html", + "Cache-Control": "no-store, no-cache, must-revalidate", + "Pragma": "no-cache", + "Expires": "0", + }, }); }, diff --git a/packages/server/shared-handlers.ts b/packages/server/shared-handlers.ts index 83675d3d5..941e93a95 100644 --- a/packages/server/shared-handlers.ts +++ b/packages/server/shared-handlers.ts @@ -141,5 +141,7 @@ export async function handleServerReady( isRemote: boolean, _port: number, ): Promise { - await openBrowser(url, { isRemote }); + const freshUrl = new URL(url); + freshUrl.searchParams.set("session", `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`); + await openBrowser(freshUrl.toString(), { isRemote }); } diff --git a/packages/server/vcs.test.ts b/packages/server/vcs.test.ts new file mode 100644 index 000000000..75aa34f44 --- /dev/null +++ b/packages/server/vcs.test.ts @@ -0,0 +1,209 @@ +/** + * VCS Detection Tests + * + * Tests for VCS provider detection and the workspace root fallback behavior. + * Run: bun test packages/server/vcs.test.ts + */ + +import { afterEach, describe, expect, it } from "bun:test"; +import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { spawnSync } from "node:child_process"; + +import { detectManagedVcs, detectVcs } from "./vcs"; + +const tempDirs: string[] = []; + +function makeTempDir(prefix: string): string { + const dir = mkdtempSync(join(tmpdir(), prefix)); + tempDirs.push(dir); + return dir; +} + +function git(cwd: string, args: string[]): string { + const result = spawnSync("git", args, { cwd, encoding: "utf-8" }); + if (result.status !== 0) { + throw new Error(result.stderr || `git ${args.join(" ")} failed`); + } + return result.stdout.trim(); +} + +function initRepo(dir: string, initialBranch = "main"): void { + git(dir, ["init"]); + git(dir, ["branch", "-M", initialBranch]); + git(dir, ["config", "user.email", "test@example.com"]); + git(dir, ["config", "user.name", "Test User"]); + writeFileSync(join(dir, "README.md"), "# Test\n", "utf-8"); + git(dir, ["add", "README.md"]); + git(dir, ["commit", "-m", "initial"]); +} + +afterEach(() => { + for (const dir of tempDirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } +}); + +describe("vcs detection", () => { + describe("detectManagedVcs", () => { + it("returns git provider when inside a git repo", async () => { + const repoDir = makeTempDir("plannotator-vcs-git-"); + initRepo(repoDir); + + const provider = await detectManagedVcs(repoDir); + + expect(provider).not.toBeNull(); + expect(provider?.id).toBe("git"); + }); + + it("returns null when not in any VCS repo", async () => { + const nonVcsDir = makeTempDir("plannotator-vcs-none-"); + // Just create a regular file, no git init + writeFileSync(join(nonVcsDir, "file.txt"), "content", "utf-8"); + + const provider = await detectManagedVcs(nonVcsDir); + + expect(provider).toBeNull(); + }); + + it("returns null for non-git workspace root with nested git repos", async () => { + // This is the key regression test: a workspace root that is NOT a git repo + // but contains multiple git repos should NOT be detected as having a VCS + const workspaceRoot = makeTempDir("plannotator-vcs-workspace-"); + + // Create nested git repos + const frontend = join(workspaceRoot, "frontend"); + const backend = join(workspaceRoot, "backend"); + mkdirSync(frontend, { recursive: true }); + mkdirSync(backend, { recursive: true }); + + initRepo(frontend); + initRepo(backend); + + // Verify the workspace root itself is NOT a git repo + const gitDir = join(workspaceRoot, ".git"); + const fs = await import("node:fs"); + expect(fs.existsSync(gitDir)).toBe(false); + + // The workspace root should NOT be detected as having a VCS + const provider = await detectManagedVcs(workspaceRoot); + + expect(provider).toBeNull(); + }); + }); + + describe("detectVcs", () => { + it("returns git provider when inside a git repo", async () => { + const repoDir = makeTempDir("plannotator-vcs-detect-git-"); + initRepo(repoDir); + + const provider = await detectVcs(repoDir); + + expect(provider.id).toBe("git"); + }); + + it("falls back to git provider when no VCS detected (legacy behavior)", async () => { + // This tests the current fallback behavior - when no VCS is detected, + // it defaults to git provider. This is the existing behavior that + // the workspace review feature needs to handle carefully. + const nonVcsDir = makeTempDir("plannotator-vcs-fallback-"); + writeFileSync(join(nonVcsDir, "file.txt"), "content", "utf-8"); + + const provider = await detectVcs(nonVcsDir); + + // Falls back to git provider even though not in a git repo + expect(provider.id).toBe("git"); + }); + + it("caches provider detection results", async () => { + const repoDir = makeTempDir("plannotator-vcs-cache-"); + initRepo(repoDir); + + // First call should detect and cache + const provider1 = await detectVcs(repoDir); + + // Second call should return cached result + const provider2 = await detectVcs(repoDir); + + expect(provider1).toBe(provider2); + }); + }); + + describe("regression: non-git workspace roots", () => { + it("workspace root without .git should not be treated as single-repo", async () => { + // Regression test for: non-git workspace roots were incorrectly treated + // as single-repo due to VCS fallback behavior + // + // Before the fix, running `plannotator review` in a workspace root that + // contained multiple git repos but wasn't itself a git repo would: + // 1. detectVcs() would fall back to git provider + // 2. The review would treat the entire workspace as a single repo + // 3. This would show incorrect diff or fail to find git context + // + // After the fix, workspace review mode should: + // 1. Check if the CWD is a git repo using detectManagedVcs() + // 2. If not, discover nested repos and enter workspace mode + // 3. Only fall back to single-repo mode if detectManagedVcs() returns a provider + + const workspaceRoot = makeTempDir("plannotator-regression-workspace-"); + + // Create multiple nested git repos (simulating a typical workspace) + const packages = join(workspaceRoot, "packages"); + const apps = join(workspaceRoot, "apps"); + + const uiPkg = join(packages, "ui"); + const apiPkg = join(packages, "api"); + const webApp = join(apps, "web"); + + mkdirSync(uiPkg, { recursive: true }); + mkdirSync(apiPkg, { recursive: true }); + mkdirSync(webApp, { recursive: true }); + + initRepo(uiPkg); + initRepo(apiPkg); + initRepo(webApp); + + // Verify workspace root is NOT a git repo + const fs = await import("node:fs"); + expect(fs.existsSync(join(workspaceRoot, ".git"))).toBe(false); + + // detectManagedVcs should return null (no VCS at workspace root) + const managedVcs = await detectManagedVcs(workspaceRoot); + expect(managedVcs).toBeNull(); + + // But detectVcs (with fallback) returns git provider + // This is the legacy behavior that workspace review must handle + const fallbackVcs = await detectVcs(workspaceRoot); + expect(fallbackVcs.id).toBe("git"); + + // The key assertion: workspace review code should use detectManagedVcs() + // to determine if it's in a repo, NOT detectVcs() which has fallback + }); + + it("single git repo at CWD should use single-repo mode", async () => { + // When CWD is itself a git repo, we should use single-repo review mode + const repoDir = makeTempDir("plannotator-regression-single-"); + initRepo(repoDir); + + const managedVcs = await detectManagedVcs(repoDir); + + expect(managedVcs).not.toBeNull(); + expect(managedVcs?.id).toBe("git"); + }); + + it("nested git repo at CWD should use single-repo mode", async () => { + // When CWD is a nested git repo within a workspace, use single-repo mode + const workspaceRoot = makeTempDir("plannotator-regression-nested-"); + + const frontend = join(workspaceRoot, "frontend"); + mkdirSync(frontend, { recursive: true }); + initRepo(frontend); + + const managedVcs = await detectManagedVcs(frontend); + + expect(managedVcs).not.toBeNull(); + expect(managedVcs?.id).toBe("git"); + }); + }); +}); diff --git a/packages/server/vcs.ts b/packages/server/vcs.ts index 5c15d02f1..0fb0b06d3 100644 --- a/packages/server/vcs.ts +++ b/packages/server/vcs.ts @@ -162,17 +162,26 @@ export { parseWorktreeDiffType, validateFilePath, runtime as gitRuntime } from " const vcsCache = new Map(); +/** Detect which VCS manages the given directory, without fallback. */ +export async function detectManagedVcs(cwd?: string): Promise { + for (const provider of providers) { + if (await provider.detect(cwd)) { + return provider; + } + } + return null; +} + /** Detect which VCS manages the given directory */ export async function detectVcs(cwd?: string): Promise { const key = cwd ?? process.cwd(); const cached = vcsCache.get(key); if (cached) return cached; - for (const provider of providers) { - if (await provider.detect(cwd)) { - vcsCache.set(key, provider); - return provider; - } + const detected = await detectManagedVcs(cwd); + if (detected) { + vcsCache.set(key, detected); + return detected; } // Default to git (existing behavior) diff --git a/packages/shared/package.json b/packages/shared/package.json index cdbb4b0cb..725ae02bc 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -8,6 +8,7 @@ "./crypto": "./crypto.ts", "./feedback-templates": "./feedback-templates.ts", "./review-core": "./review-core.ts", + "./review-workspace": "./review-workspace.ts", "./checklist": "./checklist.ts", "./types": "./types.ts", "./pr-provider": "./pr-provider.ts", diff --git a/packages/shared/review-workspace.ts b/packages/shared/review-workspace.ts new file mode 100644 index 000000000..34d9e2b50 --- /dev/null +++ b/packages/shared/review-workspace.ts @@ -0,0 +1,30 @@ +import type { DiffOption, GitContext } from "./review-core"; +import type { PRMetadata } from "./pr-provider"; + +export type WorkspaceRepoSource = "local" | "pr"; + +export interface WorkspacePRCandidate { + url: string; + metadata: PRMetadata; +} + +export interface WorkspaceRepoState { + id: string; + label: string; + cwd: string; + selected: boolean; + source: WorkspaceRepoSource; + diffType?: string; + gitContext?: GitContext; + prMetadata?: PRMetadata; + discoveredPRs?: WorkspacePRCandidate[]; + diffOptions?: DiffOption[]; + platformUser?: string | null; + viewedFiles?: string[]; + error?: string; +} + +export interface WorkspaceReviewState { + mode: "workspace"; + repos: WorkspaceRepoState[]; +} diff --git a/packages/shared/types.ts b/packages/shared/types.ts index d12a50d0e..57caccc2e 100644 --- a/packages/shared/types.ts +++ b/packages/shared/types.ts @@ -15,3 +15,10 @@ export type { WorktreeInfo, GitContext, } from "./review-core"; + +export type { + WorkspaceRepoSource, + WorkspacePRCandidate, + WorkspaceRepoState, + WorkspaceReviewState, +} from "./review-workspace"; From ff7c5596d1687c69dda9c7fdfd561256f7f3b79b Mon Sep 17 00:00:00 2001 From: Oscar Silva Date: Fri, 24 Apr 2026 18:21:55 -0300 Subject: [PATCH 2/4] fix(workspace): address critical issues from deep review - Fix race condition in label generation by pre-computing labels sequentially - Fix rewritePatchLine to support quoted paths and rename/copy headers - Add separator between aggregated patches to avoid invalid diffs - Normalize input paths in resolveWorkspaceFilePath - Add timeout to PR discovery (15s) to prevent server hangs - Fix PATCH /api/workspace/repo to rollback state on failure via applyRepoMutation - Validate body.source runtime (must be 'local' or 'pr') - Snapshot active repo in agent jobs at launch to prevent race in onJobComplete - Prevent double-prefixing of agent findings when paths are already prefixed - Fix frontend findWorkspaceRepoForPath to use longest-prefix matching - Fix shared types: diffType uses DiffType, platformUser is string | null --- bun.lock | 387 +--------------------------- packages/review-editor/App.tsx | 10 +- packages/server/agent-jobs.ts | 5 +- packages/server/review-workspace.ts | 84 +++++- packages/server/review.ts | 120 +++++---- packages/shared/agent-jobs.ts | 4 + packages/shared/review-workspace.ts | 6 +- 7 files changed, 173 insertions(+), 443 deletions(-) diff --git a/bun.lock b/bun.lock index c7407b5de..d8f3d1b2b 100644 --- a/bun.lock +++ b/bun.lock @@ -64,7 +64,7 @@ }, "apps/opencode-plugin": { "name": "@plannotator/opencode", - "version": "0.19.0", + "version": "0.19.1", "dependencies": { "@opencode-ai/plugin": "^1.1.10", }, @@ -86,7 +86,7 @@ }, "apps/pi-extension": { "name": "@plannotator/pi-extension", - "version": "0.19.0", + "version": "0.19.1", "dependencies": { "@joplin/turndown-plugin-gfm": "^1.0.64", "turndown": "^7.2.4", @@ -141,17 +141,6 @@ "vite-plugin-singlefile": "^2.0.3", }, }, - "apps/videos": { - "name": "videos", - "version": "1.0.0", - "dependencies": { - "@remotion/cli": "^4.0.443", - "@remotion/google-fonts": "^4.0.443", - "react": "^19.2.4", - "react-dom": "^19.2.4", - "remotion": "^4.0.443", - }, - }, "apps/vscode-extension": { "name": "plannotator-webview", "version": "0.16.5", @@ -200,7 +189,7 @@ }, "packages/server": { "name": "@plannotator/server", - "version": "0.19.0", + "version": "0.19.1", "dependencies": { "@plannotator/ai": "workspace:*", "@plannotator/shared": "workspace:*", @@ -441,12 +430,8 @@ "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], - "@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" } }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], - "@emnapi/runtime": ["@emnapi/runtime@1.7.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="], - "@esbuild-plugins/node-globals-polyfill": ["@esbuild-plugins/node-globals-polyfill@0.2.3", "", { "peerDependencies": { "esbuild": "*" } }, "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw=="], "@esbuild-plugins/node-modules-polyfill": ["@esbuild-plugins/node-modules-polyfill@0.2.2", "", { "dependencies": { "escape-string-regexp": "^4.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, "peerDependencies": { "esbuild": "*" } }, "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA=="], @@ -621,12 +606,6 @@ "@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="], - "@mediabunny/aac-encoder": ["@mediabunny/aac-encoder@1.39.2", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-KD6KADVzAnW7tqhRFGBOX4uaiHbd0Yxvg0lfthj3wJLAEEgEBAvi43w+ZXWeEn54X/jpabrLe4bW/eYFFvlbUA=="], - - "@mediabunny/flac-encoder": ["@mediabunny/flac-encoder@1.39.2", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-VwBr3AzZTPEEPvt4aladZiXwOf3W293eq213zDupGQi/taS8WWNqDd3eBdf8FfvlbXATfbRiycXDKyQ0HlOZaQ=="], - - "@mediabunny/mp3-encoder": ["@mediabunny/mp3-encoder@1.39.2", "", { "peerDependencies": { "mediabunny": "^1.0.0" } }, "sha512-3rrodrGnUpUP8F2d1aRUl8IvjqK3jegkupbOzvOokooSAO5rXk2Lr5jZe7TnPeiVGiXfmnoJ7s9uyUOHlCd8qw=="], - "@mermaid-js/parser": ["@mermaid-js/parser@0.6.3", "", { "dependencies": { "langium": "3.3.1" } }, "sha512-lnjOhe7zyHjc+If7yT4zoedx2vo4sHaTmtkl1+or8BRTnCtDmcTpAjpzDSfCZrshM5bCoz0GyidzadJAH1xobA=="], "@mistralai/mistralai": ["@mistralai/mistralai@1.10.0", "", { "dependencies": { "zod": "^3.20.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg=="], @@ -635,20 +614,6 @@ "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.28.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw=="], - "@module-federation/error-codes": ["@module-federation/error-codes@0.22.0", "", {}, "sha512-xF9SjnEy7vTdx+xekjPCV5cIHOGCkdn3pIxo9vU7gEZMIw0SvAEdsy6Uh17xaCpm8V0FWvR0SZoK9Ik6jGOaug=="], - - "@module-federation/runtime": ["@module-federation/runtime@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/runtime-core": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-38g5iPju2tPC3KHMPxRKmy4k4onNp6ypFPS1eKGsNLUkXgHsPMBFqAjDw96iEcjri91BrahG4XcdyKi97xZzlA=="], - - "@module-federation/runtime-core": ["@module-federation/runtime-core@0.22.0", "", { "dependencies": { "@module-federation/error-codes": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-GR1TcD6/s7zqItfhC87zAp30PqzvceoeDGYTgF3Vx2TXvsfDrhP6Qw9T4vudDQL3uJRne6t7CzdT29YyVxlgIA=="], - - "@module-federation/runtime-tools": ["@module-federation/runtime-tools@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/webpack-bundler-runtime": "0.22.0" } }, "sha512-4ScUJ/aUfEernb+4PbLdhM/c60VHl698Gn1gY21m9vyC1Ucn69fPCA1y2EwcCB7IItseRMoNhdcWQnzt/OPCNA=="], - - "@module-federation/sdk": ["@module-federation/sdk@0.22.0", "", {}, "sha512-x4aFNBKn2KVQRuNVC5A7SnrSCSqyfIWmm1DvubjbO9iKFe7ith5niw8dqSFBekYBg2Fwy+eMg4sEFNVvCAdo6g=="], - - "@module-federation/webpack-bundler-runtime": ["@module-federation/webpack-bundler-runtime@0.22.0", "", { "dependencies": { "@module-federation/runtime": "0.22.0", "@module-federation/sdk": "0.22.0" } }, "sha512-aM8gCqXu+/4wBmJtVeMeeMN5guw3chf+2i6HajKtQv7SJfxV/f4IyNQJUeUQu9HfiAZHjqtMV5Lvq/Lvh8LdyA=="], - - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.0.7", "", { "dependencies": { "@emnapi/core": "^1.5.0", "@emnapi/runtime": "^1.5.0", "@tybys/wasm-util": "^0.10.1" } }, "sha512-SeDnOO0Tk7Okiq6DbXmmBODgOAb9dp9gjlphokTUxmt8U3liIP1ZsozBahH69j/RJv+Rfs6IwUKHTgQYJ/HBAw=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], @@ -819,48 +784,6 @@ "@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="], - "@remotion/bundler": ["@remotion/bundler@4.0.448", "", { "dependencies": { "@remotion/media-parser": "4.0.448", "@remotion/studio": "4.0.448", "@remotion/studio-shared": "4.0.448", "@rspack/core": "1.7.6", "@rspack/plugin-react-refresh": "1.6.1", "esbuild": "0.25.0", "loader-utils": "2.0.4", "postcss": "8.5.1", "postcss-value-parser": "4.2.0", "react-refresh": "0.18.0", "remotion": "4.0.448", "source-map": "0.7.3", "style-loader": "4.0.0", "webpack": "5.105.0" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-vCjEGYIQ7bA/Ba64B1t3kG1VBxG5ciJhKuZtgd3r/DE0VM64blf+v5obZYc/zU6Rs0I9Bex8geambP/VNBIyqQ=="], - - "@remotion/cli": ["@remotion/cli@4.0.448", "", { "dependencies": { "@remotion/bundler": "4.0.448", "@remotion/media-utils": "4.0.448", "@remotion/player": "4.0.448", "@remotion/renderer": "4.0.448", "@remotion/studio": "4.0.448", "@remotion/studio-server": "4.0.448", "@remotion/studio-shared": "4.0.448", "dotenv": "17.3.1", "minimist": "1.2.6", "prompts": "2.4.2", "remotion": "4.0.448" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" }, "bin": { "remotion": "remotion-cli.js", "remotionb": "remotionb-cli.js", "remotiond": "remotiond-cli.js" } }, "sha512-n5ghra63XUoLvlUdHdyLj6EXWB4KhxOWo8wUn8UB1+c6jSpNDAPbwbu6XdPCSbgPJn3A1LjZ4LZPm1lP0xYmvQ=="], - - "@remotion/compositor-darwin-arm64": ["@remotion/compositor-darwin-arm64@4.0.448", "", { "os": "darwin", "cpu": "arm64" }, "sha512-vjFdLhGZhXFqqO+HKOQnaKFLVNW2SUNz6jf087gL8V3PLn5HQnmu5t1k1nyQfSxsXEeOhmwMIFy6gD41N5DJOw=="], - - "@remotion/compositor-darwin-x64": ["@remotion/compositor-darwin-x64@4.0.448", "", { "os": "darwin", "cpu": "x64" }, "sha512-Ymb0fBiNNTp8iQCYcYwodDCXVXb4x0tQM6hKgDRUyWyXgLZWid37eAJcYZntjgCVQQXsXe2q1j7DnLjGEhI32w=="], - - "@remotion/compositor-linux-arm64-gnu": ["@remotion/compositor-linux-arm64-gnu@4.0.448", "", { "os": "linux", "cpu": "arm64" }, "sha512-cYr22U3USY/B4Xj55gQiq9IgbfsF8YQKxt9kUDIi96wYNHV1Wwsdcwxp/FtUxh7JLet3XhNAeShjSvQTj1sN+g=="], - - "@remotion/compositor-linux-arm64-musl": ["@remotion/compositor-linux-arm64-musl@4.0.448", "", { "os": "linux", "cpu": "arm64" }, "sha512-aKRU3P88BFXfyF9UDqp3NkG2s/RXnR6um+UZrJXCTRlFXaMzH+j4ysReMvijkIMLfE2OoKg8WAu63tGD/EfMmQ=="], - - "@remotion/compositor-linux-x64-gnu": ["@remotion/compositor-linux-x64-gnu@4.0.448", "", { "os": "linux", "cpu": "x64" }, "sha512-HdbR3Plifz1+VLO8VGtnP2+uW0PiX378jUQA+k0/anXqa4BDgLX5GptLeHK7bU0Iio/Hbu6wsEGLMmDr80U1/Q=="], - - "@remotion/compositor-linux-x64-musl": ["@remotion/compositor-linux-x64-musl@4.0.448", "", { "os": "linux", "cpu": "x64" }, "sha512-7OK/AtkKWFFrzgZF7Ti6Ld9vIsQ4Uh61k9iKPsmAw0f3CeXHJIuG0m3uhcLoqaBsD35fnL9D12vHgXPHpHyjng=="], - - "@remotion/compositor-win32-x64-msvc": ["@remotion/compositor-win32-x64-msvc@4.0.448", "", { "os": "win32", "cpu": "x64" }, "sha512-qjygadAQ+lEBmWRqM+KX1qWy69eXQLCkJ9xUkaLDWgtPkKNglvh+sBa2Nd9hp84ye4u+7fUO8MxKEPVSt2f54g=="], - - "@remotion/google-fonts": ["@remotion/google-fonts@4.0.448", "", { "dependencies": { "remotion": "4.0.448" } }, "sha512-mpey2nMUu8PLcAt8wnNFfA22vOLNUH60SjC/YOs7e8CUoisNyjEV9itKOwVdtoUdfKDOdeAWbAQuDrSsua7gTg=="], - - "@remotion/licensing": ["@remotion/licensing@4.0.448", "", {}, "sha512-weQ+yJLJN+NEeKKMlkHT9cydUSjxwGnteHqedAd3ffaBUOpRQthWJWQwLhCupqdFyDdkRt0Rtnh5ib7/dSByPA=="], - - "@remotion/media-parser": ["@remotion/media-parser@4.0.448", "", {}, "sha512-lZANRTl/EfjfFZgysm7GcnO9WgZdofdA67XcQ5oyrzmth5pCxcKcnv/ruKSlbBYCX8r2U2Bd+nknCcXg5e0DCw=="], - - "@remotion/media-utils": ["@remotion/media-utils@4.0.448", "", { "dependencies": { "mediabunny": "1.39.2", "remotion": "4.0.448" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-q6yrn0GriPq+npBh3Bg+8y6i+slLkpFFAZSr3u2S1p+bWRD1ijN3AQaJZ1LBHD4dCPTQh2Lr4UYHUUeoxKD90Q=="], - - "@remotion/player": ["@remotion/player@4.0.448", "", { "dependencies": { "remotion": "4.0.448" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-lXwiJvAqlaUTwt5NGoGma9Md5IF9nf4Zetn6OAaNk0zTiXegUN42IFBSmzKZC8qUwNV10IpeLsTlQQEv4ntU1Q=="], - - "@remotion/renderer": ["@remotion/renderer@4.0.448", "", { "dependencies": { "@remotion/licensing": "4.0.448", "@remotion/streaming": "4.0.448", "execa": "5.1.1", "extract-zip": "2.0.1", "remotion": "4.0.448", "source-map": "^0.8.0-beta.0", "ws": "8.17.1" }, "optionalDependencies": { "@remotion/compositor-darwin-arm64": "4.0.448", "@remotion/compositor-darwin-x64": "4.0.448", "@remotion/compositor-linux-arm64-gnu": "4.0.448", "@remotion/compositor-linux-arm64-musl": "4.0.448", "@remotion/compositor-linux-x64-gnu": "4.0.448", "@remotion/compositor-linux-x64-musl": "4.0.448", "@remotion/compositor-win32-x64-msvc": "4.0.448" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-ST3Gfa7Ai0G4hqkWvk6J438tRXCVypv+x8KqKdfbIUK4v7A12eg/a1hlu5Sms7ncpFOnm10BwdtoyNpAlf68eg=="], - - "@remotion/streaming": ["@remotion/streaming@4.0.448", "", {}, "sha512-RThhasaaxaQ6hoW+SkaNiaq4HwmwYkxfIJhsxZIrby9wLKGVOh1nG/GSfMSGaeJWsRp3KsLuqiwuQw2EuINQOw=="], - - "@remotion/studio": ["@remotion/studio@4.0.448", "", { "dependencies": { "@remotion/media-utils": "4.0.448", "@remotion/player": "4.0.448", "@remotion/renderer": "4.0.448", "@remotion/studio-shared": "4.0.448", "@remotion/web-renderer": "4.0.448", "@remotion/zod-types": "4.0.448", "mediabunny": "1.39.2", "memfs": "3.4.3", "open": "^8.4.2", "remotion": "4.0.448", "semver": "7.5.3", "source-map": "0.7.3", "zod": "4.3.6" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-6uGyBFze2cDApuCf8OkJSy+ZIlWjJxVUuU29JF3vQ9OCz+OMqcJt0L+SxfUGvKihlq4PxG7Y75BMJOGuCiEMoA=="], - - "@remotion/studio-server": ["@remotion/studio-server@4.0.448", "", { "dependencies": { "@babel/parser": "7.24.1", "@babel/types": "7.24.0", "@remotion/bundler": "4.0.448", "@remotion/renderer": "4.0.448", "@remotion/studio-shared": "4.0.448", "memfs": "3.4.3", "open": "^8.4.2", "prettier": "3.8.1", "recast": "0.23.11", "remotion": "4.0.448", "semver": "7.5.3", "source-map": "0.7.3" } }, "sha512-+ksos4XFA4EV1Ask5rI1k25gnSWBYFSeTkYC5HVHrFCTjE3eOxymeK49dDojHY4gvzod23M2Sckt88fYsFol4w=="], - - "@remotion/studio-shared": ["@remotion/studio-shared@4.0.448", "", { "dependencies": { "remotion": "4.0.448" } }, "sha512-XQWQt+i67kMEBeCXDM0Kd4XK/j2Q0aqwjjSyqz9fbnh4KYh3QGrd9M1AnG9EUBbskCkfzt7isy2d8M8LcRvTBg=="], - - "@remotion/web-renderer": ["@remotion/web-renderer@4.0.448", "", { "dependencies": { "@mediabunny/aac-encoder": "1.39.2", "@mediabunny/flac-encoder": "1.39.2", "@mediabunny/mp3-encoder": "1.39.2", "@remotion/licensing": "4.0.448", "mediabunny": "1.39.2", "remotion": "4.0.448" }, "peerDependencies": { "react": ">=18.0.0", "react-dom": ">=18.0.0" } }, "sha512-iPsoLPxxiHKWZPbTZLKKxygcISZP5+rajODeWNlM+qrEN001Ardzf2MlqY+Zb/qHWsdAotw+cBPVVAv0JVZPQw=="], - - "@remotion/zod-types": ["@remotion/zod-types@4.0.448", "", { "dependencies": { "remotion": "4.0.448" }, "peerDependencies": { "zod": "4.3.6" } }, "sha512-3Hvtz30SrnKq0H7PokzrF12XS/d4BOdEt3xB3Y9obpy9JYPqDRI2p0OUpdrpJv03+4hxG6GhI1N38Of0xCMYcQ=="], - "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.53", "", {}, "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.3.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q=="], @@ -909,34 +832,6 @@ "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.54.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg=="], - "@rspack/binding": ["@rspack/binding@1.7.6", "", { "optionalDependencies": { "@rspack/binding-darwin-arm64": "1.7.6", "@rspack/binding-darwin-x64": "1.7.6", "@rspack/binding-linux-arm64-gnu": "1.7.6", "@rspack/binding-linux-arm64-musl": "1.7.6", "@rspack/binding-linux-x64-gnu": "1.7.6", "@rspack/binding-linux-x64-musl": "1.7.6", "@rspack/binding-wasm32-wasi": "1.7.6", "@rspack/binding-win32-arm64-msvc": "1.7.6", "@rspack/binding-win32-ia32-msvc": "1.7.6", "@rspack/binding-win32-x64-msvc": "1.7.6" } }, "sha512-/NrEcfo8Gx22hLGysanrV6gHMuqZSxToSci/3M4kzEQtF5cPjfOv5pqeLK/+B6cr56ul/OmE96cCdWcXeVnFjQ=="], - - "@rspack/binding-darwin-arm64": ["@rspack/binding-darwin-arm64@1.7.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-NZ9AWtB1COLUX1tA9HQQvWpTy07NSFfKBU8A6ylWd5KH8AePZztpNgLLAVPTuNO4CZXYpwcoclf8jG/luJcQdQ=="], - - "@rspack/binding-darwin-x64": ["@rspack/binding-darwin-x64@1.7.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-J2g6xk8ZS7uc024dNTGTHxoFzFovAZIRixUG7PiciLKTMP78svbSSWrmW6N8oAsAkzYfJWwQpVgWfFNRHvYxSw=="], - - "@rspack/binding-linux-arm64-gnu": ["@rspack/binding-linux-arm64-gnu@1.7.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-eQfcsaxhFrv5FmtaA7+O1F9/2yFDNIoPZzV/ZvqvFz5bBXVc4FAm/1fVpBg8Po/kX1h0chBc7Xkpry3cabFW8w=="], - - "@rspack/binding-linux-arm64-musl": ["@rspack/binding-linux-arm64-musl@1.7.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-DfQXKiyPIl7i1yECHy4eAkSmlUzzsSAbOjgMuKn7pudsWf483jg0UUYutNgXSlBjc/QSUp7906Cg8oty9OfwPA=="], - - "@rspack/binding-linux-x64-gnu": ["@rspack/binding-linux-x64-gnu@1.7.6", "", { "os": "linux", "cpu": "x64" }, "sha512-NdA+2X3lk2GGrMMnTGyYTzM3pn+zNjaqXqlgKmFBXvjfZqzSsKq3pdD1KHZCd5QHN+Fwvoszj0JFsquEVhE1og=="], - - "@rspack/binding-linux-x64-musl": ["@rspack/binding-linux-x64-musl@1.7.6", "", { "os": "linux", "cpu": "x64" }, "sha512-rEy6MHKob02t/77YNgr6dREyJ0e0tv1X6Xsg8Z5E7rPXead06zefUbfazj4RELYySWnM38ovZyJAkPx/gOn3VA=="], - - "@rspack/binding-wasm32-wasi": ["@rspack/binding-wasm32-wasi@1.7.6", "", { "dependencies": { "@napi-rs/wasm-runtime": "1.0.7" }, "cpu": "none" }, "sha512-YupOrz0daSG+YBbCIgpDgzfMM38YpChv+afZpaxx5Ml7xPeAZIIdgWmLHnQ2rts73N2M1NspAiBwV00Xx0N4Vg=="], - - "@rspack/binding-win32-arm64-msvc": ["@rspack/binding-win32-arm64-msvc@1.7.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-INj7aVXjBvlZ84kEhSK4kJ484ub0i+BzgnjDWOWM1K+eFYDZjLdAsQSS3fGGXwVc3qKbPIssFfnftATDMTEJHQ=="], - - "@rspack/binding-win32-ia32-msvc": ["@rspack/binding-win32-ia32-msvc@1.7.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-lXGvC+z67UMcw58In12h8zCa9IyYRmuptUBMItQJzu+M278aMuD1nETyGLL7e4+OZ2lvrnnBIcjXN1hfw2yRzw=="], - - "@rspack/binding-win32-x64-msvc": ["@rspack/binding-win32-x64-msvc@1.7.6", "", { "os": "win32", "cpu": "x64" }, "sha512-zeUxEc0ZaPpmaYlCeWcjSJUPuRRySiSHN23oJ2Xyw0jsQ01Qm4OScPdr0RhEOFuK/UE+ANyRtDo4zJsY52Hadw=="], - - "@rspack/core": ["@rspack/core@1.7.6", "", { "dependencies": { "@module-federation/runtime-tools": "0.22.0", "@rspack/binding": "1.7.6", "@rspack/lite-tapable": "1.1.0" }, "peerDependencies": { "@swc/helpers": ">=0.5.1" }, "optionalPeers": ["@swc/helpers"] }, "sha512-Iax6UhrfZqJajA778c1d5DBFbSIqPOSrI34kpNIiNpWd8Jq7mFIa+Z60SQb5ZQDZuUxcCZikjz5BxinFjTkg7Q=="], - - "@rspack/lite-tapable": ["@rspack/lite-tapable@1.1.0", "", {}, "sha512-E2B0JhYFmVAwdDiG14+DW0Di4Ze4Jg10Pc4/lILUrd5DRCaklduz2OvJ5HYQ6G+hd+WTzqQb3QnDNfK4yvAFYw=="], - - "@rspack/plugin-react-refresh": ["@rspack/plugin-react-refresh@1.6.1", "", { "dependencies": { "error-stack-parser": "^2.1.4", "html-entities": "^2.6.0" }, "peerDependencies": { "react-refresh": ">=0.10.0 <1.0.0", "webpack-hot-middleware": "2.x" }, "optionalPeers": ["webpack-hot-middleware"] }, "sha512-eqqW5645VG3CzGzFgNg5HqNdHVXY+567PGjtDhhrM8t67caxmsSzRmT5qfoEIfBcGgFkH9vEg7kzXwmCYQdQDw=="], - "@secretlint/config-creator": ["@secretlint/config-creator@10.2.2", "", { "dependencies": { "@secretlint/types": "^10.2.2" } }, "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ=="], "@secretlint/config-loader": ["@secretlint/config-loader@10.2.2", "", { "dependencies": { "@secretlint/profiler": "^10.2.2", "@secretlint/resolver": "^10.2.2", "@secretlint/types": "^10.2.2", "ajv": "^8.17.1", "debug": "^4.4.1", "rc-config-loader": "^4.1.3" } }, "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ=="], @@ -1123,8 +1018,6 @@ "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], - "@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="], "@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="], @@ -1199,16 +1092,8 @@ "@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="], - "@types/dom-mediacapture-transform": ["@types/dom-mediacapture-transform@0.1.11", "", { "dependencies": { "@types/dom-webcodecs": "*" } }, "sha512-Y2p+nGf1bF2XMttBnsVPHUWzRRZzqUoJAKmiP10b5umnO6DDrWI0BrGDJy1pOHoOULVmGSfFNkQrAlC5dcj6nQ=="], - - "@types/dom-webcodecs": ["@types/dom-webcodecs@0.1.13", "", {}, "sha512-O5hkiFIcjjszPIYyUSyvScyvrBoV3NOEEZx/pMlsu44TKzWNkLVBBxnxJz42in5n3QIolYOcBYFCPZZ0h8SkwQ=="], - "@types/dompurify": ["@types/dompurify@3.2.0", "", { "dependencies": { "dompurify": "*" } }, "sha512-Fgg31wv9QbLDA0SpTOXO3MaxySc4DKGLi8sna4/Utjo4r3ZRPdCt4UQee8BWr+Q5z21yifghREPJGYaEOEIACg=="], - "@types/eslint": ["@types/eslint@9.6.1", "", { "dependencies": { "@types/estree": "*", "@types/json-schema": "*" } }, "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag=="], - - "@types/eslint-scope": ["@types/eslint-scope@3.7.7", "", { "dependencies": { "@types/eslint": "*", "@types/estree": "*" } }, "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg=="], - "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="], @@ -1217,8 +1102,6 @@ "@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="], - "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="], "@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="], @@ -1251,8 +1134,6 @@ "@types/vscode": ["@types/vscode@1.109.0", "", {}, "sha512-0Pf95rnwEIwDbmXGC08r0B4TQhAbsHQ5UyTIgVgoieDe4cOnf92usuR5dEczb6bTKEp7ziZH4TV1TRGPPCExtw=="], - "@types/yauzl": ["@types/yauzl@2.10.3", "", { "dependencies": { "@types/node": "*" } }, "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q=="], - "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.3", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-91fp6CAAJSRtH5ja95T1FHSKa8aPW9/Zw6cta81jlZTUw/+Vq8jM/AfF/14h2b71wwR84JUTW/3Y8QPhDAawFA=="], "@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="], @@ -1283,46 +1164,10 @@ "@vscode/vsce-sign-win32-x64": ["@vscode/vsce-sign-win32-x64@2.0.6", "", { "os": "win32", "cpu": "x64" }, "sha512-mgth9Kvze+u8CruYMmhHw6Zgy3GRX2S+Ed5oSokDEK5vPEwGGKnmuXua9tmFhomeAnhgJnL4DCna3TiNuGrBTQ=="], - "@webassemblyjs/ast": ["@webassemblyjs/ast@1.14.1", "", { "dependencies": { "@webassemblyjs/helper-numbers": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ=="], - - "@webassemblyjs/floating-point-hex-parser": ["@webassemblyjs/floating-point-hex-parser@1.13.2", "", {}, "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA=="], - - "@webassemblyjs/helper-api-error": ["@webassemblyjs/helper-api-error@1.13.2", "", {}, "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ=="], - - "@webassemblyjs/helper-buffer": ["@webassemblyjs/helper-buffer@1.14.1", "", {}, "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA=="], - - "@webassemblyjs/helper-numbers": ["@webassemblyjs/helper-numbers@1.13.2", "", { "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.13.2", "@webassemblyjs/helper-api-error": "1.13.2", "@xtuc/long": "4.2.2" } }, "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA=="], - - "@webassemblyjs/helper-wasm-bytecode": ["@webassemblyjs/helper-wasm-bytecode@1.13.2", "", {}, "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA=="], - - "@webassemblyjs/helper-wasm-section": ["@webassemblyjs/helper-wasm-section@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/wasm-gen": "1.14.1" } }, "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw=="], - - "@webassemblyjs/ieee754": ["@webassemblyjs/ieee754@1.13.2", "", { "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw=="], - - "@webassemblyjs/leb128": ["@webassemblyjs/leb128@1.13.2", "", { "dependencies": { "@xtuc/long": "4.2.2" } }, "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw=="], - - "@webassemblyjs/utf8": ["@webassemblyjs/utf8@1.13.2", "", {}, "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ=="], - - "@webassemblyjs/wasm-edit": ["@webassemblyjs/wasm-edit@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/helper-wasm-section": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-opt": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1", "@webassemblyjs/wast-printer": "1.14.1" } }, "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ=="], - - "@webassemblyjs/wasm-gen": ["@webassemblyjs/wasm-gen@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg=="], - - "@webassemblyjs/wasm-opt": ["@webassemblyjs/wasm-opt@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-buffer": "1.14.1", "@webassemblyjs/wasm-gen": "1.14.1", "@webassemblyjs/wasm-parser": "1.14.1" } }, "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw=="], - - "@webassemblyjs/wasm-parser": ["@webassemblyjs/wasm-parser@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@webassemblyjs/helper-api-error": "1.13.2", "@webassemblyjs/helper-wasm-bytecode": "1.13.2", "@webassemblyjs/ieee754": "1.13.2", "@webassemblyjs/leb128": "1.13.2", "@webassemblyjs/utf8": "1.13.2" } }, "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ=="], - - "@webassemblyjs/wast-printer": ["@webassemblyjs/wast-printer@1.14.1", "", { "dependencies": { "@webassemblyjs/ast": "1.14.1", "@xtuc/long": "4.2.2" } }, "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw=="], - - "@xtuc/ieee754": ["@xtuc/ieee754@1.2.0", "", {}, "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA=="], - - "@xtuc/long": ["@xtuc/long@4.2.2", "", {}, "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ=="], - "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], "acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="], - "acorn-import-phases": ["acorn-import-phases@1.0.4", "", { "peerDependencies": { "acorn": "^8.14.0" } }, "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ=="], - "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], @@ -1333,8 +1178,6 @@ "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - "ajv-keywords": ["ajv-keywords@5.1.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3" }, "peerDependencies": { "ajv": "^8.8.2" } }, "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw=="], - "ansi-align": ["ansi-align@3.0.1", "", { "dependencies": { "string-width": "^4.1.0" } }, "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w=="], "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], @@ -1359,7 +1202,7 @@ "as-table": ["as-table@1.0.55", "", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ=="], - "ast-types": ["ast-types@0.16.1", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg=="], + "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], @@ -1385,8 +1228,6 @@ "basic-ftp": ["basic-ftp@5.1.0", "", {}, "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw=="], - "big.js": ["big.js@5.2.2", "", {}, "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ=="], - "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "binaryextensions": ["binaryextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw=="], @@ -1459,8 +1300,6 @@ "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "chrome-trace-event": ["chrome-trace-event@1.0.4", "", {}, "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ=="], - "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], @@ -1661,8 +1500,6 @@ "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], - "dset": ["dset@3.1.4", "", {}, "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA=="], "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], @@ -1679,8 +1516,6 @@ "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], - "emojis-list": ["emojis-list@3.0.0", "", {}, "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q=="], - "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], @@ -1693,8 +1528,6 @@ "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], - "error-stack-parser": ["error-stack-parser@2.1.4", "", { "dependencies": { "stackframe": "^1.3.4" } }, "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ=="], - "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], @@ -1719,13 +1552,9 @@ "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], - "eslint-scope": ["eslint-scope@5.1.1", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" } }, "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw=="], - "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], - "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="], - - "estraverse": ["estraverse@4.3.0", "", {}, "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw=="], + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], "estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="], @@ -1747,14 +1576,10 @@ "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], - "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], - "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], "eventsource-parser": ["eventsource-parser@3.0.6", "", {}, "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg=="], - "execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], - "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], @@ -1767,8 +1592,6 @@ "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], - "extract-zip": ["extract-zip@2.0.1", "", { "dependencies": { "debug": "^4.1.1", "get-stream": "^5.1.0", "yauzl": "^2.10.0" }, "optionalDependencies": { "@types/yauzl": "^2.9.1" }, "bin": { "extract-zip": "cli.js" } }, "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], @@ -1813,8 +1636,6 @@ "fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], - "fs-monkey": ["fs-monkey@1.0.3", "", {}, "sha512-cybjIfiiE+pTWicSCLFHSrXZ6EilF30oh91FDP9S2B051prEa7QWfrVTQm10/dDpswBDXZugPa1Ogu8Yh+HV0Q=="], - "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -1837,8 +1658,6 @@ "get-source": ["get-source@2.0.12", "", { "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" } }, "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w=="], - "get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], - "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], @@ -1905,8 +1724,6 @@ "hosted-git-info": ["hosted-git-info@9.0.2", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg=="], - "html-entities": ["html-entities@2.6.0", "", {}, "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ=="], - "html-escaper": ["html-escaper@3.0.3", "", {}, "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ=="], "html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="], @@ -1921,8 +1738,6 @@ "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], - "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -1973,8 +1788,6 @@ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], - "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], @@ -1983,8 +1796,6 @@ "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], - "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], - "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], "jose": ["jose@6.2.2", "", {}, "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ=="], @@ -1997,8 +1808,6 @@ "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], - "json-parse-even-better-errors": ["json-parse-even-better-errors@2.3.1", "", {}, "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w=="], - "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -2059,10 +1868,6 @@ "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], - "loader-runner": ["loader-runner@4.3.1", "", {}, "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q=="], - - "loader-utils": ["loader-utils@2.0.4", "", { "dependencies": { "big.js": "^5.2.2", "emojis-list": "^3.0.0", "json5": "^2.1.2" } }, "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw=="], - "lodash": ["lodash@4.17.23", "", {}, "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="], "lodash-es": ["lodash-es@4.17.23", "", {}, "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="], @@ -2081,8 +1886,6 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], - "lodash.sortby": ["lodash.sortby@4.7.0", "", {}, "sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA=="], - "lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="], "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], @@ -2147,14 +1950,8 @@ "media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="], - "mediabunny": ["mediabunny@1.39.2", "", { "dependencies": { "@types/dom-mediacapture-transform": "^0.1.11", "@types/dom-webcodecs": "0.1.13" } }, "sha512-VcrisGRt+OI7tTPrziucJoCIPYIS/DEWY37TqzQVLWSUUHiyvsiRizEypQ3FOlhfIZ4ytAG/Mw4zxfetCTyKUg=="], - - "memfs": ["memfs@3.4.3", "", { "dependencies": { "fs-monkey": "1.0.3" } }, "sha512-eivjfi7Ahr6eQTn44nvTnR60e4a1Fs1Via2kCR5lHo/kyNoiMWaXCNJ/GpSd0ilXas2JSOl9B5FTIhflXu0hlg=="], - "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], - "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], "mermaid": ["mermaid@11.12.2", "", { "dependencies": { "@braintree/sanitize-url": "^7.1.1", "@iconify/utils": "^3.0.1", "@mermaid-js/parser": "^0.6.3", "@types/d3": "^7.4.3", "cytoscape": "^3.29.3", "cytoscape-cose-bilkent": "^4.1.0", "cytoscape-fcose": "^2.2.0", "d3": "^7.9.0", "d3-sankey": "^0.12.3", "dagre-d3-es": "7.0.13", "dayjs": "^1.11.18", "dompurify": "^3.2.5", "katex": "^0.16.22", "khroma": "^2.1.0", "lodash-es": "^4.17.21", "marked": "^16.2.1", "roughjs": "^4.6.6", "stylis": "^4.3.6", "ts-dedent": "^2.2.0", "uuid": "^11.1.0" } }, "sha512-n34QPDPEKmaeCG4WDMGy0OT6PSyxKCfy2pJgShP+Qow2KLrvWjclwbc3yXfSIf4BanqWEhQEpngWwNp/XhZt6w=="], @@ -2237,15 +2034,13 @@ "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], - "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], - "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], "miniflare": ["miniflare@3.20250718.3", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "acorn": "8.14.0", "acorn-walk": "8.3.2", "exit-hook": "2.2.1", "glob-to-regexp": "0.4.1", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250718.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ=="], "minimatch": ["minimatch@10.2.2", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw=="], - "minimist": ["minimist@1.2.6", "", {}, "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q=="], + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], @@ -2275,8 +2070,6 @@ "negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], - "neo-async": ["neo-async@2.6.2", "", {}, "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw=="], - "neotraverse": ["neotraverse@0.6.18", "", {}, "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA=="], "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], @@ -2303,8 +2096,6 @@ "normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="], - "npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], - "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], @@ -2319,8 +2110,6 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], - "oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="], "oniguruma-to-es": ["oniguruma-to-es@4.3.4", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA=="], @@ -2405,12 +2194,8 @@ "postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], - "postcss-value-parser": ["postcss-value-parser@4.2.0", "", {}, "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ=="], - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], - "prettier": ["prettier@3.8.1", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg=="], - "printable-characters": ["printable-characters@1.0.42", "", {}, "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ=="], "prismjs": ["prismjs@1.30.0", "", {}, "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw=="], @@ -2431,8 +2216,6 @@ "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], - "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], - "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], "qs": ["qs@6.15.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ=="], @@ -2469,8 +2252,6 @@ "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "recast": ["recast@0.23.11", "", { "dependencies": { "ast-types": "^0.16.1", "esprima": "~4.0.0", "source-map": "~0.6.1", "tiny-invariant": "^1.3.3", "tslib": "^2.0.1" } }, "sha512-YTUo+Flmw4ZXiWfQKGcwwc11KnoRAYgzAE2E7mXKCjSviTKShtxBsN6YUUBB2gtaBzKzeKunxhUwNHQuRryhWA=="], - "recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="], "recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="], @@ -2507,8 +2288,6 @@ "remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="], - "remotion": ["remotion@4.0.448", "", { "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4NBSmQRIzntZiLEtOU/bGMQXfnGgUsBoyrewdTRyYzPl+qnvlBjqwxcntsu6SDY/W7ucEwKZUTc7GDOcFhIuAA=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], @@ -2555,8 +2334,6 @@ "scheduler": ["scheduler@0.27.0", "", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="], - "schema-utils": ["schema-utils@4.3.3", "", { "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", "ajv-formats": "^2.1.1", "ajv-keywords": "^5.1.0" } }, "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA=="], - "secretlint": ["secretlint@10.2.2", "", { "dependencies": { "@secretlint/config-creator": "^10.2.2", "@secretlint/formatter": "^10.2.2", "@secretlint/node": "^10.2.2", "@secretlint/profiler": "^10.2.2", "debug": "^4.4.1", "globby": "^14.1.0", "read-pkg": "^9.0.1" }, "bin": "./bin/secretlint.js" }, "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -2625,8 +2402,6 @@ "spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="], - "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], - "stacktracey": ["stacktracey@2.1.8", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], @@ -2649,8 +2424,6 @@ "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], - "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], @@ -2659,8 +2432,6 @@ "structured-source": ["structured-source@4.0.0", "", { "dependencies": { "boundary": "^2.0.0" } }, "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA=="], - "style-loader": ["style-loader@4.0.0", "", { "peerDependencies": { "webpack": "^5.27.0" } }, "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA=="], - "style-to-js": ["style-to-js@1.1.21", "", { "dependencies": { "style-to-object": "1.0.14" } }, "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ=="], "style-to-object": ["style-to-object@1.0.14", "", { "dependencies": { "inline-style-parser": "0.2.7" } }, "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw=="], @@ -2687,8 +2458,6 @@ "terser": ["terser@5.46.1", "", { "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.15.0", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "bin": { "terser": "bin/terser" } }, "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ=="], - "terser-webpack-plugin": ["terser-webpack-plugin@5.4.0", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.25", "jest-worker": "^27.4.5", "schema-utils": "^4.3.0", "terser": "^5.31.1" }, "peerDependencies": { "webpack": "^5.1.0" } }, "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g=="], - "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], "textextensions": ["textextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ=="], @@ -2699,24 +2468,18 @@ "tiny-inflate": ["tiny-inflate@1.0.3", "", {}, "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw=="], - "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], - "tinyexec": ["tinyexec@1.0.2", "", {}, "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], - "to-fast-properties": ["to-fast-properties@2.0.0", "", {}, "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], - "tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], - "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], "trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="], @@ -2819,8 +2582,6 @@ "vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="], - "videos": ["videos@workspace:apps/videos"], - "vite": ["vite@6.4.1", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g=="], "vite-plugin-singlefile": ["vite-plugin-singlefile@2.3.0", "", { "dependencies": { "micromatch": "^4.0.8" }, "peerDependencies": { "rollup": "^4.44.1", "vite": "^5.4.11 || ^6.0.0 || ^7.0.0" } }, "sha512-DAcHzYypM0CasNLSz/WG0VdKOCxGHErfrjOoyIPiNxTPTGmO6rRD/te93n1YL/s+miXq66ipF1brMBikf99c6A=="], @@ -2839,24 +2600,14 @@ "vscode-uri": ["vscode-uri@3.0.8", "", {}, "sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw=="], - "watchpack": ["watchpack@2.5.1", "", { "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" } }, "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg=="], - "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], - "webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], - - "webpack": ["webpack@5.105.0", "", { "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", "acorn": "^8.15.0", "acorn-import-phases": "^1.0.3", "browserslist": "^4.28.1", "chrome-trace-event": "^1.0.2", "enhanced-resolve": "^5.19.0", "es-module-lexer": "^2.0.0", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.2.11", "json-parse-even-better-errors": "^2.3.1", "loader-runner": "^4.3.1", "mime-types": "^2.1.27", "neo-async": "^2.6.2", "schema-utils": "^4.3.3", "tapable": "^2.3.0", "terser-webpack-plugin": "^5.3.16", "watchpack": "^2.5.1", "webpack-sources": "^3.3.3" }, "bin": { "webpack": "bin/webpack.js" } }, "sha512-gX/dMkRQc7QOMzgTe6KsYFM7DxeIONQSui1s0n/0xht36HvrgbxtM1xBlgx596NbpHuQU8P7QpKwrZYwUX48nw=="], - - "webpack-sources": ["webpack-sources@3.3.4", "", {}, "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q=="], - "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], - "whatwg-url": ["whatwg-url@7.1.0", "", { "dependencies": { "lodash.sortby": "^4.7.0", "tr46": "^1.0.1", "webidl-conversions": "^4.0.2" } }, "sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], @@ -2973,36 +2724,6 @@ "@plannotator/review/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], - "@remotion/bundler/esbuild": ["esbuild@0.25.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.0", "@esbuild/android-arm": "0.25.0", "@esbuild/android-arm64": "0.25.0", "@esbuild/android-x64": "0.25.0", "@esbuild/darwin-arm64": "0.25.0", "@esbuild/darwin-x64": "0.25.0", "@esbuild/freebsd-arm64": "0.25.0", "@esbuild/freebsd-x64": "0.25.0", "@esbuild/linux-arm": "0.25.0", "@esbuild/linux-arm64": "0.25.0", "@esbuild/linux-ia32": "0.25.0", "@esbuild/linux-loong64": "0.25.0", "@esbuild/linux-mips64el": "0.25.0", "@esbuild/linux-ppc64": "0.25.0", "@esbuild/linux-riscv64": "0.25.0", "@esbuild/linux-s390x": "0.25.0", "@esbuild/linux-x64": "0.25.0", "@esbuild/netbsd-arm64": "0.25.0", "@esbuild/netbsd-x64": "0.25.0", "@esbuild/openbsd-arm64": "0.25.0", "@esbuild/openbsd-x64": "0.25.0", "@esbuild/sunos-x64": "0.25.0", "@esbuild/win32-arm64": "0.25.0", "@esbuild/win32-ia32": "0.25.0", "@esbuild/win32-x64": "0.25.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw=="], - - "@remotion/bundler/postcss": ["postcss@8.5.1", "", { "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ=="], - - "@remotion/bundler/source-map": ["source-map@0.7.3", "", {}, "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="], - - "@remotion/renderer/source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="], - - "@remotion/renderer/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], - - "@remotion/studio/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], - - "@remotion/studio/semver": ["semver@7.5.3", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ=="], - - "@remotion/studio/source-map": ["source-map@0.7.3", "", {}, "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="], - - "@remotion/studio/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - - "@remotion/studio-server/@babel/parser": ["@babel/parser@7.24.1", "", { "bin": "./bin/babel-parser.js" }, "sha512-Zo9c7N3xdOIQrNip7Lc9wvRPzlRtovHVE4lkz8WEDr7uYh/GMQhSiIgFxGIArRHYdJE5kxtZjAf8rT0xhdLCzg=="], - - "@remotion/studio-server/@babel/types": ["@babel/types@7.24.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.23.4", "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w=="], - - "@remotion/studio-server/open": ["open@8.4.2", "", { "dependencies": { "define-lazy-prop": "^2.0.0", "is-docker": "^2.1.1", "is-wsl": "^2.2.0" } }, "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ=="], - - "@remotion/studio-server/semver": ["semver@7.5.3", "", { "dependencies": { "lru-cache": "^6.0.0" }, "bin": { "semver": "bin/semver.js" } }, "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ=="], - - "@remotion/studio-server/source-map": ["source-map@0.7.3", "", {}, "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ=="], - - "@remotion/zod-types/zod": ["zod@4.3.6", "", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], - "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.7.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg=="], @@ -3075,20 +2796,12 @@ "d3-sankey/d3-shape": ["d3-shape@1.3.7", "", { "dependencies": { "d3-path": "1" } }, "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw=="], - "degenerator/ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], - "encoding-sniffer/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "escodegen/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "escodegen/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "express/cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], - "extract-zip/get-stream": ["get-stream@5.2.0", "", { "dependencies": { "pump": "^3.0.0" } }, "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA=="], - "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -3103,8 +2816,6 @@ "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - "jest-worker/supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], - "katex/commander": ["commander@8.3.0", "", {}, "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww=="], "magicast/@babel/parser": ["@babel/parser@7.29.0", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww=="], @@ -3139,18 +2850,12 @@ "parse5-parser-stream/parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - "prebuild-install/minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "protobufjs/@types/node": ["@types/node@22.19.3", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA=="], "proxy-agent/lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], - "rc/minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], - "recast/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], - "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="], @@ -3161,8 +2866,6 @@ "router/path-to-regexp": ["path-to-regexp@8.4.0", "", {}, "sha512-PuseHIvAnz3bjrM2rGJtSgo1zjgxapTLZ7x2pjhzWwlp4SJQgK3f3iZIQwkpEnBaKz6seKBADpM4B4ySkuYypg=="], - "schema-utils/ajv-formats": ["ajv-formats@2.1.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA=="], - "sitemap/@types/node": ["@types/node@17.0.45", "", {}, "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw=="], "source-map-support/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -3181,18 +2884,8 @@ "terser/commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], - "videos/react": ["react@19.2.5", "", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], - - "videos/react-dom": ["react-dom@19.2.5", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], - "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], - "webpack/enhanced-resolve": ["enhanced-resolve@5.20.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.0" } }, "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA=="], - - "webpack/es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="], - - "webpack/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - "whatwg-encoding/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "wrangler/esbuild": ["esbuild@0.17.19", "", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="], @@ -3237,72 +2930,6 @@ "@plannotator/review/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], - "@remotion/bundler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ=="], - - "@remotion/bundler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.0", "", { "os": "android", "cpu": "arm" }, "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g=="], - - "@remotion/bundler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.0", "", { "os": "android", "cpu": "arm64" }, "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g=="], - - "@remotion/bundler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.0", "", { "os": "android", "cpu": "x64" }, "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg=="], - - "@remotion/bundler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw=="], - - "@remotion/bundler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg=="], - - "@remotion/bundler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.0", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w=="], - - "@remotion/bundler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A=="], - - "@remotion/bundler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.0", "", { "os": "linux", "cpu": "arm" }, "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg=="], - - "@remotion/bundler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg=="], - - "@remotion/bundler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.0", "", { "os": "linux", "cpu": "ia32" }, "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg=="], - - "@remotion/bundler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw=="], - - "@remotion/bundler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ=="], - - "@remotion/bundler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw=="], - - "@remotion/bundler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.0", "", { "os": "linux", "cpu": "none" }, "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA=="], - - "@remotion/bundler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA=="], - - "@remotion/bundler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.0", "", { "os": "linux", "cpu": "x64" }, "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw=="], - - "@remotion/bundler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.0", "", { "os": "none", "cpu": "arm64" }, "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw=="], - - "@remotion/bundler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.0", "", { "os": "none", "cpu": "x64" }, "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA=="], - - "@remotion/bundler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.0", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw=="], - - "@remotion/bundler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.0", "", { "os": "openbsd", "cpu": "x64" }, "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg=="], - - "@remotion/bundler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.0", "", { "os": "sunos", "cpu": "x64" }, "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg=="], - - "@remotion/bundler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw=="], - - "@remotion/bundler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA=="], - - "@remotion/bundler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.0", "", { "os": "win32", "cpu": "x64" }, "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ=="], - - "@remotion/studio-server/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - - "@remotion/studio-server/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "@remotion/studio-server/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "@remotion/studio-server/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - - "@remotion/studio/open/define-lazy-prop": ["define-lazy-prop@2.0.0", "", {}, "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og=="], - - "@remotion/studio/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], - - "@remotion/studio/open/is-wsl": ["is-wsl@2.2.0", "", { "dependencies": { "is-docker": "^2.0.0" } }, "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww=="], - - "@remotion/studio/semver/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "@textlint/linter-formatter/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "@textlint/linter-formatter/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -3459,8 +3086,6 @@ "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], - "webpack/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - "wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.17.19", "", { "os": "android", "cpu": "arm" }, "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A=="], "wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="], diff --git a/packages/review-editor/App.tsx b/packages/review-editor/App.tsx index eccab5620..250572922 100644 --- a/packages/review-editor/App.tsx +++ b/packages/review-editor/App.tsx @@ -224,7 +224,15 @@ const ReviewApp: React.FC = () => { // Derived: Platform mode is active when destination is platform AND we have PR/MR metadata const findWorkspaceRepoForPath = useCallback((filePath?: string | null): WorkspaceRepoState | null => { if (!workspace || !filePath) return null; - return workspace.repos.find(repo => filePath === repo.label || filePath.startsWith(`${repo.label}/`)) ?? null; + let best: WorkspaceRepoState | null = null; + for (const repo of workspace.repos) { + if (filePath === repo.label || filePath.startsWith(`${repo.label}/`)) { + if (!best || repo.label.length > best.label.length) { + best = repo; + } + } + } + return best; }, [workspace]); const activeWorkspaceRepo = useMemo(() => { diff --git a/packages/server/agent-jobs.ts b/packages/server/agent-jobs.ts index c4917c7d4..efcaae28a 100644 --- a/packages/server/agent-jobs.ts +++ b/packages/server/agent-jobs.ts @@ -132,7 +132,7 @@ export function createAgentJobHandler(options: AgentJobHandlerOptions): AgentJob command: string[], label: string, outputPath?: string, - spawnOptions?: { captureStdout?: boolean; stdinPrompt?: string; cwd?: string; prompt?: string; engine?: string; model?: string; effort?: string; reasoningEffort?: string; fastMode?: boolean; diffContext?: AgentJobInfo["diffContext"] }, + spawnOptions?: { captureStdout?: boolean; stdinPrompt?: string; cwd?: string; prompt?: string; engine?: string; model?: string; effort?: string; reasoningEffort?: string; fastMode?: boolean; diffContext?: AgentJobInfo["diffContext"]; repoId?: string; repoLabel?: string; repoCwd?: string }, ): AgentJobInfo { const id = crypto.randomUUID(); const source = jobSource(id); @@ -152,6 +152,9 @@ export function createAgentJobHandler(options: AgentJobHandlerOptions): AgentJob ...(spawnOptions?.reasoningEffort && { reasoningEffort: spawnOptions.reasoningEffort }), ...(spawnOptions?.fastMode && { fastMode: spawnOptions.fastMode }), ...(spawnOptions?.diffContext && { diffContext: spawnOptions.diffContext }), + ...(spawnOptions?.repoId && { repoId: spawnOptions.repoId }), + ...(spawnOptions?.repoLabel && { repoLabel: spawnOptions.repoLabel }), + ...(spawnOptions?.repoCwd && { repoCwd: spawnOptions.repoCwd }), }; let proc: ReturnType | null = null; diff --git a/packages/server/review-workspace.ts b/packages/server/review-workspace.ts index 4ad49c82d..42c700944 100644 --- a/packages/server/review-workspace.ts +++ b/packages/server/review-workspace.ts @@ -38,10 +38,13 @@ function prefixRepoPath(label: string, filePath: string): string { } function rewritePatchLine(line: string, label: string): string { - if (line.startsWith("diff --git a/")) { - const match = line.match(/^diff --git a\/(.+) b\/(.+)$/); + // diff --git with optional quotes around paths + if (line.startsWith("diff --git a/") || line.startsWith('diff --git "a/')) { + const match = line.match(/^diff --git (?:"?a\/(.+?)"?|\/?a\/(.+?)) (?:"?b\/(.+?)"?|\/?b\/(.+?))$/); if (!match) return line; - return `diff --git a/${prefixRepoPath(label, match[1])} b/${prefixRepoPath(label, match[2])}`; + const oldPath = match[1] ?? match[2]; + const newPath = match[3] ?? match[4]; + return `diff --git a/${prefixRepoPath(label, oldPath)} b/${prefixRepoPath(label, newPath)}`; } if (line.startsWith("--- ")) { @@ -58,6 +61,18 @@ function rewritePatchLine(line: string, label: string): string { return line; } + // rename/copy headers + if (line.startsWith("rename from ") || line.startsWith("copy from ")) { + const prefix = line.slice(0, line.indexOf(" ") + 1); + const path = line.slice(prefix.length); + return `${prefix}a/${prefixRepoPath(label, path)}`; + } + if (line.startsWith("rename to ") || line.startsWith("copy to ")) { + const prefix = line.slice(0, line.indexOf(" ") + 1); + const path = line.slice(prefix.length); + return `${prefix}b/${prefixRepoPath(label, path)}`; + } + return line; } @@ -73,14 +88,20 @@ export function resolveWorkspaceFilePath( repos: WorkspaceRepoRuntimeState[], prefixedPath: string, ): { repo: WorkspaceRepoRuntimeState; repoRelativePath: string } | null { - validateFilePath(prefixedPath); + const normalizedPath = normalizePath(prefixedPath); + validateFilePath(normalizedPath); - for (const repo of [...repos].sort((a, b) => b.label.length - a.label.length)) { + // Pre-sort once — callers should pass pre-sorted arrays for performance. + const sorted = repos[0]?.__sorted + ? repos + : [...repos].sort((a, b) => b.label.length - a.label.length); + + for (const repo of sorted) { const prefix = `${normalizePath(repo.label)}/`; - if (prefixedPath.startsWith(prefix)) { + if (normalizedPath === normalizePath(repo.label) || normalizedPath.startsWith(prefix)) { return { repo, - repoRelativePath: prefixedPath.slice(prefix.length), + repoRelativePath: normalizedPath.slice(prefix.length), }; } } @@ -160,6 +181,16 @@ function buildUniqueLabel(preferred: string, used = new Set()): string { return next; } +function withTimeout(promise: Promise, ms: number, context: string): Promise { + return Promise.race([ + promise.then((v) => v as T | null), + new Promise((resolve) => setTimeout(() => { + console.error(`[workspace] Timeout: ${context}`); + resolve(null); + }, ms)), + ]); +} + async function discoverGitHubPRCandidate(cwd: string, gitContext: GitContext): Promise { const branch = gitContext.currentBranch; if (!branch || branch === "HEAD") return null; @@ -231,7 +262,8 @@ async function discoverGitHubPRCandidate(cwd: string, gitContext: GitContext): P if (!ref) return null; try { - const pr = await fetchPR(ref); + const pr = await withTimeout(fetchPR(ref), 15000, `fetchPR ${url}`); + if (!pr) return null; return { url, metadata: pr.metadata }; } catch { return null; @@ -281,7 +313,8 @@ async function discoverGitLabPRCandidate(cwd: string, gitContext: GitContext): P if (!ref) return null; try { - const pr = await fetchPR(ref); + const pr = await withTimeout(fetchPR(ref), 15000, `fetchPR ${url}`); + if (!pr) return null; return { url, metadata: pr.metadata }; } catch { return null; @@ -300,10 +333,13 @@ export async function discoverPRCandidates(cwd: string, gitContext: GitContext): export async function buildWorkspaceLocalRepos(root: string): Promise { const repoPaths = discoverWorkspaceRepoPaths(root); const defaultDiffType = resolveDefaultDiffType(loadConfig()); + + // Pre-compute labels sequentially to avoid race conditions in parallel map const usedLabels = new Set(); + const labels = repoPaths.map((cwd) => buildRepoLabel(root, cwd, usedLabels)); const repos = await Promise.all(repoPaths.map(async (cwd, index) => { - const label = buildRepoLabel(root, cwd, usedLabels); + const label = labels[index]; try { const gitContext = await getVcsContext(cwd); const diffType = gitContext.vcsType === "p4" ? "p4-default" : defaultDiffType; @@ -341,7 +377,26 @@ export async function buildWorkspaceLocalRepos(root: string): Promise { + // Pre-compute labels sequentially to avoid race conditions in parallel map const usedLabels = new Set(); + const prefetched = await Promise.all(urls.map(async (url) => { + const ref = parsePRUrl(url); + if (!ref) return null; + try { + const pr = await fetchPR(ref); + const baseLabel = pr.metadata.platform === "github" + ? `${pr.metadata.owner}/${pr.metadata.repo}` + : pr.metadata.projectPath; + return { pr, baseLabel }; + } catch { + return null; + } + })); + + const labels = prefetched.map((p) => + p ? buildUniqueLabel(p.baseLabel, usedLabels) : buildUniqueLabel(`invalid-${prefetched.indexOf(p) + 1}`, usedLabels) + ); + const repos = await Promise.all(urls.map(async (url, index) => { const ref = parsePRUrl(url); if (!ref) { @@ -390,14 +445,17 @@ export async function buildWorkspacePRRepos(urls: string[]): Promise repo.selected); + const trimmedPatches = selected.map((repo) => repo.rawPatch.trim()).filter(Boolean); return { - rawPatch: selected.map((repo) => repo.rawPatch.trim()).filter(Boolean).join("\n"), + rawPatch: trimmedPatches.join("\n\n"), gitRef: selected.map((repo) => repo.gitRef || repo.label).filter(Boolean).join(" | ") || "Workspace review", errors: selected.flatMap((repo) => repo.error ? [`${repo.label}: ${repo.error}`] : []), }; diff --git a/packages/server/review.ts b/packages/server/review.ts index b26e781a7..953464b38 100644 --- a/packages/server/review.ts +++ b/packages/server/review.ts @@ -162,6 +162,20 @@ export async function startReviewServer( currentError = snapshot.errors.length > 0 ? snapshot.errors.join("\n") : undefined; }; + /** Apply workspace repo mutation atomically with rollback on failure. */ + const applyRepoMutation = ( + repo: WorkspaceRepoRuntimeState, + mutator: () => Promise | void, + ): Promise => { + const prev = { ...repo }; + return Promise.resolve() + .then(() => mutator()) + .catch((err) => { + Object.assign(repo, prev); + throw err; + }); + }; + const getActiveRepo = (): WorkspaceRepoRuntimeState | null => { if (!workspaceRepos?.length) return null; return workspaceRepos.find((repo) => repo.id === currentActiveRepoId) @@ -207,8 +221,9 @@ export async function startReviewServer( }, async buildCommand(provider, config) { - const cwd = getActiveRepo()?.cwd ?? options.agentCwd ?? resolveVcsCwd(currentDiffType, gitContext?.cwd) ?? process.cwd(); - const hasAgentLocalAccess = !!options.agentCwd || !!gitContext; + const activeRepo = getActiveRepo(); + const cwd = activeRepo?.cwd ?? options.agentCwd ?? resolveVcsCwd(currentDiffType, gitContext?.cwd) ?? process.cwd(); + const hasAgentLocalAccess = !!options.agentCwd || !!gitContext || !!activeRepo; const userMessageOptions = { defaultBranch: currentBase, hasLocalAccess: hasAgentLocalAccess }; // Snapshot the diff context at launch — stored on the job so @@ -226,6 +241,10 @@ export async function startReviewServer( worktreePath: worktreeParts?.path ?? null, }; + const repoSnapshot = activeRepo + ? { repoId: activeRepo.id, repoLabel: activeRepo.label, repoCwd: activeRepo.cwd } + : undefined; + if (provider === "tour") { const built = await tour.buildCommand({ cwd, @@ -235,7 +254,7 @@ export async function startReviewServer( prMetadata, config, }); - return built ? { ...built, diffContext } : built; + return built ? { ...built, diffContext, ...repoSnapshot } : built; } const userMessage = buildCodexReviewUserMessage(currentPatch, currentDiffType, userMessageOptions, prMetadata); @@ -247,7 +266,7 @@ export async function startReviewServer( const outputPath = generateOutputPath(); const prompt = CODEX_REVIEW_SYSTEM_PROMPT + "\n\n---\n\n" + userMessage; const command = await buildCodexCommand({ cwd, outputPath, prompt, model, reasoningEffort, fastMode }); - return { command, outputPath, prompt, label: "Code Review", model, reasoningEffort, fastMode: fastMode || undefined, diffContext }; + return { command, outputPath, prompt, label: "Code Review", model, reasoningEffort, fastMode: fastMode || undefined, diffContext, ...repoSnapshot }; } if (provider === "claude") { @@ -255,14 +274,23 @@ export async function startReviewServer( const effort = typeof config?.effort === "string" && config.effort ? config.effort : undefined; const prompt = CLAUDE_REVIEW_PROMPT + "\n\n---\n\n" + userMessage; const { command, stdinPrompt } = buildClaudeCommand(prompt, model, effort); - return { command, stdinPrompt, prompt, cwd, label: "Code Review", captureStdout: true, model, effort, diffContext }; + return { command, stdinPrompt, prompt, cwd, label: "Code Review", captureStdout: true, model, effort, diffContext, ...repoSnapshot }; } return null; }, async onJobComplete(job, meta) { - const cwd = resolveAgentCwd(); + const cwd = meta.cwd ?? resolveAgentCwd(); + // Use the repo snapshot captured at launch time to avoid races with UI changes + const repoLabel = job.repoLabel; + const pathTransform = repoLabel + ? (filePath: string) => { + // Avoid double-prefixing if the agent already returned a prefixed path + if (filePath.startsWith(`${repoLabel}/`)) return filePath; + return `${repoLabel}/${filePath}`; + } + : undefined; // --- Codex path --- if (job.provider === "codex" && meta.outputPath) { @@ -279,13 +307,12 @@ export async function startReviewServer( }; if (output.findings.length > 0) { - const activeRepo = getActiveRepo(); const annotations = transformReviewFindings( output.findings, job.source, cwd, "Codex", - activeRepo ? (filePath) => `${activeRepo.label}/${filePath}` : undefined, + pathTransform, ); const result = externalAnnotations.addAnnotations({ annotations }); if ("error" in result) console.error(`[codex-review] addAnnotations error:`, result.error); @@ -309,12 +336,11 @@ export async function startReviewServer( }; if (output.findings.length > 0) { - const activeRepo = getActiveRepo(); const annotations = transformClaudeFindings( output.findings, job.source, cwd, - activeRepo ? (filePath) => `${activeRepo.label}/${filePath}` : undefined, + pathTransform, ); const result = externalAnnotations.addAnnotations({ annotations }); if ("error" in result) console.error(`[claude-review] addAnnotations error:`, result.error); @@ -660,48 +686,54 @@ export async function startReviewServer( const body = (await req.json()) as { repoId: string; selected?: boolean; - source?: "local" | "pr"; + source?: string; prUrl?: string; }; const repo = workspaceRepos.find((candidate) => candidate.id === body.repoId); if (!repo) return Response.json({ error: "Unknown repo" }, { status: 404 }); - if (typeof body.selected === "boolean") repo.selected = body.selected; - if (body.source) repo.source = body.source; + if (body.source && body.source !== "local" && body.source !== "pr") { + return Response.json({ error: "Invalid source. Must be 'local' or 'pr'" }, { status: 400 }); + } + + await applyRepoMutation(repo, async () => { + if (typeof body.selected === "boolean") repo.selected = body.selected; + if (body.source) repo.source = body.source; - if (repo.source === "pr") { - const nextUrl = body.prUrl || repo.prMetadata?.url || repo.discoveredPRs?.[0]?.url; - if (!nextUrl) { - return Response.json({ error: "No PR/MR available for this repo" }, { status: 400 }); - } - const ref = parsePRUrl(nextUrl); - if (!ref) return Response.json({ error: "Invalid PR/MR URL" }, { status: 400 }); - const pr = await fetchPR(ref); - repo.prMetadata = pr.metadata; - repo.rawPatch = prefixPatchPaths(pr.rawPatch, repo.label); - repo.gitRef = pr.metadata.url; - repo.platformUser = await getPRUser(prRefFromMetadata(pr.metadata)); - if (pr.metadata.platform === "github") { - try { - const viewedMap = await fetchPRViewedFiles(prRefFromMetadata(pr.metadata)); - repo.viewedFiles = Object.entries(viewedMap) - .filter(([, isViewed]) => isViewed) - .map(([path]) => `${repo.label}/${path}`); - } catch { - repo.viewedFiles = []; + if (repo.source === "pr") { + const nextUrl = body.prUrl || repo.prMetadata?.url || repo.discoveredPRs?.[0]?.url; + if (!nextUrl) { + throw new Error("No PR/MR available for this repo"); + } + const ref = parsePRUrl(nextUrl); + if (!ref) throw new Error("Invalid PR/MR URL"); + const pr = await fetchPR(ref); + repo.prMetadata = pr.metadata; + repo.rawPatch = prefixPatchPaths(pr.rawPatch, repo.label); + repo.gitRef = pr.metadata.url; + repo.platformUser = await getPRUser(prRefFromMetadata(pr.metadata)); + if (pr.metadata.platform === "github") { + try { + const viewedMap = await fetchPRViewedFiles(prRefFromMetadata(pr.metadata)); + repo.viewedFiles = Object.entries(viewedMap) + .filter(([, isViewed]) => isViewed) + .map(([path]) => `${repo.label}/${path}`); + } catch { + repo.viewedFiles = []; + } } + repo.error = undefined; + } else if (repo.gitContext) { + const nextDiffType = (repo.diffType as DiffType | undefined) || "uncommitted"; + const result = await runVcsDiff(nextDiffType, repo.gitContext.defaultBranch, repo.cwd); + repo.rawPatch = prefixPatchPaths(result.patch, repo.label); + repo.gitRef = result.label; + repo.prMetadata = undefined; + repo.platformUser = null; + repo.viewedFiles = []; + repo.error = result.error; } - repo.error = undefined; - } else if (repo.gitContext) { - const nextDiffType = (repo.diffType as DiffType | undefined) || "uncommitted"; - const result = await runVcsDiff(nextDiffType, repo.gitContext.defaultBranch, repo.cwd); - repo.rawPatch = prefixPatchPaths(result.patch, repo.label); - repo.gitRef = result.label; - repo.prMetadata = undefined; - repo.platformUser = null; - repo.viewedFiles = []; - repo.error = result.error; - } + }); refreshWorkspaceAggregate(); return Response.json({ diff --git a/packages/shared/agent-jobs.ts b/packages/shared/agent-jobs.ts index 84283e83e..723575b8c 100644 --- a/packages/shared/agent-jobs.ts +++ b/packages/shared/agent-jobs.ts @@ -71,6 +71,10 @@ export interface AgentJobInfo { }; /** Diff context at launch time (see AgentJobDiffContext). */ diffContext?: AgentJobDiffContext; + /** Workspace repo snapshot at launch — prevents race with UI selection changes. */ + repoId?: string; + repoLabel?: string; + repoCwd?: string; } export interface AgentCapability { diff --git a/packages/shared/review-workspace.ts b/packages/shared/review-workspace.ts index 34d9e2b50..ffe2322b1 100644 --- a/packages/shared/review-workspace.ts +++ b/packages/shared/review-workspace.ts @@ -1,4 +1,4 @@ -import type { DiffOption, GitContext } from "./review-core"; +import type { DiffType, DiffOption, GitContext } from "./review-core"; import type { PRMetadata } from "./pr-provider"; export type WorkspaceRepoSource = "local" | "pr"; @@ -14,12 +14,12 @@ export interface WorkspaceRepoState { cwd: string; selected: boolean; source: WorkspaceRepoSource; - diffType?: string; + diffType?: DiffType; gitContext?: GitContext; prMetadata?: PRMetadata; discoveredPRs?: WorkspacePRCandidate[]; diffOptions?: DiffOption[]; - platformUser?: string | null; + platformUser: string | null; viewedFiles?: string[]; error?: string; } From 7279be27f1e39e4e85d09c9e8434cac9fcedb0d6 Mon Sep 17 00:00:00 2001 From: Oscar Silva Date: Sat, 9 May 2026 14:53:29 -0300 Subject: [PATCH 3/4] fix: remove duplicate gitRuntime export in vcs.ts --- packages/server/vcs.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/server/vcs.ts b/packages/server/vcs.ts index 7b8db1e87..e5b16d14a 100644 --- a/packages/server/vcs.ts +++ b/packages/server/vcs.ts @@ -78,4 +78,4 @@ export { validateFilePath, } from "@plannotator/shared/vcs-core"; -export { runtime as gitRuntime } from "./git"; + From f5224984730833d33ae391ccf6a4f9f5e0bcc129 Mon Sep 17 00:00:00 2001 From: Oscar Silva Date: Sat, 9 May 2026 15:04:12 -0300 Subject: [PATCH 4/4] fix: resolve remaining merge conflict in local review mode, remove stale detectManagedVcs import --- apps/hook/server/index.ts | 31 +++++++++++-------------------- 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/apps/hook/server/index.ts b/apps/hook/server/index.ts index 88a1ecb13..b0d8d588a 100644 --- a/apps/hook/server/index.ts +++ b/apps/hook/server/index.ts @@ -63,7 +63,7 @@ import { startAnnotateServer, handleAnnotateServerReady, } from "@plannotator/server/annotate"; -import { type DiffType, getVcsContext, runVcsDiff, gitRuntime, detectManagedVcs, prepareLocalReviewDiff } from "@plannotator/server/vcs"; +import { type DiffType, gitRuntime, prepareLocalReviewDiff } from "@plannotator/server/vcs"; import { loadConfig, resolveDefaultDiffType, resolveUseJina } from "@plannotator/shared/config"; import { parseReviewArgs } from "@plannotator/shared/review-args"; import { stripAtPrefix, resolveAtReference } from "@plannotator/shared/at-reference"; @@ -504,24 +504,6 @@ if (args[0] === "sessions") { } } else { // --- Local Review Mode --- -<<<<<<< HEAD - const managedVcs = await detectManagedVcs(process.cwd()); - if (managedVcs) { - gitContext = await getVcsContext(); - initialDiffType = gitContext.vcsType === "p4" ? "p4-default" : resolveDefaultDiffType(loadConfig()); - const diffResult = await runVcsDiff(initialDiffType, gitContext.defaultBranch); - rawPatch = diffResult.patch; - gitRef = diffResult.label; - diffError = diffResult.error; - } else { - workspaceRepos = await buildWorkspaceLocalRepos(process.cwd()); - if (workspaceRepos.length === 0) { - throw new Error("Not in a git repo and no nested repositories were found."); - } - rawPatch = ""; - gitRef = "Workspace review"; - } -======= const config = loadConfig(); const diffResult = await prepareLocalReviewDiff({ vcsType: reviewArgs.vcsType, @@ -533,7 +515,16 @@ if (args[0] === "sessions") { rawPatch = diffResult.rawPatch; gitRef = diffResult.gitRef; diffError = diffResult.error; ->>>>>>> origin/main + + // Fallback: if no VCS detected, try workspace review (multi-repo / poly-repo setups) + if (!gitContext) { + workspaceRepos = await buildWorkspaceLocalRepos(process.cwd()); + if (workspaceRepos.length === 0) { + throw new Error("Not in a git repo and no nested repositories were found."); + } + rawPatch = ""; + gitRef = "Workspace review"; + } } const reviewProject = (await detectProjectName()) ?? "_unknown";