remote-codex 0.11.43 → 0.11.45
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 +9 -0
- package/apps/relay-server/dist/index.js +17 -1
- package/apps/supervisor-api/dist/index.js +2189 -1798
- package/apps/supervisor-web/dist/assets/index-BO9S3vTX.css +1 -0
- package/apps/supervisor-web/dist/assets/index-GqVDOqbI.js +22 -0
- package/apps/supervisor-web/dist/assets/{thread-ui-Dmrigdek.js → thread-ui-BWC_ljvN.js} +11 -11
- package/apps/supervisor-web/dist/index.html +3 -3
- package/bin/remote-codex.mjs +426 -35
- package/docs/windows.md +81 -0
- package/package.json +14 -3
- package/packages/claude/src/runtimeAdapter.test.ts +32 -4
- package/packages/claude/src/runtimeAdapter.ts +21 -22
- package/packages/codex/src/appServerManager.test.ts +47 -0
- package/packages/codex/src/appServerManager.ts +6 -2
- package/packages/codex/src/runtimeAdapter.test.ts +9 -2
- package/packages/opencode/src/historyItems.ts +7 -6
- package/packages/opencode/src/runtimeAdapter.ts +7 -11
- package/packages/process-runtime/src/index.test.ts +132 -0
- package/packages/process-runtime/src/index.ts +253 -0
- package/packages/shared/src/index.ts +12 -0
- package/scripts/service-manager.mjs +112 -4
- package/scripts/verify-relay-supervisor-smoke.mjs +262 -0
- package/scripts/windows/install-relay-supervisor-task.ps1 +44 -0
- package/scripts/windows/relay-smoke.ps1 +16 -0
- package/scripts/windows/uninstall-relay-supervisor-task.ps1 +27 -0
- package/scripts/windows/validate-real-codex.mjs +680 -0
- package/apps/supervisor-web/dist/assets/index-BT0SM9C-.js +0 -21
- package/apps/supervisor-web/dist/assets/index-BcCLYWAf.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remote-codex",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.45",
|
|
4
4
|
"description": "Local web supervisor for Codex workspaces and threads.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
"bin/",
|
|
12
12
|
"scripts/service-manager.mjs",
|
|
13
13
|
"scripts/run-web-service.mjs",
|
|
14
|
+
"scripts/verify-relay-supervisor-smoke.mjs",
|
|
15
|
+
"scripts/windows/",
|
|
14
16
|
"apps/supervisor-api/dist/",
|
|
15
17
|
"apps/relay-server/dist/",
|
|
16
18
|
"apps/supervisor-web/dist/",
|
|
@@ -19,12 +21,14 @@
|
|
|
19
21
|
"packages/claude/src/",
|
|
20
22
|
"packages/codex/src/",
|
|
21
23
|
"packages/opencode/src/",
|
|
24
|
+
"packages/process-runtime/src/",
|
|
22
25
|
"packages/db/src/",
|
|
23
26
|
"packages/db/migrations/*.sql",
|
|
24
27
|
"packages/plugin-terminal/package.json",
|
|
25
28
|
"packages/plugin-terminal/plugin.json",
|
|
26
29
|
"packages/plugin-terminal/src/",
|
|
27
30
|
"packages/shared/src/",
|
|
31
|
+
"docs/windows.md",
|
|
28
32
|
"README.md"
|
|
29
33
|
],
|
|
30
34
|
"packageManager": "pnpm@10.11.1",
|
|
@@ -51,8 +55,10 @@
|
|
|
51
55
|
"build:package": "pnpm --filter @remote-codex/supervisor-api build && pnpm --filter @remote-codex/relay-server build && pnpm --filter @remote-codex/supervisor-web build",
|
|
52
56
|
"lint": "pnpm -r --if-present lint",
|
|
53
57
|
"typecheck": "pnpm -r --if-present typecheck",
|
|
54
|
-
"test": "NODE_ENV=test pnpm -r --if-present test",
|
|
58
|
+
"test": "cross-env NODE_ENV=test pnpm -r --if-present test",
|
|
55
59
|
"test:e2e": "playwright test",
|
|
60
|
+
"verify:package": "node scripts/verify-package-install.mjs",
|
|
61
|
+
"verify:relay": "node scripts/verify-relay-supervisor-smoke.mjs",
|
|
56
62
|
"db:migrate": "pnpm --filter @remote-codex/db db:migrate",
|
|
57
63
|
"release:mobile": "node scripts/publish-mobile-release-assets.mjs",
|
|
58
64
|
"impeccable": "impeccable",
|
|
@@ -65,14 +71,17 @@
|
|
|
65
71
|
"@fastify/websocket": "^11.0.2",
|
|
66
72
|
"@fontsource/noto-sans": "^5.2.10",
|
|
67
73
|
"@fontsource/noto-sans-sc": "^5.2.9",
|
|
68
|
-
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
|
|
69
74
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
70
75
|
"better-sqlite3": "^12.10.0",
|
|
76
|
+
"cross-spawn": "^7.0.6",
|
|
71
77
|
"fastify": "^5.2.1",
|
|
72
78
|
"marked": "^18.0.3",
|
|
73
79
|
"puppeteer-core": "^24.43.1",
|
|
74
80
|
"zod": "^4.4.3"
|
|
75
81
|
},
|
|
82
|
+
"optionalDependencies": {
|
|
83
|
+
"@homebridge/node-pty-prebuilt-multiarch": "^0.13.1"
|
|
84
|
+
},
|
|
76
85
|
"devDependencies": {
|
|
77
86
|
"@eslint/js": "^9.24.0",
|
|
78
87
|
"@playwright/test": "^1.59.1",
|
|
@@ -80,11 +89,13 @@
|
|
|
80
89
|
"@testing-library/jest-dom": "^6.6.3",
|
|
81
90
|
"@testing-library/react": "^16.3.0",
|
|
82
91
|
"@types/better-sqlite3": "^7.6.13",
|
|
92
|
+
"@types/cross-spawn": "^6.0.6",
|
|
83
93
|
"@types/node": "^22.14.0",
|
|
84
94
|
"@types/react": "^19.1.2",
|
|
85
95
|
"@types/react-dom": "^19.1.2",
|
|
86
96
|
"@vitejs/plugin-react": "^4.3.4",
|
|
87
97
|
"concurrently": "^9.1.2",
|
|
98
|
+
"cross-env": "^10.1.0",
|
|
88
99
|
"eslint": "^9.24.0",
|
|
89
100
|
"eslint-plugin-react-hooks": "^5.2.0",
|
|
90
101
|
"eslint-plugin-react-refresh": "^0.4.19",
|
|
@@ -94,6 +94,22 @@ class FakeQuery implements Query {
|
|
|
94
94
|
supportsEffort: true,
|
|
95
95
|
supportedEffortLevels: ['low', 'medium', 'high'],
|
|
96
96
|
},
|
|
97
|
+
{
|
|
98
|
+
value: 'claude-fable-5[1m]',
|
|
99
|
+
resolvedModel: 'claude-fable-5',
|
|
100
|
+
displayName: 'Fable',
|
|
101
|
+
description: 'Fable 5 test model',
|
|
102
|
+
supportsEffort: true,
|
|
103
|
+
supportedEffortLevels: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
value: 'opus[1m]',
|
|
107
|
+
resolvedModel: 'claude-opus-5[1m]',
|
|
108
|
+
displayName: 'Opus (1M context)',
|
|
109
|
+
description: 'Opus 5 test model',
|
|
110
|
+
supportsEffort: true,
|
|
111
|
+
supportedEffortLevels: ['low', 'medium', 'high', 'xhigh', 'max'],
|
|
112
|
+
},
|
|
97
113
|
];
|
|
98
114
|
}
|
|
99
115
|
async supportedAgents(): Promise<any[]> {
|
|
@@ -832,16 +848,28 @@ describe('ClaudeRuntimeAdapter', () => {
|
|
|
832
848
|
}),
|
|
833
849
|
expect.objectContaining({
|
|
834
850
|
model: 'fable',
|
|
835
|
-
displayName: '
|
|
851
|
+
displayName: 'Fable · 5',
|
|
836
852
|
defaultReasoningEffort: 'medium',
|
|
837
853
|
supportedReasoningEfforts: expect.arrayContaining([
|
|
838
854
|
expect.objectContaining({ reasoningEffort: 'max' }),
|
|
839
855
|
]),
|
|
840
856
|
}),
|
|
857
|
+
expect.objectContaining({
|
|
858
|
+
model: 'opus',
|
|
859
|
+
displayName: 'Opus · 5',
|
|
860
|
+
}),
|
|
861
|
+
expect.objectContaining({
|
|
862
|
+
model: 'opus[1m]',
|
|
863
|
+
displayName: 'Opus · 5 (1M context)',
|
|
864
|
+
}),
|
|
865
|
+
]),
|
|
866
|
+
);
|
|
867
|
+
await expect(adapter.listModels()).resolves.not.toEqual(
|
|
868
|
+
expect.arrayContaining([
|
|
869
|
+
expect.objectContaining({ model: 'claude-fable-5[1m]' }),
|
|
841
870
|
]),
|
|
842
871
|
);
|
|
843
872
|
expect(queries[0]?.closed).toBe(true);
|
|
844
|
-
await adapter.listModels();
|
|
845
873
|
expect(queries).toHaveLength(1);
|
|
846
874
|
|
|
847
875
|
await adapter.startSession({
|
|
@@ -1238,7 +1266,7 @@ describe('ClaudeRuntimeAdapter', () => {
|
|
|
1238
1266
|
message: {
|
|
1239
1267
|
role: 'user',
|
|
1240
1268
|
content:
|
|
1241
|
-
'<task-notification>\\n<task-id>task-1</task-id>\\n<tool-use-id>toolu_agent_xml</tool-use-id>\\n<status>completed</status>\\n<summary>Agent
|
|
1269
|
+
'<task-notification>\\n<task-id>task-1</task-id>\\n<tool-use-id>toolu_agent_xml</tool-use-id>\\n<status>completed</status>\\n<summary>Agent "Audit docs" finished</summary>\\n<result>Found one stale runbook.</result>\\n</task-notification>',
|
|
1242
1270
|
},
|
|
1243
1271
|
parent_tool_use_id: null,
|
|
1244
1272
|
uuid: '00000000-0000-4000-8000-000000000042' as any,
|
|
@@ -2352,7 +2380,7 @@ describe('ClaudeRuntimeAdapter', () => {
|
|
|
2352
2380
|
message: {
|
|
2353
2381
|
role: 'user',
|
|
2354
2382
|
content:
|
|
2355
|
-
'<task-notification>\\n<task-id>task-a</task-id>\\n<tool-use-id>toolu_task_a</tool-use-id>\\n<status>completed</status>\\n<summary>Agent
|
|
2383
|
+
'<task-notification>\\n<task-id>task-a</task-id>\\n<tool-use-id>toolu_task_a</tool-use-id>\\n<status>completed</status>\\n<summary>Agent "Audit infra docs" finished</summary>\\n<result>Infra docs need one migration checklist.</result>\\n</task-notification>',
|
|
2356
2384
|
},
|
|
2357
2385
|
parent_tool_use_id: null,
|
|
2358
2386
|
},
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
|
-
import { execFile } from 'node:child_process';
|
|
4
3
|
import fs from 'node:fs/promises';
|
|
5
4
|
import { createRequire } from 'node:module';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { pathToFileURL } from 'node:url';
|
|
8
|
-
import { promisify } from 'node:util';
|
|
9
7
|
|
|
10
8
|
import type {
|
|
11
9
|
AgentActionQuestion,
|
|
@@ -58,8 +56,9 @@ import {
|
|
|
58
56
|
userMessageHistoryItem,
|
|
59
57
|
userMessageToHistoryItem,
|
|
60
58
|
} from './historyItems';
|
|
59
|
+
import { runProcess } from '../../process-runtime/src/index';
|
|
60
|
+
import { isPathInside } from '../../workspace/src/index';
|
|
61
61
|
|
|
62
|
-
const execFileAsync = promisify(execFile);
|
|
63
62
|
|
|
64
63
|
type ClaudePromptInput = string | AsyncIterable<SDKUserMessage>;
|
|
65
64
|
type ClaudeMessageContent =
|
|
@@ -379,8 +378,7 @@ function resolvePromptAssetPath(assetPath: string, cwd: string | null | undefine
|
|
|
379
378
|
const resolvedPath = path.isAbsolute(assetPath)
|
|
380
379
|
? path.normalize(assetPath)
|
|
381
380
|
: path.resolve(cwd, assetPath);
|
|
382
|
-
|
|
383
|
-
if (relativePath === '' || (!relativePath.startsWith('..') && !path.isAbsolute(relativePath))) {
|
|
381
|
+
if (isPathInside(cwd, resolvedPath)) {
|
|
384
382
|
return resolvedPath;
|
|
385
383
|
}
|
|
386
384
|
return null;
|
|
@@ -760,9 +758,12 @@ function versionedAlias(
|
|
|
760
758
|
}
|
|
761
759
|
|
|
762
760
|
function withClaudeCodeModelAliases(models: AgentModel[]) {
|
|
763
|
-
const
|
|
764
|
-
const
|
|
765
|
-
const
|
|
761
|
+
const discoveredSonnet = models.find((model) => /^Sonnet · /.test(model.displayName));
|
|
762
|
+
const discoveredFable = models.find((model) => /^Fable · /.test(model.displayName));
|
|
763
|
+
const discoveredOpus = models.find((model) => /^Opus · /.test(model.displayName));
|
|
764
|
+
const output = models.filter(
|
|
765
|
+
(model) => model !== discoveredFable || discoveredFable.model === 'fable',
|
|
766
|
+
);
|
|
766
767
|
const defaultSonnet = versionedAlias(DEFAULT_CLAUDE_MODELS[0]!, discoveredSonnet);
|
|
767
768
|
const oneMillionSonnet = versionedAlias(
|
|
768
769
|
DEFAULT_CLAUDE_MODELS[1]!,
|
|
@@ -770,6 +771,7 @@ function withClaudeCodeModelAliases(models: AgentModel[]) {
|
|
|
770
771
|
' (1M context)',
|
|
771
772
|
);
|
|
772
773
|
const fable = versionedAlias(DEFAULT_CLAUDE_MODELS[2]!, discoveredFable);
|
|
774
|
+
const opus = versionedAlias(DEFAULT_CLAUDE_MODELS[3]!, discoveredOpus);
|
|
773
775
|
const hasSonnetAlias = output.some((model) => model.model === 'sonnet');
|
|
774
776
|
if (!hasSonnetAlias) {
|
|
775
777
|
output.unshift(defaultSonnet);
|
|
@@ -780,6 +782,10 @@ function withClaudeCodeModelAliases(models: AgentModel[]) {
|
|
|
780
782
|
if (!output.some((model) => model.model === 'fable')) {
|
|
781
783
|
output.splice(2, 0, fable);
|
|
782
784
|
}
|
|
785
|
+
if (!output.some((model) => model.model === 'opus')) {
|
|
786
|
+
const discoveredOpusIndex = output.findIndex((model) => /^Opus · /.test(model.displayName));
|
|
787
|
+
output.splice(discoveredOpusIndex >= 0 ? discoveredOpusIndex : output.length, 0, opus);
|
|
788
|
+
}
|
|
783
789
|
return output.map((model, index) => ({
|
|
784
790
|
...model,
|
|
785
791
|
isDefault: index === 0,
|
|
@@ -2558,12 +2564,7 @@ export class ClaudeRuntimeAdapter extends EventEmitter implements AgentRuntime {
|
|
|
2558
2564
|
|
|
2559
2565
|
const relativePath = `./.temp/threads/${input.localThreadId}/claude-history-${safeAssetFilePart(input.messageId)}-${input.blockIndex}.${extension}`;
|
|
2560
2566
|
const targetPath = path.resolve(input.workspacePath, relativePath);
|
|
2561
|
-
|
|
2562
|
-
if (
|
|
2563
|
-
relativeToWorkspace === '' ||
|
|
2564
|
-
relativeToWorkspace.startsWith('..') ||
|
|
2565
|
-
path.isAbsolute(relativeToWorkspace)
|
|
2566
|
-
) {
|
|
2567
|
+
if (!isPathInside(input.workspacePath, targetPath)) {
|
|
2567
2568
|
return null;
|
|
2568
2569
|
}
|
|
2569
2570
|
|
|
@@ -2825,12 +2826,10 @@ async function importOptionalPackage(specifier: string) {
|
|
|
2825
2826
|
}
|
|
2826
2827
|
|
|
2827
2828
|
async function npmGlobalRoot() {
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
return null;
|
|
2835
|
-
}
|
|
2829
|
+
const result = await runProcess({
|
|
2830
|
+
command: 'npm',
|
|
2831
|
+
args: ['root', '-g'],
|
|
2832
|
+
timeoutMs: 3_000,
|
|
2833
|
+
});
|
|
2834
|
+
return result.code === 0 ? result.stdout.trim() || null : null;
|
|
2836
2835
|
}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
3
6
|
import { PassThrough, Writable } from 'node:stream';
|
|
4
7
|
|
|
5
8
|
import { describe, expect, it } from 'vitest';
|
|
@@ -80,6 +83,8 @@ class ScriptedChild extends EventEmitter {
|
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
describe('CodexAppServerManager', () => {
|
|
86
|
+
const windowsIt = it.runIf(process.platform === 'win32');
|
|
87
|
+
|
|
83
88
|
it('starts against a newline-delimited JSON-RPC process', async () => {
|
|
84
89
|
const script = [
|
|
85
90
|
"const readline=require('node:readline');",
|
|
@@ -117,6 +122,48 @@ describe('CodexAppServerManager', () => {
|
|
|
117
122
|
await manager.stop();
|
|
118
123
|
});
|
|
119
124
|
|
|
125
|
+
windowsIt('starts an npm-style Codex command shim from a path with spaces', async () => {
|
|
126
|
+
const directory = await fs.mkdtemp(
|
|
127
|
+
path.join(os.tmpdir(), 'remote-codex app server shim '),
|
|
128
|
+
);
|
|
129
|
+
const scriptPath = path.join(directory, 'fake-codex.cjs');
|
|
130
|
+
const shimPath = path.join(directory, 'fake-codex.cmd');
|
|
131
|
+
const script = [
|
|
132
|
+
"const readline = require('node:readline');",
|
|
133
|
+
'const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });',
|
|
134
|
+
"rl.on('line', (line) => {",
|
|
135
|
+
' const message = JSON.parse(line);',
|
|
136
|
+
" if (message.method === 'initialize') {",
|
|
137
|
+
" process.stdout.write(JSON.stringify({ id: message.id, result: { userAgent: 'fake-cmd', codexHome: 'C:/tmp', platformFamily: 'windows', platformOs: 'windows' } }) + '\\n');",
|
|
138
|
+
' }',
|
|
139
|
+
'});',
|
|
140
|
+
].join('\n');
|
|
141
|
+
|
|
142
|
+
await fs.writeFile(scriptPath, script);
|
|
143
|
+
await fs.writeFile(
|
|
144
|
+
shimPath,
|
|
145
|
+
`@echo off\r\n"${process.execPath}" "%~dp0fake-codex.cjs" %*\r\n`,
|
|
146
|
+
);
|
|
147
|
+
|
|
148
|
+
const manager = new CodexAppServerManager({
|
|
149
|
+
command: shimPath,
|
|
150
|
+
startupTimeoutMs: 3_000,
|
|
151
|
+
clientInfo: {
|
|
152
|
+
name: 'test',
|
|
153
|
+
title: 'test',
|
|
154
|
+
version: '0.1.0',
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await manager.start();
|
|
160
|
+
expect(manager.getStatus().state).toBe('ready');
|
|
161
|
+
} finally {
|
|
162
|
+
await manager.stop();
|
|
163
|
+
await fs.rm(directory, { recursive: true, force: true });
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
120
167
|
it('surfaces spawn failures as a failed status instead of crashing', async () => {
|
|
121
168
|
class FailingChild extends EventEmitter {
|
|
122
169
|
stdout = new PassThrough();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { ChildProcessWithoutNullStreams
|
|
1
|
+
import { ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
2
|
import { EventEmitter } from 'node:events';
|
|
3
3
|
|
|
4
|
+
import { spawnProcess as spawnCrossPlatformProcess } from '../../process-runtime/src/index';
|
|
5
|
+
|
|
4
6
|
import { JsonRpcClient, JsonRpcClientError } from './jsonrpc';
|
|
5
7
|
import {
|
|
6
8
|
AppServerStatusSnapshot,
|
|
@@ -250,7 +252,9 @@ export class CodexAppServerManager extends EventEmitter {
|
|
|
250
252
|
this.spawnProcess =
|
|
251
253
|
options.spawnProcess ??
|
|
252
254
|
((command: string, args: string[]) =>
|
|
253
|
-
|
|
255
|
+
spawnCrossPlatformProcess({
|
|
256
|
+
command,
|
|
257
|
+
args,
|
|
254
258
|
stdio: 'pipe'
|
|
255
259
|
}) as unknown as ChildProcessWithoutNullStreams);
|
|
256
260
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
+
import path from 'node:path';
|
|
2
3
|
|
|
3
4
|
import { describe, expect, it } from 'vitest';
|
|
4
5
|
|
|
@@ -120,7 +121,10 @@ describe('CodexRuntimeAdapter', () => {
|
|
|
120
121
|
{ type: 'text', text: 'Inspect this ', text_elements: [] },
|
|
121
122
|
{
|
|
122
123
|
type: 'localImage',
|
|
123
|
-
path:
|
|
124
|
+
path: path.join(
|
|
125
|
+
path.resolve('/tmp/workspace'),
|
|
126
|
+
'.temp/threads/thread-1/photo.png',
|
|
127
|
+
),
|
|
124
128
|
},
|
|
125
129
|
{ type: 'text', text: ' then summarize.', text_elements: [] },
|
|
126
130
|
],
|
|
@@ -141,7 +145,10 @@ describe('CodexRuntimeAdapter', () => {
|
|
|
141
145
|
{ type: 'text', text: 'Inspect ', text_elements: [] },
|
|
142
146
|
{
|
|
143
147
|
type: 'localImage',
|
|
144
|
-
path:
|
|
148
|
+
path: path.join(
|
|
149
|
+
path.resolve('/tmp/workspace'),
|
|
150
|
+
'.temp/threads/thread-1/photo.heic',
|
|
151
|
+
),
|
|
145
152
|
},
|
|
146
153
|
{ type: 'text', text: '.', text_elements: [] },
|
|
147
154
|
]);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
|
|
3
|
+
import { isPathInside } from '../../workspace/src/index';
|
|
4
|
+
|
|
3
5
|
import type {
|
|
4
6
|
AgentHistoryItem,
|
|
5
7
|
AgentTurn,
|
|
@@ -163,15 +165,14 @@ function displayPath(pathValue: string | null, options: OpenCodeHistoryItemMappi
|
|
|
163
165
|
|
|
164
166
|
const root = path.resolve(options.workspacePath);
|
|
165
167
|
const absolutePath = path.resolve(pathValue);
|
|
168
|
+
if (!isPathInside(root, absolutePath)) {
|
|
169
|
+
return pathValue;
|
|
170
|
+
}
|
|
166
171
|
const relativePath = path.relative(root, absolutePath);
|
|
167
|
-
if (
|
|
168
|
-
!relativePath ||
|
|
169
|
-
relativePath.startsWith('..') ||
|
|
170
|
-
path.isAbsolute(relativePath)
|
|
171
|
-
) {
|
|
172
|
+
if (!relativePath) {
|
|
172
173
|
return pathValue;
|
|
173
174
|
}
|
|
174
|
-
return relativePath;
|
|
175
|
+
return relativePath.split(path.sep).join('/');
|
|
175
176
|
}
|
|
176
177
|
|
|
177
178
|
function toolIsLowInformationPatch(
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events';
|
|
2
|
-
import { execFile } from 'node:child_process';
|
|
3
2
|
import fs from 'node:fs/promises';
|
|
4
3
|
import net from 'node:net';
|
|
5
4
|
import { createRequire } from 'node:module';
|
|
6
5
|
import path from 'node:path';
|
|
7
6
|
import { pathToFileURL } from 'node:url';
|
|
8
|
-
import { promisify } from 'node:util';
|
|
9
7
|
|
|
10
8
|
import type {
|
|
11
9
|
AgentHistoryItem,
|
|
@@ -37,8 +35,8 @@ import {
|
|
|
37
35
|
openCodeMessageToHistoryItems,
|
|
38
36
|
openCodeMessagesToPlanUpdate,
|
|
39
37
|
} from './historyItems';
|
|
38
|
+
import { runProcess } from '../../process-runtime/src/index';
|
|
40
39
|
|
|
41
|
-
const execFileAsync = promisify(execFile);
|
|
42
40
|
const openCodeWaitTimeoutMs = 1_500;
|
|
43
41
|
const openCodePromptPollIntervalMs = 500;
|
|
44
42
|
const openCodePromptTimeoutMs = 120_000;
|
|
@@ -1476,12 +1474,10 @@ function resolveOptionalPackage(
|
|
|
1476
1474
|
}
|
|
1477
1475
|
|
|
1478
1476
|
async function npmGlobalRoot() {
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
return null;
|
|
1486
|
-
}
|
|
1477
|
+
const result = await runProcess({
|
|
1478
|
+
command: 'npm',
|
|
1479
|
+
args: ['root', '-g'],
|
|
1480
|
+
timeoutMs: 3_000,
|
|
1481
|
+
});
|
|
1482
|
+
return result.code === 0 ? result.stdout.trim() || null : null;
|
|
1487
1483
|
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
import { parseCommandLine, resolveExecutable, runProcess } from './index';
|
|
7
|
+
|
|
8
|
+
const temporaryDirectories: string[] = [];
|
|
9
|
+
|
|
10
|
+
afterEach(async () => {
|
|
11
|
+
await Promise.all(
|
|
12
|
+
temporaryDirectories.splice(0).map((directory) =>
|
|
13
|
+
fs.rm(directory, { recursive: true, force: true }),
|
|
14
|
+
),
|
|
15
|
+
);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
describe('runProcess', () => {
|
|
19
|
+
const windowsIt = it.runIf(process.platform === 'win32');
|
|
20
|
+
|
|
21
|
+
it('preserves argv values without shell interpretation', async () => {
|
|
22
|
+
const argument = 'space & (parentheses) 中文';
|
|
23
|
+
const result = await runProcess({
|
|
24
|
+
command: process.execPath,
|
|
25
|
+
args: ['-e', 'process.stdout.write(process.argv[1])', argument],
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(result.code).toBe(0);
|
|
29
|
+
expect(result.stdout).toBe(argument);
|
|
30
|
+
expect(result.spawnError).toBeNull();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('returns stderr and a non-zero exit code', async () => {
|
|
34
|
+
const result = await runProcess({
|
|
35
|
+
command: process.execPath,
|
|
36
|
+
args: ['-e', "process.stderr.write('failure'); process.exit(7)"],
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(result.code).toBe(7);
|
|
40
|
+
expect(result.stderr).toBe('failure');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('reports a missing executable', async () => {
|
|
44
|
+
const result = await runProcess({
|
|
45
|
+
command: `remote-codex-missing-${Date.now()}`,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
expect(result.code).toBeNull();
|
|
49
|
+
expect(result.spawnError).not.toBeNull();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('terminates a timed out process', async () => {
|
|
53
|
+
const result = await runProcess({
|
|
54
|
+
command: process.execPath,
|
|
55
|
+
args: ['-e', 'setInterval(() => {}, 1000)'],
|
|
56
|
+
timeoutMs: 50,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(result.timedOut).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('bounds captured output', async () => {
|
|
63
|
+
const result = await runProcess({
|
|
64
|
+
command: process.execPath,
|
|
65
|
+
args: ['-e', "process.stdout.write('x'.repeat(100))"],
|
|
66
|
+
maxOutputBytes: 10,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result.stdout).toBe('x'.repeat(10));
|
|
70
|
+
expect(result.outputTruncated).toBe(true);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
windowsIt('executes an npm-style command shim from a path with spaces', async () => {
|
|
74
|
+
const directory = await fs.mkdtemp(
|
|
75
|
+
path.join(os.tmpdir(), 'remote-codex command shim '),
|
|
76
|
+
);
|
|
77
|
+
temporaryDirectories.push(directory);
|
|
78
|
+
const shimPath = path.join(directory, 'fake-codex.cmd');
|
|
79
|
+
await fs.writeFile(shimPath, '@echo off\r\necho %~1\r\n');
|
|
80
|
+
|
|
81
|
+
const result = await runProcess({
|
|
82
|
+
command: shimPath,
|
|
83
|
+
args: ['shim executed'],
|
|
84
|
+
timeoutMs: 3_000,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
expect(result.code).toBe(0);
|
|
88
|
+
expect(result.stdout.trim()).toBe('shim executed');
|
|
89
|
+
expect(result.spawnError).toBeNull();
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('parseCommandLine', () => {
|
|
94
|
+
it('parses built-in npm and quoted node commands without a shell', () => {
|
|
95
|
+
expect(parseCommandLine('npm install -g @openai/codex@latest')).toEqual({
|
|
96
|
+
command: 'npm',
|
|
97
|
+
args: ['install', '-g', '@openai/codex@latest'],
|
|
98
|
+
});
|
|
99
|
+
expect(parseCommandLine('node -e "process.exit(0)"')).toEqual({
|
|
100
|
+
command: 'node',
|
|
101
|
+
args: ['-e', 'process.exit(0)'],
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('does not interpret shell operators', () => {
|
|
106
|
+
expect(parseCommandLine('tool first && second')).toEqual({
|
|
107
|
+
command: 'tool',
|
|
108
|
+
args: ['first', '&&', 'second'],
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
describe('resolveExecutable', () => {
|
|
114
|
+
it('resolves an executable from PATH', async () => {
|
|
115
|
+
const resolved = await resolveExecutable(process.execPath);
|
|
116
|
+
expect(resolved).toBe(path.resolve(process.execPath));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('uses Windows PATHEXT semantics for command shims', async () => {
|
|
120
|
+
const directory = await fs.mkdtemp(path.join(os.tmpdir(), 'remote-codex process '));
|
|
121
|
+
temporaryDirectories.push(directory);
|
|
122
|
+
const shimPath = path.join(directory, 'codex.CMD');
|
|
123
|
+
await fs.writeFile(shimPath, '@echo off\r\n');
|
|
124
|
+
|
|
125
|
+
const resolved = await resolveExecutable('codex', {
|
|
126
|
+
env: { PATH: directory, PATHEXT: '.EXE;.CMD' },
|
|
127
|
+
platform: 'win32',
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
expect(resolved?.toLowerCase()).toBe(shimPath.toLowerCase());
|
|
131
|
+
});
|
|
132
|
+
});
|