@golba98/codexa 1.0.1 → 1.0.3
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 +391 -94
- package/package.json +14 -8
- package/src/app.tsx +196 -63
- package/src/commands/handler.ts +9 -4
- package/src/config/buildInfo.ts +3 -0
- package/src/config/launchArgs.ts +10 -0
- package/src/config/layeredConfig.ts +0 -0
- package/src/config/persistence.ts +51 -0
- package/src/config/runtimeConfig.ts +4 -0
- package/src/config/settings.ts +6 -13
- package/src/config/toml-serialize.ts +0 -0
- package/src/config/trustStore.ts +0 -0
- package/src/config/updateCheckCache.ts +63 -0
- package/src/core/attachments.ts +0 -0
- package/src/core/auth/codexAuth.ts +0 -0
- package/src/core/channel.ts +23 -0
- package/src/core/cleanupFastFail.ts +0 -0
- package/src/core/clipboard.ts +0 -0
- package/src/core/codex.ts +0 -0
- package/src/core/codexExecArgs.ts +0 -0
- package/src/core/codexLaunch.ts +0 -0
- package/src/core/codexPrompt.ts +0 -0
- package/src/core/executables/claudeExecutable.ts +0 -0
- package/src/core/executables/codexExecutable.ts +1 -0
- package/src/core/executables/executableResolver.ts +61 -43
- package/src/core/executables/geminiExecutable.ts +0 -0
- package/src/core/githubDiagnostics.ts +0 -0
- package/src/core/hollowResponseFormat.ts +0 -0
- package/src/core/inputDebug.ts +0 -0
- package/src/core/launchContext.ts +33 -33
- package/src/core/models/codexCapabilities.ts +0 -0
- package/src/core/models/codexModelCapabilities.ts +0 -0
- package/src/core/models/modelSpecs.ts +0 -0
- package/src/core/perf/profiler.ts +0 -0
- package/src/core/perf/renderDebug.ts +0 -0
- package/src/core/planStorage.ts +0 -0
- package/src/core/process/CommandRunner.ts +0 -0
- package/src/core/process/processValidation.ts +9 -5
- package/src/core/projectInstructions.ts +0 -0
- package/src/core/providerLauncher/launcher.ts +59 -42
- package/src/core/providerLauncher/registry.ts +0 -0
- package/src/core/providerLauncher/types.ts +0 -0
- package/src/core/providerLauncher/workspaceConfig.ts +0 -0
- package/src/core/providerRuntime/anthropic.ts +0 -0
- package/src/core/providerRuntime/capabilityProfile.ts +0 -0
- package/src/core/providerRuntime/claudeCodeDiscovery.ts +0 -0
- package/src/core/providerRuntime/contextMetadata.ts +0 -0
- package/src/core/providerRuntime/gemini.ts +0 -0
- package/src/core/providerRuntime/lmstudio.ts +0 -0
- package/src/core/providerRuntime/local.ts +0 -0
- package/src/core/providerRuntime/models.ts +0 -0
- package/src/core/providerRuntime/reasoning.ts +0 -0
- package/src/core/providerRuntime/registry.ts +0 -0
- package/src/core/providerRuntime/types.ts +0 -0
- package/src/core/providers/codexJsonStream.ts +0 -0
- package/src/core/providers/codexSubprocess.ts +0 -0
- package/src/core/providers/codexTranscript.ts +0 -0
- package/src/core/providers/openaiNative.ts +0 -0
- package/src/core/providers/registry.ts +0 -0
- package/src/core/providers/types.ts +0 -0
- package/src/core/terminal/startupClear.ts +20 -0
- package/src/core/terminal/terminalCapabilities.ts +0 -0
- package/src/core/terminal/terminalControl.ts +0 -0
- package/src/core/terminal/terminalSanitize.ts +0 -0
- package/src/core/terminal/terminalSelection.ts +0 -0
- package/src/core/terminal/terminalTitle.ts +0 -0
- package/src/core/updateCheck.ts +188 -0
- package/src/core/workspaceActivity.ts +0 -0
- package/src/core/workspaceGuard.ts +0 -0
- package/src/core/workspaceRoot.ts +0 -0
- package/src/exec.ts +0 -0
- package/src/headless/execArgs.ts +1 -0
- package/src/headless/execRunner.ts +0 -0
- package/src/index.tsx +13 -9
- package/src/session/appSession.ts +0 -0
- package/src/session/chatLifecycle.ts +0 -0
- package/src/session/liveRenderScheduler.ts +0 -0
- package/src/session/planFlow.ts +0 -0
- package/src/session/planTranscript.ts +0 -0
- package/src/session/promptRunSchedule.ts +0 -0
- package/src/session/types.ts +2 -1
- package/src/test/runtimeTestUtils.ts +0 -0
- package/src/types/react-dom.d.ts +0 -0
- package/src/ui/ActionRequiredBlock.tsx +5 -5
- package/src/ui/ActivityBars.tsx +3 -3
- package/src/ui/ActivityIndicator.tsx +6 -6
- package/src/ui/AgentBlock.tsx +6 -6
- package/src/ui/AnimatedStatusText.tsx +1 -1
- package/src/ui/AppShell.tsx +13 -5
- package/src/ui/AttachmentImportPanel.tsx +8 -8
- package/src/ui/AuthPanel.tsx +20 -20
- package/src/ui/BackendPicker.tsx +0 -0
- package/src/ui/BottomComposer.tsx +117 -78
- package/src/ui/DashCard.tsx +3 -3
- package/src/ui/Markdown.tsx +17 -17
- package/src/ui/ModePicker.tsx +0 -0
- package/src/ui/ModelPicker.tsx +0 -0
- package/src/ui/ModelPickerScreen.tsx +10 -10
- package/src/ui/ModelReasoningPicker.tsx +14 -14
- package/src/ui/Panel.tsx +3 -3
- package/src/ui/PermissionsPanel.tsx +0 -0
- package/src/ui/PlanActionPicker.tsx +6 -6
- package/src/ui/PlanReviewPanel.tsx +8 -8
- package/src/ui/ProviderPicker.tsx +18 -18
- package/src/ui/ReasoningPicker.tsx +0 -0
- package/src/ui/RunFooter.tsx +3 -3
- package/src/ui/SelectionPanel.tsx +4 -4
- package/src/ui/SettingsPanel.tsx +8 -8
- package/src/ui/Spinner.tsx +1 -1
- package/src/ui/StaticIntroItem.tsx +0 -0
- package/src/ui/StaticTranscriptItem.tsx +0 -0
- package/src/ui/TextEntryPanel.tsx +10 -10
- package/src/ui/ThemePicker.tsx +0 -0
- package/src/ui/ThinkingBlock.tsx +8 -8
- package/src/ui/Timeline.tsx +21 -17
- package/src/ui/TopHeader.tsx +421 -257
- package/src/ui/TurnGroup.tsx +31 -31
- package/src/ui/UpdateAvailableCard.tsx +41 -0
- package/src/ui/UpdatePromptPanel.tsx +203 -0
- package/src/ui/busyStatusAnimation.ts +0 -0
- package/src/ui/commandNormalize.ts +0 -0
- package/src/ui/diffRenderer.ts +0 -0
- package/src/ui/focus.ts +3 -0
- package/src/ui/inputBuffer.ts +0 -0
- package/src/ui/layout.ts +1 -1
- package/src/ui/logoVariants.ts +107 -0
- package/src/ui/modeDisplay.ts +12 -12
- package/src/ui/outputPipeline.ts +0 -0
- package/src/ui/progressEntries.ts +0 -0
- package/src/ui/runActivityView.ts +0 -0
- package/src/ui/runtimeDisplay.ts +100 -0
- package/src/ui/slashCommands.ts +0 -0
- package/src/ui/terminalAnswerFormat.ts +0 -0
- package/src/ui/textLayout.ts +0 -0
- package/src/ui/theme.tsx +274 -395
- package/src/ui/themeFlow.ts +0 -0
- package/src/ui/timelineMeasure.ts +29 -19
- package/src/ui/useThrottledValue.ts +0 -0
- package/scripts/audit-codexa-capabilities.mjs +0 -466
- package/scripts/smoke-terminal-bench.mjs +0 -35
- package/src/appRenderStability.test.ts +0 -131
- package/src/commands/handler.test.ts +0 -643
- package/src/config/launchArgs.test.ts +0 -158
- package/src/config/layeredConfig.test.ts +0 -143
- package/src/config/persistence.test.ts +0 -110
- package/src/config/runtimeConfig.test.ts +0 -218
- package/src/config/settings.test.ts +0 -155
- package/src/config/trustStore.test.ts +0 -29
- package/src/core/attachments.test.ts +0 -155
- package/src/core/auth/codexAuth.test.ts +0 -68
- package/src/core/cleanupFastFail.test.ts +0 -76
- package/src/core/codexExecArgs.test.ts +0 -195
- package/src/core/codexLaunch.test.ts +0 -205
- package/src/core/codexPrompt.test.ts +0 -252
- package/src/core/executables/codexExecutable.test.ts +0 -212
- package/src/core/executables/executableResolver.test.ts +0 -129
- package/src/core/executables/geminiExecutable.test.ts +0 -116
- package/src/core/executables/pathSanityScan.test.ts +0 -47
- package/src/core/githubDiagnostics.test.ts +0 -92
- package/src/core/hollowResponseFormat.test.ts +0 -58
- package/src/core/launchContext.test.ts +0 -157
- package/src/core/models/codexCapabilities.test.ts +0 -45
- package/src/core/models/codexModelCapabilities.test.ts +0 -246
- package/src/core/models/modelSpecs.test.ts +0 -283
- package/src/core/perf/renderDebug.test.ts +0 -230
- package/src/core/planStorage.test.ts +0 -143
- package/src/core/process/CommandRunner.test.ts +0 -105
- package/src/core/projectInstructions.test.ts +0 -50
- package/src/core/providerLauncher/launcher.test.ts +0 -238
- package/src/core/providerLauncher/registry.test.ts +0 -324
- package/src/core/providerLauncher/workspaceConfig.test.ts +0 -638
- package/src/core/providerRuntime/anthropic.test.ts +0 -1120
- package/src/core/providerRuntime/capabilityProfile.test.ts +0 -311
- package/src/core/providerRuntime/contextMetadata.test.ts +0 -468
- package/src/core/providerRuntime/gemini.test.ts +0 -437
- package/src/core/providerRuntime/lmstudio.test.ts +0 -168
- package/src/core/providerRuntime/local.test.ts +0 -787
- package/src/core/providerRuntime/registry.test.ts +0 -233
- package/src/core/providers/codexJsonStream.test.ts +0 -148
- package/src/core/providers/codexSubprocess.test.ts +0 -68
- package/src/core/providers/codexTranscript.test.ts +0 -284
- package/src/core/terminal/terminalCapabilities.test.ts +0 -93
- package/src/core/terminal/terminalControl.test.ts +0 -75
- package/src/core/terminal/terminalSanitize.test.ts +0 -22
- package/src/core/terminal/terminalSelection.test.ts +0 -42
- package/src/core/terminal/terminalTitle.test.ts +0 -328
- package/src/core/workspaceActivity.test.ts +0 -163
- package/src/core/workspaceGuard.test.ts +0 -151
- package/src/core/workspaceRoot.test.ts +0 -23
- package/src/exec.test.ts +0 -13
- package/src/headless/execArgs.test.ts +0 -147
- package/src/headless/execRunner.test.ts +0 -434
- package/src/index.test.tsx +0 -618
- package/src/session/appSession.test.ts +0 -897
- package/src/session/chatLifecycle.test.ts +0 -64
- package/src/session/liveRenderScheduler.test.ts +0 -201
- package/src/session/planFlow.test.ts +0 -103
- package/src/session/planTranscript.test.ts +0 -65
- package/src/session/promptRunSchedule.test.ts +0 -36
- package/src/ui/ActivityIndicator.test.tsx +0 -58
- package/src/ui/AgentBlock.test.ts +0 -6
- package/src/ui/AnimatedStatusText.test.ts +0 -16
- package/src/ui/AppShell.test.tsx +0 -1739
- package/src/ui/AttachmentImportPanel.test.tsx +0 -204
- package/src/ui/BottomComposer.test.ts +0 -674
- package/src/ui/CodexLogo.tsx +0 -55
- package/src/ui/Markdown.test.ts +0 -157
- package/src/ui/ModelPickerProviderScope.test.tsx +0 -411
- package/src/ui/ModelPickerScreen.test.tsx +0 -99
- package/src/ui/ModelPickerState.test.tsx +0 -151
- package/src/ui/ModelReasoningPicker.test.tsx +0 -447
- package/src/ui/PlanReviewPanel.test.tsx +0 -267
- package/src/ui/PromptCardBorder.test.tsx +0 -161
- package/src/ui/ProviderPicker.test.tsx +0 -289
- package/src/ui/ProviderShortcut.test.tsx +0 -143
- package/src/ui/SettingsPanel.test.tsx +0 -233
- package/src/ui/Timeline.test.ts +0 -2067
- package/src/ui/TimelineNavigation.test.tsx +0 -201
- package/src/ui/TopHeader.test.tsx +0 -239
- package/src/ui/TurnGroup.test.tsx +0 -365
- package/src/ui/busyStatusAnimation.test.ts +0 -30
- package/src/ui/commandNormalize.test.ts +0 -142
- package/src/ui/diffRenderer.test.ts +0 -102
- package/src/ui/focusFlow.test.tsx +0 -1098
- package/src/ui/inputBuffer.test.ts +0 -151
- package/src/ui/layout.test.ts +0 -145
- package/src/ui/modeDisplay.test.ts +0 -42
- package/src/ui/runActivityView.test.ts +0 -89
- package/src/ui/runLifecycleView.test.tsx +0 -237
- package/src/ui/statusRenderIsolation.test.tsx +0 -654
- package/src/ui/terminalAnswerFormat.test.ts +0 -19
- package/src/ui/textLayout.test.ts +0 -18
- package/src/ui/themeFlow.test.ts +0 -53
- package/src/ui/timelineMeasureCache.test.ts +0 -986
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "fs";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import { dirname, join } from "path";
|
|
4
|
+
|
|
5
|
+
export interface UpdateCheckCache {
|
|
6
|
+
lastChecked: number;
|
|
7
|
+
currentVersion: string;
|
|
8
|
+
latestVersion: string | null;
|
|
9
|
+
updateAvailable: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const CACHE_FILE = join(homedir(), ".codexa-update-check.json");
|
|
13
|
+
|
|
14
|
+
export function loadUpdateCheckCache(): UpdateCheckCache | null {
|
|
15
|
+
try {
|
|
16
|
+
const text = readFileSync(CACHE_FILE, "utf-8");
|
|
17
|
+
const data = JSON.parse(text) as Record<string, unknown>;
|
|
18
|
+
if (typeof data.lastChecked !== "number") return null;
|
|
19
|
+
if (typeof data.currentVersion !== "string") return null;
|
|
20
|
+
return {
|
|
21
|
+
lastChecked: data.lastChecked,
|
|
22
|
+
currentVersion: data.currentVersion,
|
|
23
|
+
latestVersion: typeof data.latestVersion === "string" ? data.latestVersion : null,
|
|
24
|
+
updateAvailable: data.updateAvailable === true,
|
|
25
|
+
};
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function saveUpdateCheckCache(cache: UpdateCheckCache): void {
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(dirname(CACHE_FILE), { recursive: true });
|
|
34
|
+
const tmp = `${CACHE_FILE}.tmp`;
|
|
35
|
+
writeFileSync(tmp, JSON.stringify(cache, null, 2), "utf-8");
|
|
36
|
+
renameSync(tmp, CACHE_FILE);
|
|
37
|
+
} catch {
|
|
38
|
+
// Best-effort — never crash on cache write failure.
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function stripV(v: string): string {
|
|
43
|
+
return v.startsWith("v") ? v.slice(1) : v;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Returns true only when the cache is still usable:
|
|
48
|
+
* - `runningVersion` matches the version the cache was written for (version-mismatch = stale)
|
|
49
|
+
* - The cache was written within `intervalHours`
|
|
50
|
+
*/
|
|
51
|
+
export function isCacheValid(
|
|
52
|
+
cache: UpdateCheckCache,
|
|
53
|
+
intervalHours: number,
|
|
54
|
+
runningVersion?: string,
|
|
55
|
+
): boolean {
|
|
56
|
+
if (runningVersion !== undefined) {
|
|
57
|
+
if (stripV(cache.currentVersion) !== stripV(runningVersion)) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const maxAgeMs = intervalHours * 60 * 60 * 1000;
|
|
62
|
+
return Date.now() - cache.lastChecked < maxAgeMs;
|
|
63
|
+
}
|
package/src/core/attachments.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { APP_VERSION } from "../config/settings.js";
|
|
2
|
+
|
|
3
|
+
export const CODEXA_CHANNEL_ENV = "CODEXA_CHANNEL";
|
|
4
|
+
export const LOCAL_DEV_CHANNEL = "local-dev";
|
|
5
|
+
|
|
6
|
+
export function getCodexaChannel(env: NodeJS.ProcessEnv = process.env): string {
|
|
7
|
+
return env[CODEXA_CHANNEL_ENV]?.trim() || "published";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function isLocalDevChannel(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
11
|
+
return getCodexaChannel(env) === LOCAL_DEV_CHANNEL;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatCodexaVersionLabel(
|
|
15
|
+
version: string = APP_VERSION,
|
|
16
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
17
|
+
): string {
|
|
18
|
+
return isLocalDevChannel(env) ? `${version}-dev local` : version;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function formatCodexaBrandLabel(env: NodeJS.ProcessEnv = process.env): string {
|
|
22
|
+
return `Codexa v${formatCodexaVersionLabel(APP_VERSION, env)}`;
|
|
23
|
+
}
|
|
File without changes
|
package/src/core/clipboard.ts
CHANGED
|
File without changes
|
package/src/core/codex.ts
CHANGED
|
File without changes
|
|
File without changes
|
package/src/core/codexLaunch.ts
CHANGED
|
File without changes
|
package/src/core/codexPrompt.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -101,6 +101,7 @@ export function spawnCodexProcess(
|
|
|
101
101
|
options: SpawnOptions,
|
|
102
102
|
): ReturnType<typeof spawn> {
|
|
103
103
|
const spec = buildSpawnSpec(executable, args);
|
|
104
|
+
if (!spec.executable) throw new Error("Codex executable path is empty.");
|
|
104
105
|
return spawn(spec.executable, spec.args, { ...options, shell: false });
|
|
105
106
|
}
|
|
106
107
|
|
|
@@ -21,18 +21,27 @@ export interface ExecutableResolverOptions {
|
|
|
21
21
|
requireResolvedFile?: boolean;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function validateConfiguredExecutable(value: string, label: string, cwd: string): string {
|
|
25
|
-
return normalizeExecutableValue(value, {
|
|
24
|
+
function validateConfiguredExecutable(value: string, label: string, cwd: string): string {
|
|
25
|
+
return normalizeExecutableValue(value, {
|
|
26
26
|
label,
|
|
27
27
|
cwd,
|
|
28
28
|
requireExistingPath: /[\\/]/.test(value) || /^[\s"']*[A-Za-z]:/.test(value),
|
|
29
29
|
allowBareExecutable: true,
|
|
30
|
-
});
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function validateResolvedExecutable(value: string, label: string, cwd: string): string {
|
|
34
|
+
return normalizeExecutableValue(value, {
|
|
35
|
+
label,
|
|
36
|
+
cwd,
|
|
37
|
+
requireExistingPath: false,
|
|
38
|
+
allowBareExecutable: true,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function resolveWithWhere(
|
|
43
|
+
runCommandImpl: CommandRunner,
|
|
44
|
+
cwd: string,
|
|
36
45
|
query: string,
|
|
37
46
|
requireResolvedFile: boolean,
|
|
38
47
|
): Promise<string | null> {
|
|
@@ -47,13 +56,19 @@ async function resolveWithWhere(
|
|
|
47
56
|
const lines = whereResult.stdout
|
|
48
57
|
.trim()
|
|
49
58
|
.split(/[\r\n]+/)
|
|
50
|
-
.map((l) => l.trim())
|
|
51
|
-
.filter(Boolean);
|
|
52
|
-
for (const line of lines) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
}
|
|
59
|
+
.map((l) => l.trim())
|
|
60
|
+
.filter(Boolean);
|
|
61
|
+
for (const line of lines) {
|
|
62
|
+
let candidate: string;
|
|
63
|
+
try {
|
|
64
|
+
candidate = validateResolvedExecutable(line, "where.exe result", cwd);
|
|
65
|
+
} catch {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (!requireResolvedFile || existsSync(candidate)) return candidate;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
57
72
|
|
|
58
73
|
/**
|
|
59
74
|
* Resolves an executable's location.
|
|
@@ -98,41 +113,44 @@ export async function resolveExecutable(options: ExecutableResolverOptions): Pro
|
|
|
98
113
|
for (const candidate of options.commandNames) {
|
|
99
114
|
knownCandidates.push(join(dir, candidate));
|
|
100
115
|
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
for (const candidate of knownCandidates) {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
for (const candidate of knownCandidates) {
|
|
119
|
+
const validated = validateResolvedExecutable(candidate, `${options.label} known executable`, cwd);
|
|
120
|
+
if (existsSync(validated)) return validated;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 5. Explicit known file fallbacks
|
|
125
|
+
for (const candidate of options.knownFilePaths ?? []) {
|
|
126
|
+
const validated = validateResolvedExecutable(candidate, `${options.label} known executable`, cwd);
|
|
127
|
+
if (existsSync(validated)) return validated;
|
|
128
|
+
}
|
|
112
129
|
|
|
113
130
|
if (options.allowBareFallback === false) {
|
|
114
131
|
throw new Error(`${options.label} executable was not found.`);
|
|
115
132
|
}
|
|
116
133
|
|
|
117
|
-
// 6. Bare name fallback — prefer the name without an extension (works on Unix).
|
|
118
|
-
const bareName = options.commandNames.find((c) => !c.includes(".")) ?? options.commandNames[0];
|
|
119
|
-
return bareName
|
|
120
|
-
}
|
|
134
|
+
// 6. Bare name fallback — prefer the name without an extension (works on Unix).
|
|
135
|
+
const bareName = options.commandNames.find((c) => !c.includes(".")) ?? options.commandNames[0];
|
|
136
|
+
return validateResolvedExecutable(bareName!, `${options.label} executable`, cwd);
|
|
137
|
+
}
|
|
121
138
|
|
|
122
139
|
/**
|
|
123
140
|
* Builds the spawn spec for a resolved executable.
|
|
124
141
|
* .cmd and .bat files must be invoked via `cmd.exe /d /s /c` on Windows.
|
|
125
142
|
*/
|
|
126
|
-
export function buildSpawnSpec(
|
|
127
|
-
executable: string,
|
|
128
|
-
args: string[],
|
|
129
|
-
): { executable: string; args: string[] } {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
}
|
|
143
|
+
export function buildSpawnSpec(
|
|
144
|
+
executable: string,
|
|
145
|
+
args: string[],
|
|
146
|
+
): { executable: string; args: string[] } {
|
|
147
|
+
const validatedExecutable = validateResolvedExecutable(executable, "Executable", process.cwd());
|
|
148
|
+
if (process.platform === "win32") {
|
|
149
|
+
const lower = validatedExecutable.toLowerCase();
|
|
150
|
+
if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
|
|
151
|
+
validateWindowsBatchExecutableForCmd(validatedExecutable, "Windows batch executable");
|
|
152
|
+
return { executable: "cmd.exe", args: ["/d", "/s", "/c", "call", validatedExecutable, ...args] };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return { executable: validatedExecutable, args };
|
|
156
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/core/inputDebug.ts
CHANGED
|
File without changes
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { statSync } from "fs";
|
|
2
2
|
import { basename, join, resolve } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
|
-
import { resolveWorkspacePath } from "./workspaceGuard.js";
|
|
5
|
-
import { normalizeWorkspaceRoot } from "./workspaceRoot.js";
|
|
4
|
+
import { resolveWorkspacePath } from "./workspaceGuard.js";
|
|
5
|
+
import { normalizeWorkspaceRoot } from "./workspaceRoot.js";
|
|
6
|
+
import { CODEXA_CHANNEL_ENV, LOCAL_DEV_CHANNEL, isLocalDevChannel } from "./channel.js";
|
|
6
7
|
|
|
7
8
|
export type LaunchKind = "installed-bin" | "dev-run";
|
|
8
9
|
|
|
@@ -46,9 +47,9 @@ const ENV_KEYS = {
|
|
|
46
47
|
launchKind: "CODEXA_LAUNCH_KIND",
|
|
47
48
|
packageRoot: "CODEXA_PACKAGE_ROOT",
|
|
48
49
|
launcherScript: "CODEXA_LAUNCHER_SCRIPT",
|
|
49
|
-
relaunchExecutable: "CODEXA_RELAUNCH_EXECUTABLE",
|
|
50
|
-
relaunchArgs: "CODEXA_RELAUNCH_ARGS",
|
|
51
|
-
} as const;
|
|
50
|
+
relaunchExecutable: "CODEXA_RELAUNCH_EXECUTABLE",
|
|
51
|
+
relaunchArgs: "CODEXA_RELAUNCH_ARGS",
|
|
52
|
+
} as const;
|
|
52
53
|
|
|
53
54
|
function getDefaultPackageRoot(): string {
|
|
54
55
|
return normalizeWorkspaceRoot(resolve(fileURLToPath(new URL(".", import.meta.url)), "..", ".."));
|
|
@@ -153,18 +154,17 @@ export function buildWorkspaceStatusMessage(launchContext: LaunchContext): strin
|
|
|
153
154
|
"",
|
|
154
155
|
];
|
|
155
156
|
|
|
156
|
-
if (launchContext.launchKind === "dev-run") {
|
|
157
|
-
lines.push(
|
|
158
|
-
"This session was started from a
|
|
159
|
-
"Recommended
|
|
160
|
-
|
|
161
|
-
"
|
|
162
|
-
"
|
|
163
|
-
"
|
|
164
|
-
"
|
|
165
|
-
"",
|
|
166
|
-
"
|
|
167
|
-
" /workspace relaunch <path>",
|
|
157
|
+
if (launchContext.launchKind === "dev-run") {
|
|
158
|
+
lines.push(
|
|
159
|
+
"This session was started from a local dev launch.",
|
|
160
|
+
"Recommended setup:",
|
|
161
|
+
" npm run install:dev-bin",
|
|
162
|
+
" which codexa-dev",
|
|
163
|
+
" cd <target-folder>",
|
|
164
|
+
" codexa-dev",
|
|
165
|
+
"",
|
|
166
|
+
"Quick recovery from here:",
|
|
167
|
+
" /workspace relaunch <path>",
|
|
168
168
|
);
|
|
169
169
|
} else {
|
|
170
170
|
lines.push(
|
|
@@ -181,17 +181,16 @@ export function buildDevLaunchNotice(launchContext: LaunchContext): string | nul
|
|
|
181
181
|
return null;
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
-
return [
|
|
185
|
-
"This session was started from
|
|
186
|
-
"Normal
|
|
187
|
-
|
|
188
|
-
"
|
|
189
|
-
"
|
|
190
|
-
"
|
|
191
|
-
"
|
|
192
|
-
"",
|
|
193
|
-
"
|
|
194
|
-
" /workspace relaunch <path>",
|
|
184
|
+
return [
|
|
185
|
+
"This session was started from local-dev Codexa.",
|
|
186
|
+
"Normal flow:",
|
|
187
|
+
" npm run install:dev-bin",
|
|
188
|
+
" which codexa-dev",
|
|
189
|
+
" cd <target-folder>",
|
|
190
|
+
" codexa-dev",
|
|
191
|
+
"",
|
|
192
|
+
"Recovery from this session:",
|
|
193
|
+
" /workspace relaunch <path>",
|
|
195
194
|
].join("\n");
|
|
196
195
|
}
|
|
197
196
|
|
|
@@ -216,11 +215,12 @@ function buildRelaunchEnv(
|
|
|
216
215
|
[ENV_KEYS.workspaceRoot]: targetWorkspaceRoot,
|
|
217
216
|
[ENV_KEYS.launchKind]: launchContext.launchKind,
|
|
218
217
|
[ENV_KEYS.packageRoot]: launchContext.packageRoot,
|
|
219
|
-
[ENV_KEYS.launcherScript]: launchContext.launcherScriptPath ?? "",
|
|
220
|
-
[ENV_KEYS.relaunchExecutable]: launchContext.relaunchExecutable,
|
|
221
|
-
[ENV_KEYS.relaunchArgs]: JSON.stringify(launchContext.relaunchArgs),
|
|
222
|
-
|
|
223
|
-
}
|
|
218
|
+
[ENV_KEYS.launcherScript]: launchContext.launcherScriptPath ?? "",
|
|
219
|
+
[ENV_KEYS.relaunchExecutable]: launchContext.relaunchExecutable,
|
|
220
|
+
[ENV_KEYS.relaunchArgs]: JSON.stringify(launchContext.relaunchArgs),
|
|
221
|
+
[CODEXA_CHANNEL_ENV]: isLocalDevChannel(baseEnv) ? LOCAL_DEV_CHANNEL : baseEnv[CODEXA_CHANNEL_ENV],
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
224
|
|
|
225
225
|
export function createWorkspaceRelaunchPlan(
|
|
226
226
|
targetInput: string,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/core/planStorage.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -23,10 +23,14 @@ function stripBalancedWrappingQuotes(value: string): string {
|
|
|
23
23
|
return value;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function looksLikePath(value: string): boolean {
|
|
27
|
-
return /[\\/]/.test(value) || /^[A-Za-z]:/.test(value);
|
|
28
|
-
}
|
|
29
|
-
|
|
26
|
+
function looksLikePath(value: string): boolean {
|
|
27
|
+
return /[\\/]/.test(value) || /^[A-Za-z]:/.test(value);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isWindowsAbsolutePath(value: string): boolean {
|
|
31
|
+
return /^[A-Za-z]:[\\/]/.test(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
function hasWindowsBatchExtension(value: string): boolean {
|
|
31
35
|
return /\.(?:cmd|bat)$/i.test(value);
|
|
32
36
|
}
|
|
@@ -67,7 +71,7 @@ export function normalizeExecutableValue(
|
|
|
67
71
|
return trimmed;
|
|
68
72
|
}
|
|
69
73
|
|
|
70
|
-
const normalized = isAbsolute(trimmed)
|
|
74
|
+
const normalized = isAbsolute(trimmed) || isWindowsAbsolutePath(trimmed) ? trimmed : resolve(cwd, trimmed);
|
|
71
75
|
validateCommonExecutableSyntax(normalized, options.label);
|
|
72
76
|
|
|
73
77
|
if (requireExistingPath && !existsSync(normalized)) {
|
|
File without changes
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { spawn, type ChildProcess } from "child_process";
|
|
2
|
-
import { existsSync } from "fs";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
1
|
+
import { spawn, type ChildProcess } from "child_process";
|
|
2
|
+
import { accessSync, constants, existsSync } from "fs";
|
|
3
|
+
import { delimiter, join } from "path";
|
|
4
|
+
import { buildSpawnSpec } from "../executables/executableResolver.js";
|
|
5
|
+
import { normalizeExecutableValue } from "../process/processValidation.js";
|
|
6
|
+
import type { ProviderConfig, ProviderLaunchCommand } from "./types.js";
|
|
6
7
|
|
|
7
8
|
export interface ProviderLaunchSpec {
|
|
8
9
|
executable: string;
|
|
@@ -75,43 +76,55 @@ function setRawMode(stdin: RawModeStream | null | undefined, enabled: boolean):
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
function executableLooksLikePath(executable: string): boolean {
|
|
79
|
-
return /[\\/]/.test(executable) || /^[A-Za-z]:/.test(executable);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
79
|
+
function executableLooksLikePath(executable: string): boolean {
|
|
80
|
+
return /[\\/]/.test(executable) || /^[A-Za-z]:/.test(executable);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function canAccessExecutable(path: string): boolean {
|
|
84
|
+
try {
|
|
85
|
+
if (process.platform === "win32") {
|
|
86
|
+
return existsSync(path);
|
|
87
|
+
}
|
|
88
|
+
accessSync(path, constants.X_OK);
|
|
89
|
+
return true;
|
|
90
|
+
} catch {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function getPathExecutableCandidates(executable: string): string[] {
|
|
96
|
+
if (process.platform !== "win32" || /\.[A-Za-z0-9_-]+$/.test(executable)) {
|
|
97
|
+
return [executable];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const pathExt = process.env.PATHEXT?.split(";").filter(Boolean) ?? [".COM", ".EXE", ".BAT", ".CMD"];
|
|
101
|
+
return [executable, ...pathExt.map((ext) => `${executable}${ext.toLowerCase()}`)];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function commandExistsOnPath(executable: string): Promise<boolean> {
|
|
105
|
+
let candidate: string;
|
|
106
|
+
try {
|
|
107
|
+
candidate = normalizeExecutableValue(executable, {
|
|
108
|
+
label: "Provider launch command",
|
|
109
|
+
allowBareExecutable: true,
|
|
110
|
+
});
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const trimmed = candidate.trim();
|
|
116
|
+
if (executableLooksLikePath(trimmed)) {
|
|
117
|
+
return canAccessExecutable(trimmed);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const pathEntries = process.env.PATH?.split(delimiter).filter(Boolean) ?? [];
|
|
121
|
+
for (const dir of pathEntries) {
|
|
122
|
+
for (const name of getPathExecutableCandidates(trimmed)) {
|
|
123
|
+
if (canAccessExecutable(join(dir, name))) return true;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
115
128
|
|
|
116
129
|
function formatSpawnError(provider: ProviderConfig, executable: string, error: NodeJS.ErrnoException): ProviderLaunchResult {
|
|
117
130
|
if (error.code === "ENOENT") {
|
|
@@ -162,6 +175,10 @@ export async function launchProviderCli(
|
|
|
162
175
|
let child: ChildProcess;
|
|
163
176
|
try {
|
|
164
177
|
const spawnSpec = buildSpawnSpec(spec.executable, spec.args);
|
|
178
|
+
if (!spawnSpec.executable) {
|
|
179
|
+
resolve({ status: "spawn-error", message: "Executable path is empty after resolution." });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
165
182
|
child = spawnImpl(spawnSpec.executable, spawnSpec.args, {
|
|
166
183
|
cwd: spec.cwd,
|
|
167
184
|
shell: false,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface StartupClearOptions {
|
|
2
|
+
write: (chunk: string) => boolean | void;
|
|
3
|
+
/** True when --no-clear was passed on the CLI. */
|
|
4
|
+
noClear: boolean;
|
|
5
|
+
env: Record<string, string | undefined>;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Writes a full terminal clear (viewport + scrollback + cursor home) to stdout
|
|
10
|
+
* before the first Ink render. Skipped when --no-clear or CODEXA_NO_CLEAR=1.
|
|
11
|
+
*
|
|
12
|
+
* Only call this from startApp() in TTY mode — never inside render loops.
|
|
13
|
+
*/
|
|
14
|
+
export function performStartupClear(options: StartupClearOptions): void {
|
|
15
|
+
if (options.noClear || options.env["CODEXA_NO_CLEAR"] === "1") return;
|
|
16
|
+
// \x1b[2J: clear visible viewport
|
|
17
|
+
// \x1b[3J: clear scrollback buffer
|
|
18
|
+
// \x1b[H: move cursor to top-left
|
|
19
|
+
options.write("\x1b[2J\x1b[3J\x1b[H");
|
|
20
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|