@golba98/codexa 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +320 -0
- package/bin/codexa.js +445 -0
- package/package.json +45 -0
- package/scripts/audit-codexa-capabilities.mjs +466 -0
- package/scripts/smoke-terminal-bench.mjs +35 -0
- package/src/app.tsx +4561 -0
- package/src/appRenderStability.test.ts +131 -0
- package/src/commands/handler.test.ts +643 -0
- package/src/commands/handler.ts +875 -0
- package/src/config/launchArgs.test.ts +158 -0
- package/src/config/launchArgs.ts +186 -0
- package/src/config/layeredConfig.test.ts +143 -0
- package/src/config/layeredConfig.ts +836 -0
- package/src/config/persistence.test.ts +110 -0
- package/src/config/persistence.ts +311 -0
- package/src/config/runtimeConfig.test.ts +218 -0
- package/src/config/runtimeConfig.ts +554 -0
- package/src/config/settings.test.ts +155 -0
- package/src/config/settings.ts +401 -0
- package/src/config/toml-serialize.ts +98 -0
- package/src/config/trustStore.test.ts +29 -0
- package/src/config/trustStore.ts +68 -0
- package/src/core/attachments.test.ts +155 -0
- package/src/core/attachments.ts +71 -0
- package/src/core/auth/codexAuth.test.ts +68 -0
- package/src/core/auth/codexAuth.ts +359 -0
- package/src/core/cleanupFastFail.test.ts +76 -0
- package/src/core/cleanupFastFail.ts +67 -0
- package/src/core/clipboard.ts +24 -0
- package/src/core/codex.ts +124 -0
- package/src/core/codexExecArgs.test.ts +195 -0
- package/src/core/codexExecArgs.ts +152 -0
- package/src/core/codexLaunch.test.ts +205 -0
- package/src/core/codexLaunch.ts +162 -0
- package/src/core/codexPrompt.test.ts +252 -0
- package/src/core/codexPrompt.ts +428 -0
- package/src/core/executables/claudeExecutable.ts +63 -0
- package/src/core/executables/codexExecutable.test.ts +212 -0
- package/src/core/executables/codexExecutable.ts +159 -0
- package/src/core/executables/executableResolver.test.ts +129 -0
- package/src/core/executables/executableResolver.ts +138 -0
- package/src/core/executables/geminiExecutable.test.ts +116 -0
- package/src/core/executables/geminiExecutable.ts +78 -0
- package/src/core/executables/pathSanityScan.test.ts +47 -0
- package/src/core/githubDiagnostics.test.ts +92 -0
- package/src/core/githubDiagnostics.ts +222 -0
- package/src/core/hollowResponseFormat.test.ts +58 -0
- package/src/core/hollowResponseFormat.ts +39 -0
- package/src/core/inputDebug.ts +51 -0
- package/src/core/launchContext.test.ts +157 -0
- package/src/core/launchContext.ts +266 -0
- package/src/core/models/codexCapabilities.test.ts +45 -0
- package/src/core/models/codexCapabilities.ts +95 -0
- package/src/core/models/codexModelCapabilities.test.ts +246 -0
- package/src/core/models/codexModelCapabilities.ts +571 -0
- package/src/core/models/modelSpecs.test.ts +283 -0
- package/src/core/models/modelSpecs.ts +300 -0
- package/src/core/perf/profiler.ts +125 -0
- package/src/core/perf/renderDebug.test.ts +230 -0
- package/src/core/perf/renderDebug.ts +373 -0
- package/src/core/planStorage.test.ts +143 -0
- package/src/core/planStorage.ts +141 -0
- package/src/core/process/CommandRunner.test.ts +105 -0
- package/src/core/process/CommandRunner.ts +269 -0
- package/src/core/process/processValidation.ts +101 -0
- package/src/core/projectInstructions.test.ts +50 -0
- package/src/core/projectInstructions.ts +54 -0
- package/src/core/providerLauncher/launcher.test.ts +238 -0
- package/src/core/providerLauncher/launcher.ts +203 -0
- package/src/core/providerLauncher/registry.test.ts +324 -0
- package/src/core/providerLauncher/registry.ts +253 -0
- package/src/core/providerLauncher/types.ts +84 -0
- package/src/core/providerLauncher/workspaceConfig.test.ts +638 -0
- package/src/core/providerLauncher/workspaceConfig.ts +407 -0
- package/src/core/providerRuntime/anthropic.test.ts +1120 -0
- package/src/core/providerRuntime/anthropic.ts +576 -0
- package/src/core/providerRuntime/capabilityProfile.test.ts +311 -0
- package/src/core/providerRuntime/capabilityProfile.ts +288 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +446 -0
- package/src/core/providerRuntime/contextMetadata.test.ts +468 -0
- package/src/core/providerRuntime/contextMetadata.ts +409 -0
- package/src/core/providerRuntime/gemini.test.ts +437 -0
- package/src/core/providerRuntime/gemini.ts +784 -0
- package/src/core/providerRuntime/lmstudio.test.ts +168 -0
- package/src/core/providerRuntime/lmstudio.ts +118 -0
- package/src/core/providerRuntime/local.test.ts +787 -0
- package/src/core/providerRuntime/local.ts +754 -0
- package/src/core/providerRuntime/models.ts +150 -0
- package/src/core/providerRuntime/reasoning.ts +17 -0
- package/src/core/providerRuntime/registry.test.ts +233 -0
- package/src/core/providerRuntime/registry.ts +203 -0
- package/src/core/providerRuntime/types.ts +103 -0
- package/src/core/providers/codexJsonStream.test.ts +148 -0
- package/src/core/providers/codexJsonStream.ts +305 -0
- package/src/core/providers/codexSubprocess.test.ts +68 -0
- package/src/core/providers/codexSubprocess.ts +372 -0
- package/src/core/providers/codexTranscript.test.ts +284 -0
- package/src/core/providers/codexTranscript.ts +695 -0
- package/src/core/providers/openaiNative.ts +13 -0
- package/src/core/providers/registry.ts +21 -0
- package/src/core/providers/types.ts +59 -0
- package/src/core/terminal/terminalCapabilities.test.ts +93 -0
- package/src/core/terminal/terminalCapabilities.ts +100 -0
- package/src/core/terminal/terminalControl.test.ts +75 -0
- package/src/core/terminal/terminalControl.ts +147 -0
- package/src/core/terminal/terminalSanitize.test.ts +22 -0
- package/src/core/terminal/terminalSanitize.ts +147 -0
- package/src/core/terminal/terminalSelection.test.ts +42 -0
- package/src/core/terminal/terminalSelection.ts +66 -0
- package/src/core/terminal/terminalTitle.test.ts +328 -0
- package/src/core/terminal/terminalTitle.ts +483 -0
- package/src/core/workspaceActivity.test.ts +163 -0
- package/src/core/workspaceActivity.ts +380 -0
- package/src/core/workspaceGuard.test.ts +151 -0
- package/src/core/workspaceGuard.ts +288 -0
- package/src/core/workspaceRoot.test.ts +23 -0
- package/src/core/workspaceRoot.ts +47 -0
- package/src/exec.test.ts +13 -0
- package/src/exec.ts +72 -0
- package/src/headless/execArgs.test.ts +147 -0
- package/src/headless/execArgs.ts +294 -0
- package/src/headless/execRunner.test.ts +434 -0
- package/src/headless/execRunner.ts +304 -0
- package/src/index.test.tsx +618 -0
- package/src/index.tsx +296 -0
- package/src/session/appSession.test.ts +897 -0
- package/src/session/appSession.ts +761 -0
- package/src/session/chatLifecycle.test.ts +64 -0
- package/src/session/chatLifecycle.ts +951 -0
- package/src/session/liveRenderScheduler.test.ts +201 -0
- package/src/session/liveRenderScheduler.ts +214 -0
- package/src/session/planFlow.test.ts +103 -0
- package/src/session/planFlow.ts +149 -0
- package/src/session/planTranscript.test.ts +65 -0
- package/src/session/planTranscript.ts +15 -0
- package/src/session/promptRunSchedule.test.ts +36 -0
- package/src/session/promptRunSchedule.ts +26 -0
- package/src/session/types.ts +228 -0
- package/src/test/runtimeTestUtils.ts +14 -0
- package/src/types/react-dom.d.ts +3 -0
- package/src/ui/ActionRequiredBlock.tsx +38 -0
- package/src/ui/ActivityBars.tsx +68 -0
- package/src/ui/ActivityIndicator.test.tsx +58 -0
- package/src/ui/ActivityIndicator.tsx +58 -0
- package/src/ui/AgentBlock.test.ts +6 -0
- package/src/ui/AgentBlock.tsx +130 -0
- package/src/ui/AnimatedStatusText.test.ts +16 -0
- package/src/ui/AnimatedStatusText.tsx +69 -0
- package/src/ui/AppShell.test.tsx +1739 -0
- package/src/ui/AppShell.tsx +698 -0
- package/src/ui/AttachmentImportPanel.test.tsx +204 -0
- package/src/ui/AttachmentImportPanel.tsx +98 -0
- package/src/ui/AuthPanel.tsx +113 -0
- package/src/ui/BackendPicker.tsx +28 -0
- package/src/ui/BottomComposer.test.ts +674 -0
- package/src/ui/BottomComposer.tsx +1028 -0
- package/src/ui/CodexLogo.tsx +55 -0
- package/src/ui/DashCard.tsx +82 -0
- package/src/ui/Markdown.test.ts +157 -0
- package/src/ui/Markdown.tsx +310 -0
- package/src/ui/ModePicker.tsx +27 -0
- package/src/ui/ModelPicker.tsx +31 -0
- package/src/ui/ModelPickerProviderScope.test.tsx +411 -0
- package/src/ui/ModelPickerScreen.test.tsx +99 -0
- package/src/ui/ModelPickerScreen.tsx +416 -0
- package/src/ui/ModelPickerState.test.tsx +151 -0
- package/src/ui/ModelReasoningPicker.test.tsx +447 -0
- package/src/ui/ModelReasoningPicker.tsx +458 -0
- package/src/ui/Panel.tsx +51 -0
- package/src/ui/PermissionsPanel.tsx +78 -0
- package/src/ui/PlanActionPicker.tsx +119 -0
- package/src/ui/PlanReviewPanel.test.tsx +267 -0
- package/src/ui/PlanReviewPanel.tsx +212 -0
- package/src/ui/PromptCardBorder.test.tsx +161 -0
- package/src/ui/ProviderPicker.test.tsx +289 -0
- package/src/ui/ProviderPicker.tsx +321 -0
- package/src/ui/ProviderShortcut.test.tsx +143 -0
- package/src/ui/ReasoningPicker.tsx +46 -0
- package/src/ui/RunFooter.tsx +65 -0
- package/src/ui/SelectionPanel.tsx +67 -0
- package/src/ui/SettingsPanel.test.tsx +233 -0
- package/src/ui/SettingsPanel.tsx +156 -0
- package/src/ui/Spinner.tsx +25 -0
- package/src/ui/StaticIntroItem.tsx +54 -0
- package/src/ui/StaticTranscriptItem.tsx +56 -0
- package/src/ui/TextEntryPanel.tsx +139 -0
- package/src/ui/ThemePicker.tsx +31 -0
- package/src/ui/ThinkingBlock.tsx +100 -0
- package/src/ui/Timeline.test.ts +2067 -0
- package/src/ui/Timeline.tsx +1472 -0
- package/src/ui/TimelineNavigation.test.tsx +201 -0
- package/src/ui/TopHeader.test.tsx +239 -0
- package/src/ui/TopHeader.tsx +257 -0
- package/src/ui/TurnGroup.test.tsx +365 -0
- package/src/ui/TurnGroup.tsx +657 -0
- package/src/ui/busyStatusAnimation.test.ts +30 -0
- package/src/ui/busyStatusAnimation.ts +11 -0
- package/src/ui/commandNormalize.test.ts +142 -0
- package/src/ui/commandNormalize.ts +66 -0
- package/src/ui/diffRenderer.test.ts +102 -0
- package/src/ui/diffRenderer.ts +116 -0
- package/src/ui/focus.ts +61 -0
- package/src/ui/focusFlow.test.tsx +1098 -0
- package/src/ui/inputBuffer.test.ts +151 -0
- package/src/ui/inputBuffer.ts +203 -0
- package/src/ui/layout.test.ts +145 -0
- package/src/ui/layout.ts +287 -0
- package/src/ui/modeDisplay.test.ts +42 -0
- package/src/ui/modeDisplay.ts +52 -0
- package/src/ui/outputPipeline.ts +64 -0
- package/src/ui/progressEntries.ts +156 -0
- package/src/ui/runActivityView.test.ts +89 -0
- package/src/ui/runActivityView.ts +37 -0
- package/src/ui/runLifecycleView.test.tsx +237 -0
- package/src/ui/slashCommands.ts +41 -0
- package/src/ui/statusRenderIsolation.test.tsx +654 -0
- package/src/ui/terminalAnswerFormat.test.ts +19 -0
- package/src/ui/terminalAnswerFormat.ts +128 -0
- package/src/ui/textLayout.test.ts +18 -0
- package/src/ui/textLayout.ts +338 -0
- package/src/ui/theme.tsx +395 -0
- package/src/ui/themeFlow.test.ts +53 -0
- package/src/ui/themeFlow.ts +41 -0
- package/src/ui/timelineMeasure.ts +3088 -0
- package/src/ui/timelineMeasureCache.test.ts +986 -0
- package/src/ui/useThrottledValue.ts +31 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, statSync } from "fs";
|
|
2
|
+
import { join, relative, sep } from "path";
|
|
3
|
+
|
|
4
|
+
export type RunFileOperation = "created" | "modified" | "deleted";
|
|
5
|
+
export type RunDiffLineKind = "added" | "removed";
|
|
6
|
+
|
|
7
|
+
export interface RunDiffLine {
|
|
8
|
+
kind: RunDiffLineKind;
|
|
9
|
+
text: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface RunFileActivity {
|
|
13
|
+
path: string;
|
|
14
|
+
operation: RunFileOperation;
|
|
15
|
+
detectedAt: number;
|
|
16
|
+
addedLines?: number;
|
|
17
|
+
removedLines?: number;
|
|
18
|
+
diffLines?: RunDiffLine[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface RunActivitySummary {
|
|
22
|
+
created: number;
|
|
23
|
+
modified: number;
|
|
24
|
+
deleted: number;
|
|
25
|
+
totalActivityCount: number;
|
|
26
|
+
recent: RunFileActivity[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface WorkspaceActivityTracker {
|
|
30
|
+
stop: () => void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface WorkspaceActivityTrackerOptions {
|
|
34
|
+
rootDir: string;
|
|
35
|
+
onActivity: (activity: RunFileActivity[]) => void;
|
|
36
|
+
pollIntervalMs?: number;
|
|
37
|
+
initialSnapshot?: Map<string, WorkspaceFileSnapshot>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface WorkspaceFileSnapshot {
|
|
41
|
+
path: string;
|
|
42
|
+
mtimeMs: number;
|
|
43
|
+
size: number;
|
|
44
|
+
content?: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const DEFAULT_POLL_INTERVAL_MS = 400;
|
|
48
|
+
const MAX_TRACKED_TEXT_BYTES = 128 * 1024;
|
|
49
|
+
const MAX_DIFFABLE_LINE_COUNT = 240;
|
|
50
|
+
const MAX_DIFF_PREVIEW_LINES = 6;
|
|
51
|
+
const MAX_SUMMARY_ACTIVITY_ITEMS = 6;
|
|
52
|
+
|
|
53
|
+
const IGNORED_DIRECTORY_NAMES = new Set([
|
|
54
|
+
".cache",
|
|
55
|
+
".git",
|
|
56
|
+
".next",
|
|
57
|
+
".parcel-cache",
|
|
58
|
+
".turbo",
|
|
59
|
+
".vercel",
|
|
60
|
+
".yarn",
|
|
61
|
+
"build",
|
|
62
|
+
"coverage",
|
|
63
|
+
"dist",
|
|
64
|
+
"node_modules",
|
|
65
|
+
"out",
|
|
66
|
+
"temp",
|
|
67
|
+
"tmp",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
const BINARY_EXTENSIONS = new Set([
|
|
71
|
+
".bmp",
|
|
72
|
+
".class",
|
|
73
|
+
".dll",
|
|
74
|
+
".dylib",
|
|
75
|
+
".exe",
|
|
76
|
+
".gif",
|
|
77
|
+
".ico",
|
|
78
|
+
".jar",
|
|
79
|
+
".jpeg",
|
|
80
|
+
".jpg",
|
|
81
|
+
".lockb",
|
|
82
|
+
".mp3",
|
|
83
|
+
".mp4",
|
|
84
|
+
".o",
|
|
85
|
+
".otf",
|
|
86
|
+
".pdf",
|
|
87
|
+
".png",
|
|
88
|
+
".so",
|
|
89
|
+
".ttf",
|
|
90
|
+
".wasm",
|
|
91
|
+
".webp",
|
|
92
|
+
".woff",
|
|
93
|
+
".woff2",
|
|
94
|
+
".zip",
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
function normalizePath(path: string): string {
|
|
98
|
+
return path.split(sep).join("/");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function shouldIgnoreDirectory(name: string): boolean {
|
|
102
|
+
return IGNORED_DIRECTORY_NAMES.has(name);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function isBinaryBuffer(buffer: Buffer): boolean {
|
|
106
|
+
return buffer.includes(0);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function shouldTrackTextContent(path: string, size: number): boolean {
|
|
110
|
+
const lower = path.toLowerCase();
|
|
111
|
+
const dotIndex = lower.lastIndexOf(".");
|
|
112
|
+
const ext = dotIndex >= 0 ? lower.slice(dotIndex) : "";
|
|
113
|
+
if (size > MAX_TRACKED_TEXT_BYTES) return false;
|
|
114
|
+
if (BINARY_EXTENSIONS.has(ext)) return false;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function readTrackedFileContent(path: string, size: number): string | undefined {
|
|
119
|
+
if (!shouldTrackTextContent(path, size)) return undefined;
|
|
120
|
+
|
|
121
|
+
const buffer = readFileSync(path);
|
|
122
|
+
if (isBinaryBuffer(buffer)) return undefined;
|
|
123
|
+
return buffer.toString("utf8").replace(/\r\n/g, "\n").replace(/\r/g, "\n");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function splitTextLines(text: string): string[] {
|
|
127
|
+
if (!text) return [];
|
|
128
|
+
const lines = text.split("\n");
|
|
129
|
+
if (lines.at(-1) === "") {
|
|
130
|
+
lines.pop();
|
|
131
|
+
}
|
|
132
|
+
return lines;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
interface DiffOperation {
|
|
136
|
+
kind: "equal" | RunDiffLineKind;
|
|
137
|
+
text: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// LCS diff has O(n*m) complexity. Skip diffing for large files to avoid
|
|
141
|
+
// blocking the poll loop — callers treat null as "too large to diff".
|
|
142
|
+
function buildLineDiff(before: string[], after: string[]): DiffOperation[] | null {
|
|
143
|
+
if (before.length + after.length > MAX_DIFFABLE_LINE_COUNT) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const rows = before.length + 1;
|
|
148
|
+
const cols = after.length + 1;
|
|
149
|
+
const matrix = Array.from({ length: rows }, () => Array<number>(cols).fill(0));
|
|
150
|
+
|
|
151
|
+
for (let row = before.length - 1; row >= 0; row -= 1) {
|
|
152
|
+
for (let col = after.length - 1; col >= 0; col -= 1) {
|
|
153
|
+
if (before[row] === after[col]) {
|
|
154
|
+
matrix[row]![col] = 1 + matrix[row + 1]![col + 1]!;
|
|
155
|
+
} else {
|
|
156
|
+
matrix[row]![col] = Math.max(matrix[row + 1]![col]!, matrix[row]![col + 1]!);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const operations: DiffOperation[] = [];
|
|
162
|
+
let row = 0;
|
|
163
|
+
let col = 0;
|
|
164
|
+
while (row < before.length && col < after.length) {
|
|
165
|
+
if (before[row] === after[col]) {
|
|
166
|
+
operations.push({ kind: "equal", text: before[row]! });
|
|
167
|
+
row += 1;
|
|
168
|
+
col += 1;
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (matrix[row + 1]![col]! >= matrix[row]![col + 1]!) {
|
|
173
|
+
operations.push({ kind: "removed", text: before[row]! });
|
|
174
|
+
row += 1;
|
|
175
|
+
} else {
|
|
176
|
+
operations.push({ kind: "added", text: after[col]! });
|
|
177
|
+
col += 1;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
while (row < before.length) {
|
|
182
|
+
operations.push({ kind: "removed", text: before[row]! });
|
|
183
|
+
row += 1;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
while (col < after.length) {
|
|
187
|
+
operations.push({ kind: "added", text: after[col]! });
|
|
188
|
+
col += 1;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return operations;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function createTextDiffExcerpt(
|
|
195
|
+
beforeContent: string,
|
|
196
|
+
afterContent: string,
|
|
197
|
+
): Pick<RunFileActivity, "addedLines" | "removedLines" | "diffLines"> | null {
|
|
198
|
+
const operations = buildLineDiff(splitTextLines(beforeContent), splitTextLines(afterContent));
|
|
199
|
+
if (!operations) {
|
|
200
|
+
return {
|
|
201
|
+
addedLines: afterContent ? splitTextLines(afterContent).length : 0,
|
|
202
|
+
removedLines: beforeContent ? splitTextLines(beforeContent).length : 0,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const diffOperations = operations.filter(
|
|
207
|
+
(operation) => operation.kind === "added" || operation.kind === "removed",
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
const diffLines: RunDiffLine[] = diffOperations
|
|
211
|
+
.slice(0, MAX_DIFF_PREVIEW_LINES)
|
|
212
|
+
.map((operation) => ({
|
|
213
|
+
kind: operation.kind as RunDiffLineKind,
|
|
214
|
+
text: operation.text,
|
|
215
|
+
}));
|
|
216
|
+
|
|
217
|
+
const addedLines = operations.filter((operation) => operation.kind === "added").length;
|
|
218
|
+
const removedLines = operations.filter((operation) => operation.kind === "removed").length;
|
|
219
|
+
|
|
220
|
+
if (addedLines === 0 && removedLines === 0) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
addedLines,
|
|
226
|
+
removedLines,
|
|
227
|
+
diffLines,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function captureWorkspaceSnapshot(rootDir: string): Map<string, WorkspaceFileSnapshot> {
|
|
232
|
+
const snapshot = new Map<string, WorkspaceFileSnapshot>();
|
|
233
|
+
|
|
234
|
+
function walk(dir: string): void {
|
|
235
|
+
let entries;
|
|
236
|
+
try {
|
|
237
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
238
|
+
} catch {
|
|
239
|
+
// Skip directories we can't read (EPERM, locked temp dirs, etc.)
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const entry of entries) {
|
|
244
|
+
if (entry.isDirectory()) {
|
|
245
|
+
if (shouldIgnoreDirectory(entry.name)) continue;
|
|
246
|
+
walk(join(dir, entry.name));
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (!entry.isFile()) continue;
|
|
251
|
+
|
|
252
|
+
const fullPath = join(dir, entry.name);
|
|
253
|
+
try {
|
|
254
|
+
const stats = statSync(fullPath);
|
|
255
|
+
const path = normalizePath(relative(rootDir, fullPath));
|
|
256
|
+
snapshot.set(path, {
|
|
257
|
+
path,
|
|
258
|
+
mtimeMs: stats.mtimeMs,
|
|
259
|
+
size: stats.size,
|
|
260
|
+
content: readTrackedFileContent(fullPath, stats.size),
|
|
261
|
+
});
|
|
262
|
+
} catch {
|
|
263
|
+
// Skip files we can't stat (deleted between readdir and stat, etc.)
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
walk(rootDir);
|
|
269
|
+
return snapshot;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function diffWorkspaceSnapshots(
|
|
273
|
+
previous: Map<string, WorkspaceFileSnapshot>,
|
|
274
|
+
next: Map<string, WorkspaceFileSnapshot>,
|
|
275
|
+
detectedAt = Date.now(),
|
|
276
|
+
): RunFileActivity[] {
|
|
277
|
+
const activity: RunFileActivity[] = [];
|
|
278
|
+
const allPaths = new Set([...previous.keys(), ...next.keys()]);
|
|
279
|
+
|
|
280
|
+
for (const path of [...allPaths].sort()) {
|
|
281
|
+
const before = previous.get(path);
|
|
282
|
+
const after = next.get(path);
|
|
283
|
+
|
|
284
|
+
if (!before && after) {
|
|
285
|
+
const diff = createTextDiffExcerpt("", after.content ?? "");
|
|
286
|
+
activity.push({
|
|
287
|
+
path,
|
|
288
|
+
operation: "created",
|
|
289
|
+
detectedAt,
|
|
290
|
+
addedLines: diff?.addedLines,
|
|
291
|
+
removedLines: diff?.removedLines,
|
|
292
|
+
diffLines: diff?.diffLines,
|
|
293
|
+
});
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (before && !after) {
|
|
298
|
+
const diff = createTextDiffExcerpt(before.content ?? "", "");
|
|
299
|
+
activity.push({
|
|
300
|
+
path,
|
|
301
|
+
operation: "deleted",
|
|
302
|
+
detectedAt,
|
|
303
|
+
addedLines: diff?.addedLines,
|
|
304
|
+
removedLines: diff?.removedLines,
|
|
305
|
+
diffLines: diff?.diffLines,
|
|
306
|
+
});
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (!before || !after) continue;
|
|
311
|
+
|
|
312
|
+
const metadataChanged = before.mtimeMs !== after.mtimeMs || before.size !== after.size;
|
|
313
|
+
const contentChanged = before.content !== after.content;
|
|
314
|
+
if (!metadataChanged && !contentChanged) continue;
|
|
315
|
+
|
|
316
|
+
const diff = before.content !== undefined && after.content !== undefined
|
|
317
|
+
? createTextDiffExcerpt(before.content, after.content)
|
|
318
|
+
: undefined;
|
|
319
|
+
|
|
320
|
+
activity.push({
|
|
321
|
+
path,
|
|
322
|
+
operation: "modified",
|
|
323
|
+
detectedAt,
|
|
324
|
+
addedLines: diff?.addedLines,
|
|
325
|
+
removedLines: diff?.removedLines,
|
|
326
|
+
diffLines: diff?.diffLines,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return activity;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function summarizeRunActivity(activity: RunFileActivity[]): RunActivitySummary | undefined {
|
|
334
|
+
if (activity.length === 0) return undefined;
|
|
335
|
+
|
|
336
|
+
let created = 0;
|
|
337
|
+
let modified = 0;
|
|
338
|
+
let deleted = 0;
|
|
339
|
+
for (const item of activity) {
|
|
340
|
+
if (item.operation === "created") created += 1;
|
|
341
|
+
if (item.operation === "modified") modified += 1;
|
|
342
|
+
if (item.operation === "deleted") deleted += 1;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return {
|
|
346
|
+
created,
|
|
347
|
+
modified,
|
|
348
|
+
deleted,
|
|
349
|
+
totalActivityCount: activity.length,
|
|
350
|
+
recent: activity.slice(-MAX_SUMMARY_ACTIVITY_ITEMS),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export function createWorkspaceActivityTracker(
|
|
355
|
+
options: WorkspaceActivityTrackerOptions,
|
|
356
|
+
): WorkspaceActivityTracker {
|
|
357
|
+
let stopped = false;
|
|
358
|
+
let previous = options.initialSnapshot ?? captureWorkspaceSnapshot(options.rootDir);
|
|
359
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
360
|
+
|
|
361
|
+
const poll = () => {
|
|
362
|
+
if (stopped) return;
|
|
363
|
+
const next = captureWorkspaceSnapshot(options.rootDir);
|
|
364
|
+
const activity = diffWorkspaceSnapshots(previous, next);
|
|
365
|
+
previous = next;
|
|
366
|
+
if (activity.length > 0) {
|
|
367
|
+
options.onActivity(activity);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const timer = setInterval(poll, pollIntervalMs);
|
|
372
|
+
timer.unref?.();
|
|
373
|
+
|
|
374
|
+
return {
|
|
375
|
+
stop() {
|
|
376
|
+
stopped = true;
|
|
377
|
+
clearInterval(timer);
|
|
378
|
+
},
|
|
379
|
+
};
|
|
380
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
containsDirectoryNavigationCommand,
|
|
5
|
+
findOutsideWorkspacePaths,
|
|
6
|
+
getPromptWorkspaceGuardMessage,
|
|
7
|
+
getShellWorkspaceGuardMessage,
|
|
8
|
+
isPathInsideAllowedRoots,
|
|
9
|
+
isPathInsideWorkspace,
|
|
10
|
+
resolveWorkspacePath,
|
|
11
|
+
} from "./workspaceGuard.js";
|
|
12
|
+
import { normalizeWorkspaceRoot } from "./workspaceRoot.js";
|
|
13
|
+
|
|
14
|
+
const workspaceRoot = normalizeWorkspaceRoot("C:/Users/Example/OneDrive/Desktop/3-Python/Programs/2-Personal/20-Tester");
|
|
15
|
+
|
|
16
|
+
test("allows absolute paths inside the locked workspace", () => {
|
|
17
|
+
const violations = findOutsideWorkspacePaths(
|
|
18
|
+
"Please update C:\\Users\\Example\\OneDrive\\Desktop\\3-Python\\Programs\\2-Personal\\20-Tester\\src\\main.py",
|
|
19
|
+
workspaceRoot,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
assert.deepEqual(violations, []);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test("blocks absolute paths outside the locked workspace", () => {
|
|
26
|
+
const violations = findOutsideWorkspacePaths(
|
|
27
|
+
"Please edit C:\\Users\\Example\\Desktop\\Other\\notes.txt",
|
|
28
|
+
workspaceRoot,
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
assert.equal(violations.length, 1);
|
|
32
|
+
assert.equal(
|
|
33
|
+
violations[0]?.normalizedPath,
|
|
34
|
+
"C:\\Users\\Example\\Desktop\\Other\\notes.txt",
|
|
35
|
+
);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("parses quoted windows paths with spaces", () => {
|
|
39
|
+
const violations = findOutsideWorkspacePaths(
|
|
40
|
+
"Use \"C:\\Users\\Example\\Desktop\\Other Folder\\notes file.txt\" instead",
|
|
41
|
+
workspaceRoot,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
assert.equal(violations.length, 1);
|
|
45
|
+
assert.equal(
|
|
46
|
+
violations[0]?.normalizedPath,
|
|
47
|
+
"C:\\Users\\Example\\Desktop\\Other Folder\\notes file.txt",
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("resolves relative paths inside the workspace", () => {
|
|
52
|
+
assert.equal(
|
|
53
|
+
resolveWorkspacePath("src\\main.py", workspaceRoot),
|
|
54
|
+
"C:\\Users\\Example\\OneDrive\\Desktop\\3-Python\\Programs\\2-Personal\\20-Tester\\src\\main.py",
|
|
55
|
+
);
|
|
56
|
+
assert.equal(isPathInsideWorkspace("src\\main.py", workspaceRoot), true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("blocks explicit relative paths that escape the workspace", () => {
|
|
60
|
+
const violations = findOutsideWorkspacePaths("Edit ..\\outside.txt", workspaceRoot);
|
|
61
|
+
|
|
62
|
+
assert.equal(violations.length, 1);
|
|
63
|
+
assert.equal(
|
|
64
|
+
violations[0]?.normalizedPath,
|
|
65
|
+
"C:\\Users\\Example\\OneDrive\\Desktop\\3-Python\\Programs\\2-Personal\\outside.txt",
|
|
66
|
+
);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("treats same-path drive letter case differences as inside the workspace", () => {
|
|
70
|
+
assert.equal(
|
|
71
|
+
isPathInsideWorkspace(
|
|
72
|
+
"c:\\users\\Example\\onedrive\\desktop\\3-python\\programs\\2-personal\\20-tester\\src\\main.py",
|
|
73
|
+
workspaceRoot,
|
|
74
|
+
),
|
|
75
|
+
true,
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("allows configured writable roots outside the locked workspace", () => {
|
|
80
|
+
const extraRoot = "C:\\Users\\Example\\Desktop\\Allowed Root";
|
|
81
|
+
|
|
82
|
+
assert.equal(
|
|
83
|
+
isPathInsideAllowedRoots("C:\\Users\\Example\\Desktop\\Allowed Root\\notes.txt", workspaceRoot, [extraRoot]),
|
|
84
|
+
true,
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const promptMessage = getPromptWorkspaceGuardMessage(
|
|
88
|
+
"Edit \"C:\\Users\\Example\\Desktop\\Allowed Root\\notes.txt\"",
|
|
89
|
+
workspaceRoot,
|
|
90
|
+
[extraRoot],
|
|
91
|
+
);
|
|
92
|
+
assert.equal(promptMessage, null);
|
|
93
|
+
|
|
94
|
+
const shellMessage = getShellWorkspaceGuardMessage(
|
|
95
|
+
"type \"C:\\Users\\Example\\Desktop\\Allowed Root\\notes.txt\"",
|
|
96
|
+
workspaceRoot,
|
|
97
|
+
[extraRoot],
|
|
98
|
+
);
|
|
99
|
+
assert.equal(shellMessage, null);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("still blocks paths outside the workspace and configured writable roots", () => {
|
|
103
|
+
const extraRoot = "C:\\Users\\Example\\Desktop\\Allowed Root";
|
|
104
|
+
const message = getPromptWorkspaceGuardMessage(
|
|
105
|
+
"Edit C:\\Users\\Example\\Desktop\\Other\\notes.txt",
|
|
106
|
+
workspaceRoot,
|
|
107
|
+
[extraRoot],
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
assert(message);
|
|
111
|
+
assert.match(message, /Allowed writable roots:/i);
|
|
112
|
+
assert.match(message, /Allowed Root/i);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("returns a prompt guard message before a run starts", () => {
|
|
116
|
+
const message = getPromptWorkspaceGuardMessage(
|
|
117
|
+
"Edit C:\\Users\\Example\\Desktop\\Other\\notes.txt",
|
|
118
|
+
workspaceRoot,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
assert(message);
|
|
122
|
+
assert.match(message, /Run blocked/i);
|
|
123
|
+
assert.match(message, /Locked workspace:/i);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("blocks directory-changing shell commands", () => {
|
|
127
|
+
assert.equal(containsDirectoryNavigationCommand("cd .."), true);
|
|
128
|
+
assert.equal(containsDirectoryNavigationCommand("Set-Location .."), true);
|
|
129
|
+
assert.equal(containsDirectoryNavigationCommand("pushd .."), true);
|
|
130
|
+
|
|
131
|
+
const message = getShellWorkspaceGuardMessage("cd ..", workspaceRoot);
|
|
132
|
+
assert(message);
|
|
133
|
+
assert.match(message, /Directory-changing commands/i);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("blocks shell commands that reference outside absolute paths", () => {
|
|
137
|
+
const message = getShellWorkspaceGuardMessage(
|
|
138
|
+
"type C:\\Users\\Example\\Desktop\\Other\\notes.txt",
|
|
139
|
+
workspaceRoot,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
assert(message);
|
|
143
|
+
assert.match(message, /Outside path references:/i);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("blocks shell commands that reference escaping relative paths", () => {
|
|
147
|
+
const message = getShellWorkspaceGuardMessage("type ..\\outside.txt", workspaceRoot);
|
|
148
|
+
|
|
149
|
+
assert(message);
|
|
150
|
+
assert.match(message, /Outside path references:/i);
|
|
151
|
+
});
|