@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,288 @@
|
|
|
1
|
+
import { posix, win32 } from "path";
|
|
2
|
+
import { normalizeWorkspaceRoot } from "./workspaceRoot.js";
|
|
3
|
+
|
|
4
|
+
type PathStyle = "windows" | "posix";
|
|
5
|
+
|
|
6
|
+
export interface WorkspacePathViolation {
|
|
7
|
+
rawPath: string;
|
|
8
|
+
normalizedPath: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const QUOTED_ABSOLUTE_PATH_PATTERN =
|
|
12
|
+
/(["'`])((?:[A-Za-z]:[\\/]|\\\\[^\\/\r\n]+[\\/][^\\/\r\n]+[\\/]|\/)[^"'`\r\n]+?)\1/g;
|
|
13
|
+
const QUOTED_RELATIVE_PATH_PATTERN = /(["'`])((?:\.\.?[\\/])[^"'`\r\n]+?)\1/g;
|
|
14
|
+
const WINDOWS_DRIVE_PATH_PATTERN = /(?:^|[\s([{\],;=])([A-Za-z]:[\\/][^\s"'`<>|]+)/g;
|
|
15
|
+
const WINDOWS_UNC_PATH_PATTERN =
|
|
16
|
+
/(?:^|[\s([{\],;=])(\\\\[^\\/\s"'`<>|]+[\\/][^\\/\s"'`<>|]+(?:[\\/][^\s"'`<>|]+)*)/g;
|
|
17
|
+
const POSIX_ABSOLUTE_PATH_PATTERN = /(?:^|[\s([{\],;=])(\/(?:[^\/\s"'`<>|]+\/)+[^\s"'`<>|]+)/g;
|
|
18
|
+
const RELATIVE_PATH_PATTERN = /(?:^|[\s([{\],;=])((?:\.\.?[\\/])(?:[^\s"'`<>|]+(?:[\\/][^\s"'`<>|]+)*))/g;
|
|
19
|
+
const TRAILING_PUNCTUATION_PATTERN = /[),.;:\]}]+$/;
|
|
20
|
+
const DIRECTORY_NAVIGATION_PATTERN = /(?:^|[;&\n]|&&|\|\|)\s*(cd|pushd|set-location)\b/i;
|
|
21
|
+
|
|
22
|
+
function detectPathStyle(pathValue: string): PathStyle | null {
|
|
23
|
+
if (/^[A-Za-z]:[\\/]/.test(pathValue) || /^\\\\[^\\\/]+[\\\/][^\\\/]+/.test(pathValue)) {
|
|
24
|
+
return "windows";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
if (pathValue.startsWith("/")) {
|
|
28
|
+
return "posix";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getPathApi(style: PathStyle) {
|
|
35
|
+
return style === "windows" ? win32 : posix;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stripWrappingQuotes(text: string): string {
|
|
39
|
+
return text.replace(/^["'`]|["'`]$/g, "");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stripTrailingPunctuation(text: string): string {
|
|
43
|
+
return text.replace(TRAILING_PUNCTUATION_PATTERN, "");
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isExplicitRelativePath(pathValue: string): boolean {
|
|
47
|
+
return /^(?:\.\.?[\\/])/.test(pathValue);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeAbsolutePath(pathValue: string, style: PathStyle): string {
|
|
51
|
+
const pathApi = getPathApi(style);
|
|
52
|
+
const resolved = pathApi.normalize(pathApi.resolve(pathValue));
|
|
53
|
+
const { root } = pathApi.parse(resolved);
|
|
54
|
+
|
|
55
|
+
let normalized = resolved;
|
|
56
|
+
while (normalized.length > root.length && /[\\/]+$/.test(normalized)) {
|
|
57
|
+
normalized = normalized.slice(0, -1);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function normalizeComparisonPath(pathValue: string, style: PathStyle): string {
|
|
64
|
+
const normalized = normalizeAbsolutePath(pathValue, style);
|
|
65
|
+
return style === "windows" ? normalized.toLowerCase() : normalized;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function normalizeAllowedRoots(workspaceRoot: string, allowedRoots: readonly string[] = []): string[] {
|
|
69
|
+
const normalizedWorkspace = normalizeWorkspaceRoot(workspaceRoot);
|
|
70
|
+
const roots = [normalizedWorkspace, ...allowedRoots];
|
|
71
|
+
const seen = new Set<string>();
|
|
72
|
+
const normalizedRoots: string[] = [];
|
|
73
|
+
|
|
74
|
+
for (const root of roots) {
|
|
75
|
+
const style = detectPathStyle(root);
|
|
76
|
+
if (!style) continue;
|
|
77
|
+
const normalizedRoot = normalizeAbsolutePath(root, style);
|
|
78
|
+
const comparisonKey = normalizeComparisonPath(normalizedRoot, style);
|
|
79
|
+
if (seen.has(comparisonKey)) continue;
|
|
80
|
+
seen.add(comparisonKey);
|
|
81
|
+
normalizedRoots.push(normalizedRoot);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return normalizedRoots;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function resolveWorkspacePath(pathValue: string, workspaceRoot: string): string {
|
|
88
|
+
const trimmed = stripWrappingQuotes(pathValue.trim());
|
|
89
|
+
const workspaceStyle = detectPathStyle(workspaceRoot) ?? "windows";
|
|
90
|
+
const explicitStyle = detectPathStyle(trimmed);
|
|
91
|
+
|
|
92
|
+
if (explicitStyle) {
|
|
93
|
+
return normalizeAbsolutePath(trimmed, explicitStyle);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const pathApi = getPathApi(workspaceStyle);
|
|
97
|
+
// Normalize separators in relative paths to match the workspace style so
|
|
98
|
+
// that Windows-style backslashes work correctly on POSIX systems and vice versa.
|
|
99
|
+
const separatorNormalized = workspaceStyle === "windows"
|
|
100
|
+
? trimmed.replace(/\//g, "\\")
|
|
101
|
+
: trimmed.replace(/\\/g, "/");
|
|
102
|
+
return normalizeAbsolutePath(pathApi.resolve(workspaceRoot, separatorNormalized), workspaceStyle);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export function isPathInsideWorkspace(pathValue: string, workspaceRoot: string): boolean {
|
|
106
|
+
const normalizedWorkspace = normalizeWorkspaceRoot(workspaceRoot);
|
|
107
|
+
return isPathInsideAllowedRoots(pathValue, normalizedWorkspace);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isPathInsideAllowedRoots(
|
|
111
|
+
pathValue: string,
|
|
112
|
+
workspaceRoot: string,
|
|
113
|
+
allowedRoots: readonly string[] = [],
|
|
114
|
+
): boolean {
|
|
115
|
+
const normalizedWorkspace = normalizeWorkspaceRoot(workspaceRoot);
|
|
116
|
+
const normalizedRoots = normalizeAllowedRoots(normalizedWorkspace, allowedRoots);
|
|
117
|
+
const resolvedTarget = resolveWorkspacePath(pathValue, normalizedWorkspace);
|
|
118
|
+
const targetStyle = detectPathStyle(resolvedTarget);
|
|
119
|
+
|
|
120
|
+
if (!targetStyle) {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
for (const root of normalizedRoots) {
|
|
125
|
+
const rootStyle = detectPathStyle(root);
|
|
126
|
+
if (!rootStyle || rootStyle !== targetStyle) {
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const pathApi = getPathApi(rootStyle);
|
|
131
|
+
const rootForComparison = normalizeComparisonPath(root, rootStyle);
|
|
132
|
+
const targetForComparison = normalizeComparisonPath(resolvedTarget, rootStyle);
|
|
133
|
+
const relativePath = pathApi.relative(rootForComparison, targetForComparison);
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
relativePath === "" ||
|
|
137
|
+
(!relativePath.startsWith("..") && !pathApi.isAbsolute(relativePath))
|
|
138
|
+
) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function extractExplicitPathReferences(text: string): string[] {
|
|
147
|
+
const matches: string[] = [];
|
|
148
|
+
const seen = new Set<string>();
|
|
149
|
+
|
|
150
|
+
const addMatch = (candidate: string) => {
|
|
151
|
+
const cleaned = stripTrailingPunctuation(stripWrappingQuotes(candidate.trim()));
|
|
152
|
+
if (!cleaned || (!detectPathStyle(cleaned) && !isExplicitRelativePath(cleaned))) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const comparisonKey = detectPathStyle(cleaned) === "posix" ? cleaned : cleaned.toLowerCase();
|
|
157
|
+
if (seen.has(comparisonKey)) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
seen.add(comparisonKey);
|
|
162
|
+
matches.push(cleaned);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
for (const pattern of [
|
|
166
|
+
QUOTED_ABSOLUTE_PATH_PATTERN,
|
|
167
|
+
QUOTED_RELATIVE_PATH_PATTERN,
|
|
168
|
+
WINDOWS_DRIVE_PATH_PATTERN,
|
|
169
|
+
WINDOWS_UNC_PATH_PATTERN,
|
|
170
|
+
POSIX_ABSOLUTE_PATH_PATTERN,
|
|
171
|
+
RELATIVE_PATH_PATTERN,
|
|
172
|
+
]) {
|
|
173
|
+
pattern.lastIndex = 0;
|
|
174
|
+
let match = pattern.exec(text);
|
|
175
|
+
while (match) {
|
|
176
|
+
addMatch(match[2] ?? match[1] ?? "");
|
|
177
|
+
match = pattern.exec(text);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return matches;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function findOutsideWorkspacePaths(
|
|
185
|
+
text: string,
|
|
186
|
+
workspaceRoot: string,
|
|
187
|
+
allowedRoots: readonly string[] = [],
|
|
188
|
+
): WorkspacePathViolation[] {
|
|
189
|
+
const violations: WorkspacePathViolation[] = [];
|
|
190
|
+
const seen = new Set<string>();
|
|
191
|
+
const workspaceStyle = detectPathStyle(workspaceRoot) ?? "windows";
|
|
192
|
+
|
|
193
|
+
for (const rawPath of extractExplicitPathReferences(text)) {
|
|
194
|
+
if (isPathInsideAllowedRoots(rawPath, workspaceRoot, allowedRoots)) {
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const explicitStyle = detectPathStyle(rawPath) ?? workspaceStyle;
|
|
199
|
+
const normalizedPath = detectPathStyle(rawPath)
|
|
200
|
+
? normalizeAbsolutePath(rawPath, explicitStyle)
|
|
201
|
+
: resolveWorkspacePath(rawPath, workspaceRoot);
|
|
202
|
+
const comparisonKey = explicitStyle === "windows" ? normalizedPath.toLowerCase() : normalizedPath;
|
|
203
|
+
|
|
204
|
+
if (seen.has(comparisonKey)) {
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
seen.add(comparisonKey);
|
|
209
|
+
violations.push({ rawPath, normalizedPath });
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return violations;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function containsDirectoryNavigationCommand(command: string): boolean {
|
|
216
|
+
return DIRECTORY_NAVIGATION_PATTERN.test(command);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function formatOutsidePathBlockMessage(
|
|
220
|
+
heading: string,
|
|
221
|
+
workspaceRoot: string,
|
|
222
|
+
violations: WorkspacePathViolation[],
|
|
223
|
+
allowedRoots: readonly string[] = [],
|
|
224
|
+
): string {
|
|
225
|
+
const normalizedWorkspace = normalizeWorkspaceRoot(workspaceRoot);
|
|
226
|
+
const normalizedAllowedRoots = normalizeAllowedRoots(normalizedWorkspace, allowedRoots)
|
|
227
|
+
.filter((root) => normalizeComparisonPath(root, detectPathStyle(root) ?? "windows") !== normalizeComparisonPath(normalizedWorkspace, detectPathStyle(normalizedWorkspace) ?? "windows"));
|
|
228
|
+
const paths = violations.map((item) => ` - ${item.normalizedPath}`).join("\n");
|
|
229
|
+
const extraRootsBlock = normalizedAllowedRoots.length > 0
|
|
230
|
+
? [
|
|
231
|
+
"Allowed writable roots:",
|
|
232
|
+
...normalizedAllowedRoots.map((root) => ` - ${root}`),
|
|
233
|
+
].join("\n")
|
|
234
|
+
: null;
|
|
235
|
+
|
|
236
|
+
return [
|
|
237
|
+
heading,
|
|
238
|
+
`Locked workspace: ${normalizedWorkspace}`,
|
|
239
|
+
extraRootsBlock,
|
|
240
|
+
"Outside path references:",
|
|
241
|
+
paths,
|
|
242
|
+
"Use relative paths inside this workspace, or relaunch the CLI from the folder you want to edit.",
|
|
243
|
+
].filter(Boolean).join("\n");
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function getPromptWorkspaceGuardMessage(
|
|
247
|
+
prompt: string,
|
|
248
|
+
workspaceRoot: string,
|
|
249
|
+
allowedRoots: readonly string[] = [],
|
|
250
|
+
): string | null {
|
|
251
|
+
const violations = findOutsideWorkspacePaths(prompt, workspaceRoot, allowedRoots);
|
|
252
|
+
if (violations.length === 0) {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return formatOutsidePathBlockMessage(
|
|
257
|
+
"Run blocked: this session can only work inside the locked workspace.",
|
|
258
|
+
workspaceRoot,
|
|
259
|
+
violations,
|
|
260
|
+
allowedRoots,
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export function getShellWorkspaceGuardMessage(
|
|
265
|
+
command: string,
|
|
266
|
+
workspaceRoot: string,
|
|
267
|
+
allowedRoots: readonly string[] = [],
|
|
268
|
+
): string | null {
|
|
269
|
+
if (containsDirectoryNavigationCommand(command)) {
|
|
270
|
+
return [
|
|
271
|
+
"Shell command blocked: this session is locked to the launch folder.",
|
|
272
|
+
`Locked workspace: ${normalizeWorkspaceRoot(workspaceRoot)}`,
|
|
273
|
+
"Directory-changing commands like cd, Set-Location, and pushd are disabled here.",
|
|
274
|
+
].join("\n");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const violations = findOutsideWorkspacePaths(command, workspaceRoot, allowedRoots);
|
|
278
|
+
if (violations.length === 0) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return formatOutsidePathBlockMessage(
|
|
283
|
+
"Shell command blocked: it references paths outside the locked workspace.",
|
|
284
|
+
workspaceRoot,
|
|
285
|
+
violations,
|
|
286
|
+
allowedRoots,
|
|
287
|
+
);
|
|
288
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { normalizeWorkspaceRoot, resolveWorkspaceRoot } from "./workspaceRoot.js";
|
|
4
|
+
|
|
5
|
+
test("prefers CODEX_WORKSPACE_ROOT when provided", () => {
|
|
6
|
+
const originalWorkspace = process.env.CODEX_WORKSPACE_ROOT;
|
|
7
|
+
try {
|
|
8
|
+
process.env.CODEX_WORKSPACE_ROOT = "D:/project/";
|
|
9
|
+
assert.equal(resolveWorkspaceRoot(), normalizeWorkspaceRoot("D:/project/"));
|
|
10
|
+
} finally {
|
|
11
|
+
process.env.CODEX_WORKSPACE_ROOT = originalWorkspace;
|
|
12
|
+
}
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("falls back to process.cwd when no workspace override exists", () => {
|
|
16
|
+
const originalWorkspace = process.env.CODEX_WORKSPACE_ROOT;
|
|
17
|
+
try {
|
|
18
|
+
delete process.env.CODEX_WORKSPACE_ROOT;
|
|
19
|
+
assert.equal(resolveWorkspaceRoot(), normalizeWorkspaceRoot(process.cwd()));
|
|
20
|
+
} finally {
|
|
21
|
+
process.env.CODEX_WORKSPACE_ROOT = originalWorkspace;
|
|
22
|
+
}
|
|
23
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { parse, resolve, win32 } from "path";
|
|
2
|
+
|
|
3
|
+
function isWindowsStylePath(p: string): boolean {
|
|
4
|
+
return /^[A-Za-z]:[\\/]/.test(p) || /^\\\\/.test(p);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function normalizeWorkspaceRoot(pathValue: string): string {
|
|
8
|
+
let candidate = pathValue.trim();
|
|
9
|
+
// Guard against obviously unsafe values that could break command-line parsing.
|
|
10
|
+
if (candidate.includes("\n") || candidate.includes("\r") || candidate.includes("\0")) {
|
|
11
|
+
candidate = process.cwd();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
if (isWindowsStylePath(candidate)) {
|
|
15
|
+
const resolved = win32.normalize(candidate);
|
|
16
|
+
const { root } = win32.parse(resolved);
|
|
17
|
+
let normalized = resolved;
|
|
18
|
+
while (normalized.length > root.length && /[\\/]+$/.test(normalized)) {
|
|
19
|
+
normalized = normalized.slice(0, -1);
|
|
20
|
+
}
|
|
21
|
+
return normalized;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const resolved = resolve(candidate);
|
|
25
|
+
const { root } = parse(resolved);
|
|
26
|
+
|
|
27
|
+
let normalized = resolved;
|
|
28
|
+
while (normalized.length > root.length && /[\\/]+$/.test(normalized)) {
|
|
29
|
+
normalized = normalized.slice(0, -1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return normalized;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function resolveWorkspaceRoot(): string {
|
|
36
|
+
const candidates = [
|
|
37
|
+
process.env.CODEX_WORKSPACE_ROOT,
|
|
38
|
+
process.cwd(),
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
for (const candidate of candidates) {
|
|
42
|
+
const value = candidate?.trim();
|
|
43
|
+
if (value) return normalizeWorkspaceRoot(value);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return normalizeWorkspaceRoot(process.cwd());
|
|
47
|
+
}
|
package/src/exec.test.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
|
|
6
|
+
test("codexa exec entrypoint does not import interactive UI modules", () => {
|
|
7
|
+
const source = readFileSync(fileURLToPath(new URL("./exec.ts", import.meta.url)), "utf8");
|
|
8
|
+
|
|
9
|
+
assert.doesNotMatch(source, /from\s+["']\.\/index(?:\.js)?["']/);
|
|
10
|
+
assert.doesNotMatch(source, /from\s+["']\.\/app(?:\.js)?["']/);
|
|
11
|
+
assert.doesNotMatch(source, /from\s+["']ink["']/);
|
|
12
|
+
assert.doesNotMatch(source, /from\s+["']react["']/);
|
|
13
|
+
});
|
package/src/exec.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { parseHeadlessExecArgs } from "./headless/execArgs.js";
|
|
2
|
+
import {
|
|
3
|
+
createHeadlessExecTiming,
|
|
4
|
+
HEADLESS_EXEC_PARSE_ERROR,
|
|
5
|
+
runHeadlessExec,
|
|
6
|
+
} from "./headless/execRunner.js";
|
|
7
|
+
|
|
8
|
+
export function printExecHelp(): void {
|
|
9
|
+
console.log(`Usage:
|
|
10
|
+
codexa exec "prompt"
|
|
11
|
+
codexa exec --prompt "prompt"
|
|
12
|
+
codexa exec [--profile <name>] [-c key=value] "prompt"
|
|
13
|
+
codexa exec --model gpt-5.4-mini --reasoning medium "Reply with exactly: CODEXA_READY"
|
|
14
|
+
|
|
15
|
+
Options:
|
|
16
|
+
--prompt <text> Prompt to submit in headless mode.
|
|
17
|
+
--reasoning <effort>
|
|
18
|
+
Reasoning effort: none, minimal, low, medium, high, xhigh.
|
|
19
|
+
--benchmark-diagnostics
|
|
20
|
+
Alias for --timing.
|
|
21
|
+
--timing Print optional codexa exec phase timing to stderr.
|
|
22
|
+
--codexa-prompt-policy <raw|wrapped>
|
|
23
|
+
Prompt policy for exec mode. Defaults to raw.
|
|
24
|
+
--skip-git-repo-check
|
|
25
|
+
Forward --skip-git-repo-check to codex exec.
|
|
26
|
+
--profile <name> Load a Codex profile from config.
|
|
27
|
+
-m, --model <name> Select the Codex model for this launch.
|
|
28
|
+
-c, --config <key=val> Override a runtime config value.
|
|
29
|
+
-h, --help Show this help text and exit.
|
|
30
|
+
`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const isMainModule = Boolean((import.meta as ImportMeta & { main?: boolean }).main);
|
|
34
|
+
|
|
35
|
+
if (isMainModule) {
|
|
36
|
+
const argv = process.argv.slice(2);
|
|
37
|
+
const timing = createHeadlessExecTiming({
|
|
38
|
+
enabled: process.env.CODEXA_EXEC_TIMING === "1"
|
|
39
|
+
|| argv.includes("--timing")
|
|
40
|
+
|| argv.includes("--benchmark-diagnostics"),
|
|
41
|
+
stderr: process.stderr,
|
|
42
|
+
startTimeMs: Number(process.env.CODEXA_EXEC_TIMING_EPOCH_MS) || undefined,
|
|
43
|
+
});
|
|
44
|
+
timing.mark("codexa_exec_process_start", { pid: process.pid });
|
|
45
|
+
|
|
46
|
+
const parsed = parseHeadlessExecArgs(argv);
|
|
47
|
+
if (!parsed.ok) {
|
|
48
|
+
console.error(`[codexa exec] parse: ${parsed.error}`);
|
|
49
|
+
timing.mark("codexa_exec_process_exit", { exit_code: HEADLESS_EXEC_PARSE_ERROR });
|
|
50
|
+
process.exit(HEADLESS_EXEC_PARSE_ERROR);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
timing.mark("args_parsed", {
|
|
54
|
+
prompt_character_count: parsed.value.prompt.length,
|
|
55
|
+
prompt_policy: parsed.value.promptPolicy,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (parsed.value.help) {
|
|
59
|
+
printExecHelp();
|
|
60
|
+
timing.mark("codexa_exec_process_exit", { exit_code: 0 });
|
|
61
|
+
process.exit(0);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const result = await runHeadlessExec({
|
|
65
|
+
prompt: parsed.value.prompt,
|
|
66
|
+
launchArgs: parsed.value.launchArgs,
|
|
67
|
+
promptPolicy: parsed.value.promptPolicy,
|
|
68
|
+
benchmarkDiagnostics: (parsed.value.timing || process.env.CODEXA_EXEC_TIMING === "1") ? timing : undefined,
|
|
69
|
+
});
|
|
70
|
+
timing.mark("codexa_exec_process_exit", { exit_code: result.exitCode });
|
|
71
|
+
process.exit(result.exitCode);
|
|
72
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import { parseHeadlessExecArgs } from "./execArgs.js";
|
|
4
|
+
|
|
5
|
+
test("parses codexa exec positional prompt", () => {
|
|
6
|
+
const parsed = parseHeadlessExecArgs(["Print", "the", "directory"]);
|
|
7
|
+
|
|
8
|
+
assert.equal(parsed.ok, true);
|
|
9
|
+
if (!parsed.ok) return;
|
|
10
|
+
assert.equal(parsed.value.prompt, "Print the directory");
|
|
11
|
+
assert.equal(parsed.value.promptPolicy, "raw");
|
|
12
|
+
assert.equal(parsed.value.launchArgs.initialPrompt, "Print the directory");
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
test("parses codexa exec --prompt value", () => {
|
|
16
|
+
const parsed = parseHeadlessExecArgs(["--prompt", "Print the directory"]);
|
|
17
|
+
|
|
18
|
+
assert.equal(parsed.ok, true);
|
|
19
|
+
if (!parsed.ok) return;
|
|
20
|
+
assert.equal(parsed.value.prompt, "Print the directory");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("parses codexa exec --prompt=value", () => {
|
|
24
|
+
const parsed = parseHeadlessExecArgs(["--prompt=Print the directory"]);
|
|
25
|
+
|
|
26
|
+
assert.equal(parsed.ok, true);
|
|
27
|
+
if (!parsed.ok) return;
|
|
28
|
+
assert.equal(parsed.value.prompt, "Print the directory");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("rejects missing and empty prompts", () => {
|
|
32
|
+
const missing = parseHeadlessExecArgs([]);
|
|
33
|
+
assert.equal(missing.ok, false);
|
|
34
|
+
if (!missing.ok) {
|
|
35
|
+
assert.match(missing.error, /missing prompt/i);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const empty = parseHeadlessExecArgs(["--prompt", " "]);
|
|
39
|
+
assert.equal(empty.ok, false);
|
|
40
|
+
if (!empty.ok) {
|
|
41
|
+
assert.match(empty.error, /--prompt/i);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("parses codexa exec --reasoning value", () => {
|
|
46
|
+
const parsed = parseHeadlessExecArgs(["--reasoning", "medium", "Print files"]);
|
|
47
|
+
|
|
48
|
+
assert.equal(parsed.ok, true);
|
|
49
|
+
if (!parsed.ok) return;
|
|
50
|
+
assert.equal(parsed.value.prompt, "Print files");
|
|
51
|
+
assert.deepEqual(parsed.value.launchArgs.configOverrides, ["model_reasoning_effort=medium"]);
|
|
52
|
+
assert.deepEqual(parsed.value.launchArgs.passthroughArgs, ["--reasoning", "medium"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("parses codexa exec --reasoning=value", () => {
|
|
56
|
+
const parsed = parseHeadlessExecArgs(["--reasoning=high", "Print files"]);
|
|
57
|
+
|
|
58
|
+
assert.equal(parsed.ok, true);
|
|
59
|
+
if (!parsed.ok) return;
|
|
60
|
+
assert.equal(parsed.value.prompt, "Print files");
|
|
61
|
+
assert.deepEqual(parsed.value.launchArgs.configOverrides, ["model_reasoning_effort=high"]);
|
|
62
|
+
assert.deepEqual(parsed.value.launchArgs.passthroughArgs, ["--reasoning=high"]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test("parses timing and prompt policy flags without forwarding them to Codex", () => {
|
|
66
|
+
const parsed = parseHeadlessExecArgs([
|
|
67
|
+
"--timing",
|
|
68
|
+
"--codexa-prompt-policy",
|
|
69
|
+
"wrapped",
|
|
70
|
+
"Print files",
|
|
71
|
+
]);
|
|
72
|
+
|
|
73
|
+
assert.equal(parsed.ok, true);
|
|
74
|
+
if (!parsed.ok) return;
|
|
75
|
+
assert.equal(parsed.value.timing, true);
|
|
76
|
+
assert.equal(parsed.value.promptPolicy, "wrapped");
|
|
77
|
+
assert.equal(parsed.value.prompt, "Print files");
|
|
78
|
+
assert.deepEqual(parsed.value.launchArgs.passthroughArgs, []);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("rejects invalid prompt policy values", () => {
|
|
82
|
+
const parsed = parseHeadlessExecArgs(["--codexa-prompt-policy", "verbose", "Prompt"]);
|
|
83
|
+
|
|
84
|
+
assert.equal(parsed.ok, false);
|
|
85
|
+
if (!parsed.ok) {
|
|
86
|
+
assert.match(parsed.error, /codexa-prompt-policy/i);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("parses codexa exec --model and --reasoning together", () => {
|
|
91
|
+
const parsed = parseHeadlessExecArgs([
|
|
92
|
+
"--model", "gpt-5.4-mini",
|
|
93
|
+
"--reasoning", "medium",
|
|
94
|
+
"Reply with exactly: CODEXA_READY",
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
assert.equal(parsed.ok, true);
|
|
98
|
+
if (!parsed.ok) return;
|
|
99
|
+
assert.equal(parsed.value.prompt, "Reply with exactly: CODEXA_READY");
|
|
100
|
+
assert.deepEqual(parsed.value.launchArgs.configOverrides, [
|
|
101
|
+
"model=\"gpt-5.4-mini\"",
|
|
102
|
+
"model_reasoning_effort=medium",
|
|
103
|
+
]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("preserves profile and repeated config overrides", () => {
|
|
107
|
+
const parsed = parseHeadlessExecArgs([
|
|
108
|
+
"--benchmark-diagnostics",
|
|
109
|
+
"--skip-git-repo-check",
|
|
110
|
+
"--profile",
|
|
111
|
+
"bench",
|
|
112
|
+
"--model",
|
|
113
|
+
"gpt-5.4-mini",
|
|
114
|
+
"-c",
|
|
115
|
+
"model_reasoning_effort=medium",
|
|
116
|
+
"--config",
|
|
117
|
+
"sandbox_mode=\"workspace-write\"",
|
|
118
|
+
"--config=approval_policy=\"never\"",
|
|
119
|
+
"Print",
|
|
120
|
+
"files",
|
|
121
|
+
]);
|
|
122
|
+
|
|
123
|
+
assert.equal(parsed.ok, true);
|
|
124
|
+
if (!parsed.ok) return;
|
|
125
|
+
assert.equal(parsed.value.benchmarkDiagnostics, true);
|
|
126
|
+
assert.equal(parsed.value.timing, true);
|
|
127
|
+
assert.equal(parsed.value.prompt, "Print files");
|
|
128
|
+
assert.equal(parsed.value.launchArgs.profile, "bench");
|
|
129
|
+
assert.deepEqual(parsed.value.launchArgs.configOverrides, [
|
|
130
|
+
"model=\"gpt-5.4-mini\"",
|
|
131
|
+
"model_reasoning_effort=medium",
|
|
132
|
+
"sandbox_mode=\"workspace-write\"",
|
|
133
|
+
"approval_policy=\"never\"",
|
|
134
|
+
]);
|
|
135
|
+
assert.deepEqual(parsed.value.launchArgs.passthroughArgs, [
|
|
136
|
+
"--skip-git-repo-check",
|
|
137
|
+
"--profile",
|
|
138
|
+
"bench",
|
|
139
|
+
"--model",
|
|
140
|
+
"gpt-5.4-mini",
|
|
141
|
+
"-c",
|
|
142
|
+
"model_reasoning_effort=medium",
|
|
143
|
+
"--config",
|
|
144
|
+
"sandbox_mode=\"workspace-write\"",
|
|
145
|
+
"--config=approval_policy=\"never\"",
|
|
146
|
+
]);
|
|
147
|
+
});
|