@karlorz/codexhost-cli 0.0.0-stage → 0.13.2-2
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 +27 -2
- package/bin/codexhost.js +510 -0
- package/package.json +41 -4
package/README.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @karlorz/codexhost-cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Run Pi and Claude Code as first-class external harnesses inside Codex Desktop.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @karlorz/codexhost-cli@0.13.2-2
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
npm automatically installs the matching macOS, Windows, or Linux platform package. Node.js 22 or 24 and the official ChatGPT/Codex Desktop are required.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
codexhost --version
|
|
17
|
+
codexhost
|
|
18
|
+
codexhost remote install
|
|
19
|
+
codexhost remote status
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
The `codexhost` command starts Codex Desktop. On macOS and Linux it returns immediately while the packaged Launcher keeps supervising in the background. On Windows, the command remains attached until Codex Desktop exits so shells that clean up process trees of completed commands cannot discard the supervisor. Re-running `codexhost` attaches to the same controlled instance.
|
|
23
|
+
|
|
24
|
+
On macOS, `remote install` installs a current-user Aqua Harness broker so Background SSH Hosts can use native Claude Code login without reading, copying, or unlocking Keychain credentials.
|
|
25
|
+
|
|
26
|
+
If installation used `--omit=optional`, reinstall without that option so npm can select the native package for the current architecture.
|
|
27
|
+
|
|
28
|
+
If startup reports a platform package version mismatch, close Codex Desktop and run the exact reinstall command in the error. This installs both the CLI and its platform package at the same version, replacing stale independently installed payloads. `codexhost --version` reports the CLI package version; startup also validates the platform payload before launching it.
|
package/bin/codexhost.js
ADDED
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { homedir } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const version = "0.13.2-2";
|
|
10
|
+
const userArguments = process.argv.slice(2);
|
|
11
|
+
const repositoryUrl = "https://github.com/karlorz/codex-host";
|
|
12
|
+
const startupTraceStartedAt = Date.now();
|
|
13
|
+
function startupTrace(stage) {
|
|
14
|
+
if (process.env.CODEXHOST_STARTUP_TRACE !== "1") return;
|
|
15
|
+
console.error(
|
|
16
|
+
"[codexhost startup +" + (Date.now() - startupTraceStartedAt) + "ms] npm: " + stage,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
function printStarPrompt() {
|
|
20
|
+
const locale =
|
|
21
|
+
process.env.LC_ALL ??
|
|
22
|
+
process.env.LC_MESSAGES ??
|
|
23
|
+
process.env.LANG ??
|
|
24
|
+
Intl.DateTimeFormat().resolvedOptions().locale;
|
|
25
|
+
const prompt = /^zh(?:[_-]|$)/iu.test(locale)
|
|
26
|
+
? "⭐ 如果这个项目对你有帮助,请给我们一个 Star ⭐"
|
|
27
|
+
: "⭐ If this project helps you, please give us a Star ⭐";
|
|
28
|
+
const useColor =
|
|
29
|
+
process.stdout.isTTY && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb";
|
|
30
|
+
if (useColor) {
|
|
31
|
+
console.log(
|
|
32
|
+
"\u001B[33m" + prompt + "\u001B[0m\n\u001B[36m" + repositoryUrl + "\u001B[0m",
|
|
33
|
+
);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log(prompt + "\n" + repositoryUrl);
|
|
37
|
+
}
|
|
38
|
+
if (
|
|
39
|
+
userArguments.length === 1 &&
|
|
40
|
+
(userArguments[0] === "--version" || userArguments[0] === "-v")
|
|
41
|
+
) {
|
|
42
|
+
console.log(version);
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
if (userArguments.length === 0 || userArguments[0] === "launch") {
|
|
46
|
+
printStarPrompt();
|
|
47
|
+
}
|
|
48
|
+
startupTrace("entry");
|
|
49
|
+
|
|
50
|
+
const platformPackages = {
|
|
51
|
+
"darwin-arm64": "@karlorz/codexhost-cli-darwin-arm64",
|
|
52
|
+
"darwin-x64": "@karlorz/codexhost-cli-darwin-x64",
|
|
53
|
+
"win32-x64": "@karlorz/codexhost-cli-win32-x64",
|
|
54
|
+
"win32-arm64": "@karlorz/codexhost-cli-win32-arm64",
|
|
55
|
+
"linux-x64": "@karlorz/codexhost-cli-linux-x64",
|
|
56
|
+
"linux-arm64": "@karlorz/codexhost-cli-linux-arm64"
|
|
57
|
+
};
|
|
58
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
59
|
+
const platformPackage = platformPackages[platformKey];
|
|
60
|
+
if (!platformPackage) fail(`unsupported platform '${platformKey}'`);
|
|
61
|
+
|
|
62
|
+
const require = createRequire(import.meta.url);
|
|
63
|
+
let packageRoot;
|
|
64
|
+
try {
|
|
65
|
+
packageRoot = path.dirname(require.resolve(`${platformPackage}/package.json`));
|
|
66
|
+
} catch {
|
|
67
|
+
// Local folder installs symlink packages into the source tree; Node realpaths
|
|
68
|
+
// the entry script, so the sibling platform package drops off the resolution
|
|
69
|
+
// chain. Fall back to the npm global layout derived from the bin symlink.
|
|
70
|
+
packageRoot = null;
|
|
71
|
+
if (process.platform !== "win32" && process.argv[1]) {
|
|
72
|
+
const prefix = path.dirname(path.dirname(path.resolve(process.argv[1])));
|
|
73
|
+
const globalPackage = path.join(
|
|
74
|
+
prefix,
|
|
75
|
+
"lib",
|
|
76
|
+
"node_modules",
|
|
77
|
+
platformPackage,
|
|
78
|
+
"package.json",
|
|
79
|
+
);
|
|
80
|
+
if (existsSync(globalPackage)) packageRoot = path.dirname(globalPackage);
|
|
81
|
+
}
|
|
82
|
+
if (!packageRoot) {
|
|
83
|
+
fail(
|
|
84
|
+
`missing optional platform package '${platformPackage}'. Reinstall @karlorz/codexhost-cli without --omit=optional.`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
let platformVersion;
|
|
90
|
+
try {
|
|
91
|
+
platformVersion = JSON.parse(readFileSync(path.join(packageRoot, "package.json"), "utf8"))?.version;
|
|
92
|
+
} catch {
|
|
93
|
+
fail(`cannot read platform package metadata: ${packageRoot}`);
|
|
94
|
+
}
|
|
95
|
+
if (platformVersion !== version) {
|
|
96
|
+
fail(
|
|
97
|
+
`platform package version mismatch: '${platformPackage}' at '${packageRoot}' has ${JSON.stringify(platformVersion) ?? "no version"}; expected ${version}. Close Codex Desktop, then run: npm install -g @karlorz/codexhost-cli@${version} ${platformPackage}@${version}`,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
startupTrace("platform package resolved");
|
|
101
|
+
const executableSuffix = process.platform === "win32" ? ".exe" : "";
|
|
102
|
+
const launcher = path.join(packageRoot, "bin", `codexhost${executableSuffix}`);
|
|
103
|
+
const shim = path.join(packageRoot, "libexec", `codexhost-shim${executableSuffix}`);
|
|
104
|
+
const hostRuntime = path.join(packageRoot, "app", "host-runtime.mjs");
|
|
105
|
+
const desktopController = path.join(packageRoot, "app", "desktop-controller.mjs");
|
|
106
|
+
const rendererExtension = path.join(packageRoot, "app", "renderer-extension.js");
|
|
107
|
+
const consoleServer = path.join(packageRoot, "app", "console-server.mjs");
|
|
108
|
+
|
|
109
|
+
function fail(message) {
|
|
110
|
+
console.error(`codexhost: ${message}`);
|
|
111
|
+
process.exit(1);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Desktop resources are validated by the Launcher after it starts the recovery
|
|
115
|
+
// console. Do not prevent recovery when one of those resources is missing.
|
|
116
|
+
if (!existsSync(launcher)) fail(`missing launcher: ${launcher}`);
|
|
117
|
+
|
|
118
|
+
function existingFile(filePath) {
|
|
119
|
+
return typeof filePath === "string" && filePath.length > 0 && existsSync(filePath)
|
|
120
|
+
? filePath
|
|
121
|
+
: undefined;
|
|
122
|
+
}
|
|
123
|
+
function officialNpmCliPath(nodePath) {
|
|
124
|
+
return process.platform === "win32"
|
|
125
|
+
? path.join(path.dirname(nodePath), "node_modules", "npm", "bin", "npm-cli.js")
|
|
126
|
+
: path.join(
|
|
127
|
+
path.dirname(path.dirname(nodePath)),
|
|
128
|
+
"lib",
|
|
129
|
+
"node_modules",
|
|
130
|
+
"npm",
|
|
131
|
+
"bin",
|
|
132
|
+
"npm-cli.js",
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
function homebrewNpmCliPath(nodePath) {
|
|
136
|
+
if (process.platform === "win32") return undefined;
|
|
137
|
+
const segments = nodePath.split(path.sep);
|
|
138
|
+
const cellarIndex = segments.lastIndexOf("Cellar");
|
|
139
|
+
if (
|
|
140
|
+
cellarIndex >= 1 &&
|
|
141
|
+
segments[cellarIndex + 1] === "node" &&
|
|
142
|
+
segments.at(-2) === "bin" &&
|
|
143
|
+
segments.at(-1) === "node"
|
|
144
|
+
) {
|
|
145
|
+
const brewPrefix = segments.slice(0, cellarIndex).join(path.sep);
|
|
146
|
+
return (
|
|
147
|
+
existingFile(
|
|
148
|
+
path.join(brewPrefix, "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
149
|
+
) ??
|
|
150
|
+
existingFile(
|
|
151
|
+
path.join(
|
|
152
|
+
path.dirname(path.dirname(nodePath)),
|
|
153
|
+
"libexec",
|
|
154
|
+
"lib",
|
|
155
|
+
"node_modules",
|
|
156
|
+
"npm",
|
|
157
|
+
"bin",
|
|
158
|
+
"npm-cli.js",
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
const brewPrefix = process.env.HOMEBREW_PREFIX;
|
|
164
|
+
if (typeof brewPrefix === "string" && brewPrefix.length > 0) {
|
|
165
|
+
return existingFile(
|
|
166
|
+
path.join(brewPrefix, "lib", "node_modules", "npm", "bin", "npm-cli.js"),
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
function pathNpmCliPath() {
|
|
172
|
+
const pathValue = process.env.PATH;
|
|
173
|
+
if (typeof pathValue !== "string" || pathValue.length === 0) return undefined;
|
|
174
|
+
const npmName = process.platform === "win32" ? "npm.cmd" : "npm";
|
|
175
|
+
for (const directory of pathValue.split(path.delimiter)) {
|
|
176
|
+
if (!directory) continue;
|
|
177
|
+
const candidate = path.join(directory, npmName);
|
|
178
|
+
if (!existsSync(candidate)) continue;
|
|
179
|
+
try {
|
|
180
|
+
const resolved = realpathSync(candidate);
|
|
181
|
+
if (path.basename(resolved) === "npm-cli.js") return resolved;
|
|
182
|
+
} catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return undefined;
|
|
187
|
+
}
|
|
188
|
+
const npmCliPath =
|
|
189
|
+
existingFile(process.env.npm_execpath) ??
|
|
190
|
+
existingFile(officialNpmCliPath(process.execPath)) ??
|
|
191
|
+
homebrewNpmCliPath(process.execPath) ??
|
|
192
|
+
pathNpmCliPath();
|
|
193
|
+
if (!npmCliPath) fail("could not locate the npm CLI used to update this global installation");
|
|
194
|
+
const updateEnvironment = {
|
|
195
|
+
...process.env,
|
|
196
|
+
CODEXHOST_NPM_NODE_PATH: process.execPath,
|
|
197
|
+
CODEXHOST_NPM_CLI_PATH: path.resolve(npmCliPath),
|
|
198
|
+
CODEXHOST_NPM_LAUNCHER_PATH: fileURLToPath(import.meta.url),
|
|
199
|
+
CODEXHOST_NPM_PACKAGE_ROOT: packageRoot,
|
|
200
|
+
};
|
|
201
|
+
// A managed SSH installation deliberately exports these variables from the
|
|
202
|
+
// remote login profile so stock Codex can enter the remote Host. When this npm
|
|
203
|
+
// command starts the local Desktop on that same machine, replace the remote
|
|
204
|
+
// bootstrap with a local data root. CODEXHOST_CLAUDE_COMMAND is intentionally
|
|
205
|
+
// shared and therefore preserved.
|
|
206
|
+
const remoteSshBootstrapEnvironment = [
|
|
207
|
+
"CODEX_INSTALL_DIR",
|
|
208
|
+
"CODEXHOST_DATA_DIR",
|
|
209
|
+
// Retired; older remote profile blocks still export it.
|
|
210
|
+
"CODEXHOST_DEFAULT_AGENT",
|
|
211
|
+
"CODEXHOST_HOST_NODE_PATH",
|
|
212
|
+
"CODEXHOST_HOST_RUNTIME_PATH",
|
|
213
|
+
"CODEXHOST_REMOTE_SSH_MANAGED",
|
|
214
|
+
"CODEXHOST_STOCK_CODEX_PATH",
|
|
215
|
+
];
|
|
216
|
+
if (updateEnvironment.CODEXHOST_REMOTE_SSH_MANAGED === "1") {
|
|
217
|
+
for (const name of remoteSshBootstrapEnvironment) delete updateEnvironment[name];
|
|
218
|
+
updateEnvironment.CODEXHOST_DATA_DIR = path.join(homedir(), ".codexhost");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let launchArguments;
|
|
222
|
+
let remoteArguments = null;
|
|
223
|
+
let brokerArguments = null;
|
|
224
|
+
let delegationArguments = null;
|
|
225
|
+
let consoleArguments = null;
|
|
226
|
+
if (userArguments.length === 0) {
|
|
227
|
+
launchArguments = ["launch"];
|
|
228
|
+
} else if (userArguments[0] === "launch") {
|
|
229
|
+
launchArguments = userArguments;
|
|
230
|
+
} else if (userArguments[0] === "inspect") {
|
|
231
|
+
launchArguments = userArguments;
|
|
232
|
+
} else if (userArguments[0] === "console" || userArguments[0] === "update") {
|
|
233
|
+
launchArguments = null;
|
|
234
|
+
consoleArguments = userArguments.slice(1);
|
|
235
|
+
} else if (userArguments[0] === "remote") {
|
|
236
|
+
launchArguments = null;
|
|
237
|
+
remoteArguments = userArguments.slice(1);
|
|
238
|
+
} else if (userArguments[0] === "broker") {
|
|
239
|
+
launchArguments = null;
|
|
240
|
+
brokerArguments = userArguments.slice(1);
|
|
241
|
+
} else if (
|
|
242
|
+
userArguments[0] === "harness" ||
|
|
243
|
+
userArguments[0] === "delegate" ||
|
|
244
|
+
userArguments[0] === "thread"
|
|
245
|
+
) {
|
|
246
|
+
launchArguments = null;
|
|
247
|
+
delegationArguments = userArguments;
|
|
248
|
+
} else if (userArguments[0] === "--help" || userArguments[0] === "-h") {
|
|
249
|
+
console.log(
|
|
250
|
+
[
|
|
251
|
+
"usage:",
|
|
252
|
+
" codexhost",
|
|
253
|
+
" codexhost --version",
|
|
254
|
+
" codexhost inspect",
|
|
255
|
+
" codexhost console",
|
|
256
|
+
" codexhost update",
|
|
257
|
+
" codexhost launch [launcher options]",
|
|
258
|
+
" codexhost remote install|start|stop|status|uninstall",
|
|
259
|
+
" codexhost broker install|status|stop|uninstall",
|
|
260
|
+
" codexhost delegate --help",
|
|
261
|
+
" codexhost harness inspect ...",
|
|
262
|
+
" codexhost delegate start ...",
|
|
263
|
+
" codexhost thread send|cancel|read|wait|list ...",
|
|
264
|
+
"",
|
|
265
|
+
"This npm package uses the current Node.js runtime and the packaged",
|
|
266
|
+
"Rust launcher/shim. Codex Desktop must already be installed.",
|
|
267
|
+
].join("\n"),
|
|
268
|
+
);
|
|
269
|
+
process.exit(0);
|
|
270
|
+
} else {
|
|
271
|
+
fail(
|
|
272
|
+
`unknown command '${userArguments[0]}'. Run 'codexhost --help' for usage.`,
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (launchArguments?.[0] === "launch") {
|
|
277
|
+
const injected = new Set();
|
|
278
|
+
for (let index = 1; index < launchArguments.length; index += 1) {
|
|
279
|
+
const argument = launchArguments[index];
|
|
280
|
+
if (
|
|
281
|
+
argument === "--node" ||
|
|
282
|
+
argument === "--shim" ||
|
|
283
|
+
argument === "--host-runtime" ||
|
|
284
|
+
argument === "--desktop-controller" ||
|
|
285
|
+
argument === "--renderer"
|
|
286
|
+
) {
|
|
287
|
+
injected.add(argument);
|
|
288
|
+
index += 1;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const extras = [];
|
|
292
|
+
if (!injected.has("--node")) extras.push("--node", process.execPath);
|
|
293
|
+
if (!injected.has("--shim")) extras.push("--shim", shim);
|
|
294
|
+
if (!injected.has("--host-runtime")) extras.push("--host-runtime", hostRuntime);
|
|
295
|
+
if (!injected.has("--desktop-controller")) {
|
|
296
|
+
extras.push("--desktop-controller", desktopController);
|
|
297
|
+
}
|
|
298
|
+
if (!injected.has("--renderer")) extras.push("--renderer", rendererExtension);
|
|
299
|
+
launchArguments = ["launch", ...extras, ...launchArguments.slice(1)];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (consoleArguments !== null) {
|
|
303
|
+
if (consoleArguments.length > 0) fail(userArguments[0] + " accepts no arguments");
|
|
304
|
+
if (!existsSync(consoleServer)) fail(`missing console: ${consoleServer}`);
|
|
305
|
+
const child = spawn(process.execPath, [consoleServer, userArguments[0] === "update" ? "update" : "open"], {
|
|
306
|
+
env: { ...updateEnvironment, CODEXHOST_LAUNCHER_EXECUTABLE: launcher },
|
|
307
|
+
stdio: "inherit",
|
|
308
|
+
windowsHide: true,
|
|
309
|
+
});
|
|
310
|
+
child.on("error", (error) => fail(error.message));
|
|
311
|
+
child.on("exit", (code) => process.exit(code ?? 1));
|
|
312
|
+
} else if (delegationArguments !== null) {
|
|
313
|
+
const child = spawn(
|
|
314
|
+
process.execPath,
|
|
315
|
+
[hostRuntime, "--codexhost-delegation-cli", ...delegationArguments],
|
|
316
|
+
{
|
|
317
|
+
env: {
|
|
318
|
+
...updateEnvironment,
|
|
319
|
+
CODEXHOST_CLI_PATH: fileURLToPath(import.meta.url),
|
|
320
|
+
},
|
|
321
|
+
stdio: "inherit",
|
|
322
|
+
windowsHide: true,
|
|
323
|
+
},
|
|
324
|
+
);
|
|
325
|
+
child.on("error", (error) => fail(error.message));
|
|
326
|
+
child.on("exit", (code, signal) => {
|
|
327
|
+
if (signal) {
|
|
328
|
+
process.kill(process.pid, signal);
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
process.exit(code ?? 1);
|
|
332
|
+
});
|
|
333
|
+
} else if (brokerArguments !== null) {
|
|
334
|
+
const child = spawn(
|
|
335
|
+
launcher,
|
|
336
|
+
[
|
|
337
|
+
"broker",
|
|
338
|
+
...brokerArguments,
|
|
339
|
+
"--node", process.execPath, "--host-runtime", hostRuntime,
|
|
340
|
+
],
|
|
341
|
+
{
|
|
342
|
+
env: updateEnvironment,
|
|
343
|
+
stdio: "inherit",
|
|
344
|
+
windowsHide: true,
|
|
345
|
+
},
|
|
346
|
+
);
|
|
347
|
+
child.on("error", (error) => fail(error.message));
|
|
348
|
+
child.on("exit", (code, signal) => {
|
|
349
|
+
if (signal) {
|
|
350
|
+
process.kill(process.pid, signal);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
process.exit(code ?? 1);
|
|
354
|
+
});
|
|
355
|
+
} else if (remoteArguments !== null) {
|
|
356
|
+
// Every Harness whose plugin selects a BrokeredHarnessAdapter for a managed
|
|
357
|
+
// macOS remote Host needs its own Aqua LaunchAgent. Keep this list in sync with
|
|
358
|
+
// those plugin factories; Claude Code keeps the legacy unlabelled invocation.
|
|
359
|
+
// Install only registers them: the remote Host starts a broker on demand for an
|
|
360
|
+
// installed Harness and the broker exits when idle.
|
|
361
|
+
const nativeBrokerHarnessIds = ["claude-code", "codebuddy", "workbuddy", "cursor-cli"];
|
|
362
|
+
const runNativeBroker = (command) => {
|
|
363
|
+
let index = 0;
|
|
364
|
+
let firstFailure = 0;
|
|
365
|
+
// Run every broker even after a failure so one broken service cannot leave
|
|
366
|
+
// the others stale or installed, then report the first failure.
|
|
367
|
+
const next = () => {
|
|
368
|
+
if (index >= nativeBrokerHarnessIds.length) {
|
|
369
|
+
process.exit(firstFailure);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
const harnessId = nativeBrokerHarnessIds[index++];
|
|
373
|
+
const broker = spawn(
|
|
374
|
+
launcher,
|
|
375
|
+
[
|
|
376
|
+
"broker",
|
|
377
|
+
command,
|
|
378
|
+
...(harnessId === "claude-code" ? [] : ["--harness", harnessId]),
|
|
379
|
+
"--node", process.execPath, "--host-runtime", hostRuntime,
|
|
380
|
+
],
|
|
381
|
+
{
|
|
382
|
+
env: updateEnvironment,
|
|
383
|
+
// remote status is a stable JSON stdout surface. Keep the broker's
|
|
384
|
+
// human-readable status beside it on stderr instead of corrupting JSON.
|
|
385
|
+
stdio: command === "status" ? ["inherit", process.stderr, "inherit"] : "inherit",
|
|
386
|
+
windowsHide: true,
|
|
387
|
+
},
|
|
388
|
+
);
|
|
389
|
+
broker.on("error", (error) => fail(error.message));
|
|
390
|
+
broker.on("exit", (code, signal) => {
|
|
391
|
+
if (signal) {
|
|
392
|
+
process.kill(process.pid, signal);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (code !== 0 && firstFailure === 0) firstFailure = code ?? 1;
|
|
396
|
+
next();
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
next();
|
|
400
|
+
};
|
|
401
|
+
const child = spawn(
|
|
402
|
+
process.execPath,
|
|
403
|
+
[
|
|
404
|
+
hostRuntime,
|
|
405
|
+
"--codexhost-remote",
|
|
406
|
+
...remoteArguments,
|
|
407
|
+
"--node",
|
|
408
|
+
process.execPath,
|
|
409
|
+
"--shim",
|
|
410
|
+
shim,
|
|
411
|
+
"--host-runtime",
|
|
412
|
+
hostRuntime,
|
|
413
|
+
],
|
|
414
|
+
{
|
|
415
|
+
env: updateEnvironment,
|
|
416
|
+
stdio: "inherit",
|
|
417
|
+
windowsHide: true,
|
|
418
|
+
},
|
|
419
|
+
);
|
|
420
|
+
child.on("error", (error) => fail(error.message));
|
|
421
|
+
child.on("exit", (code, signal) => {
|
|
422
|
+
if (signal) {
|
|
423
|
+
process.kill(process.pid, signal);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (code === 0 && process.platform === "darwin") {
|
|
427
|
+
if (remoteArguments[0] === "install") {
|
|
428
|
+
runNativeBroker("install");
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (remoteArguments[0] === "status") {
|
|
432
|
+
runNativeBroker("status");
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
if (remoteArguments[0] === "uninstall") {
|
|
436
|
+
runNativeBroker("uninstall");
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
if (remoteArguments[0] === "stop") {
|
|
440
|
+
// Brokers stay registered for on-demand starts by the next remote Host.
|
|
441
|
+
runNativeBroker("stop");
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
process.exit(code ?? 1);
|
|
446
|
+
});
|
|
447
|
+
} else if (launchArguments?.[0] === "launch") {
|
|
448
|
+
// The Launcher prints "ready" once the Desktop, Controller, and Host chain
|
|
449
|
+
// are up, then detaches from the terminal to keep supervising. Windows
|
|
450
|
+
// command hosts may clean up a completed command's process tree, so keep the
|
|
451
|
+
// npm parent alive there until the managed Desktop exits. Other platforms
|
|
452
|
+
// return immediately after startup as before.
|
|
453
|
+
startupTrace("spawning Launcher");
|
|
454
|
+
const keepLauncherForeground = process.platform === "win32";
|
|
455
|
+
const child = spawn(launcher, launchArguments, {
|
|
456
|
+
env: updateEnvironment,
|
|
457
|
+
stdio: ["ignore", "pipe", "inherit"],
|
|
458
|
+
windowsHide: true,
|
|
459
|
+
});
|
|
460
|
+
let finished = false;
|
|
461
|
+
const finish = (code) => {
|
|
462
|
+
if (finished) return;
|
|
463
|
+
finished = true;
|
|
464
|
+
startupTrace("Launcher finished startup with code " + code);
|
|
465
|
+
process.exit(code);
|
|
466
|
+
};
|
|
467
|
+
child.stdout.setEncoding("utf8");
|
|
468
|
+
let ready = false;
|
|
469
|
+
let launcherOutput = "";
|
|
470
|
+
const readyMarker = "ready\n";
|
|
471
|
+
child.stdout.on("data", (chunk) => {
|
|
472
|
+
if (ready) return;
|
|
473
|
+
const output = launcherOutput + chunk;
|
|
474
|
+
if (output.includes(readyMarker)) {
|
|
475
|
+
ready = true;
|
|
476
|
+
launcherOutput = "";
|
|
477
|
+
startupTrace("received Launcher ready");
|
|
478
|
+
if (!keepLauncherForeground) finish(0);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
// Only retain the tail needed to recognize a marker split across chunks.
|
|
482
|
+
launcherOutput = output.slice(1 - readyMarker.length);
|
|
483
|
+
});
|
|
484
|
+
child.on("error", (error) => {
|
|
485
|
+
startupTrace("Launcher spawn failed: " + error.message);
|
|
486
|
+
fail(error.message);
|
|
487
|
+
});
|
|
488
|
+
child.on("exit", (code, signal) => {
|
|
489
|
+
startupTrace(ready ? "Launcher exited after ready" : "Launcher exited before ready");
|
|
490
|
+
if (signal) {
|
|
491
|
+
process.kill(process.pid, signal);
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
finish(code ?? 1);
|
|
495
|
+
});
|
|
496
|
+
} else {
|
|
497
|
+
const child = spawn(launcher, launchArguments, {
|
|
498
|
+
env: updateEnvironment,
|
|
499
|
+
stdio: "inherit",
|
|
500
|
+
windowsHide: true,
|
|
501
|
+
});
|
|
502
|
+
child.on("error", (error) => fail(error.message));
|
|
503
|
+
child.on("exit", (code, signal) => {
|
|
504
|
+
if (signal) {
|
|
505
|
+
process.kill(process.pid, signal);
|
|
506
|
+
return;
|
|
507
|
+
}
|
|
508
|
+
process.exit(code ?? 1);
|
|
509
|
+
});
|
|
510
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,43 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@karlorz/codexhost-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.13.2-2",
|
|
4
|
+
"description": "Run Pi and Claude Code as first-class external harnesses inside Codex Desktop.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"codexhost": "bin/codexhost.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/**",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22"
|
|
15
|
+
},
|
|
16
|
+
"optionalDependencies": {
|
|
17
|
+
"@karlorz/codexhost-cli-darwin-arm64": "0.13.2-2",
|
|
18
|
+
"@karlorz/codexhost-cli-darwin-x64": "0.13.2-2",
|
|
19
|
+
"@karlorz/codexhost-cli-win32-x64": "0.13.2-2",
|
|
20
|
+
"@karlorz/codexhost-cli-win32-arm64": "0.13.2-2",
|
|
21
|
+
"@karlorz/codexhost-cli-linux-x64": "0.13.2-2",
|
|
22
|
+
"@karlorz/codexhost-cli-linux-arm64": "0.13.2-2"
|
|
23
|
+
},
|
|
24
|
+
"keywords": [
|
|
25
|
+
"codex",
|
|
26
|
+
"codexhost",
|
|
27
|
+
"pi",
|
|
28
|
+
"claude-code",
|
|
29
|
+
"agent",
|
|
30
|
+
"harness"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/karlorz/codex-host.git"
|
|
35
|
+
},
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/karlorz/codex-host/issues"
|
|
38
|
+
},
|
|
39
|
+
"homepage": "https://github.com/karlorz/codex-host#readme",
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
}
|
|
43
|
+
}
|