copillm 0.3.0 → 0.3.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.
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
1
|
import { inspectStoredCredential } from "../../auth/credentials.js";
|
|
3
2
|
import { loadConfig } from "../../config/config.js";
|
|
4
3
|
import { getCopillmHome } from "../../config/home.js";
|
|
@@ -8,9 +7,8 @@ import { inspectLock, releaseLock } from "../../server/lock.js";
|
|
|
8
7
|
import { buildCodexEnvBundle } from "../agentEnv.js";
|
|
9
8
|
import { ensureAuthenticatedInteractive } from "../auth/ensure.js";
|
|
10
9
|
import { computeUptimeSeconds, formatUptime, stopByPid } from "../daemon/lifecycle.js";
|
|
11
|
-
import { probeHealth, readLiveLock,
|
|
10
|
+
import { probeDebugEndpoint, probeHealth, readLiveLock, warnIfDebugRequestedButInactive } from "../daemon/probes.js";
|
|
12
11
|
import { runDaemon } from "../daemon/runDaemon.js";
|
|
13
|
-
import { daemonSpawnEnv } from "../daemon/spawnEnv.js";
|
|
14
12
|
import { buildClaudeExportCommand } from "../integrations/claudeExport.js";
|
|
15
13
|
import { formatStartBanner, formatStopHumanLine, displayHomePath } from "../integrations/banner.js";
|
|
16
14
|
import { refreshCodexHome } from "../integrations/refreshCodex.js";
|
|
@@ -19,7 +17,8 @@ import { writeAuthStatusLine } from "../shared/backends.js";
|
|
|
19
17
|
import { currentDebugLogPath, enableRuntimeDebug, getRootLogger, resolveCopillmDebug } from "../shared/debug.js";
|
|
20
18
|
import { isDevModeActive } from "../shared/devMode.js";
|
|
21
19
|
import { writeCommandOutput, writeHealthOutput } from "../shared/output.js";
|
|
22
|
-
import {
|
|
20
|
+
import { spawnDetachedDaemon } from "../daemon/spawnDetached.js";
|
|
21
|
+
import { resolveRestartDecision } from "../daemon/restart.js";
|
|
23
22
|
export function register(program) {
|
|
24
23
|
program
|
|
25
24
|
.command("start")
|
|
@@ -79,55 +78,8 @@ export function register(program) {
|
|
|
79
78
|
});
|
|
80
79
|
return;
|
|
81
80
|
}
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
daemonCommand.args.push("--debug");
|
|
85
|
-
}
|
|
86
|
-
const child = spawn(daemonCommand.command, daemonCommand.args, {
|
|
87
|
-
detached: true,
|
|
88
|
-
stdio: "ignore",
|
|
89
|
-
env: daemonSpawnEnv(debug)
|
|
90
|
-
});
|
|
91
|
-
child.unref();
|
|
92
|
-
const started = await waitForDaemonReady(child.pid ?? null, 8_000);
|
|
93
|
-
if (!started) {
|
|
94
|
-
throw new Error("Detached daemon start timed out.");
|
|
95
|
-
}
|
|
96
|
-
const sharedDetached = await loadSharedStartContextIfNeeded(opts);
|
|
97
|
-
const codex = opts.codex === false ? null : await refreshCodexHome(started.port, opts.codexModel ?? null, sharedDetached);
|
|
98
|
-
const pi = opts.pi === false ? null : await refreshPiHome(started.port, sharedDetached);
|
|
99
|
-
const claude = buildClaudeExportCommand(started.port, null);
|
|
100
|
-
const banner = formatStartBanner({
|
|
101
|
-
port: started.port,
|
|
102
|
-
pid: started.pid,
|
|
103
|
-
mode: "detached",
|
|
104
|
-
debug,
|
|
105
|
-
debugLogPath: currentDebugLogPath(debug),
|
|
106
|
-
codex,
|
|
107
|
-
pi
|
|
108
|
-
});
|
|
109
|
-
writeCommandOutput(opts, banner, {
|
|
110
|
-
status: "ok",
|
|
111
|
-
mode: "detached",
|
|
112
|
-
pid: started.pid,
|
|
113
|
-
port: started.port,
|
|
114
|
-
debug,
|
|
115
|
-
debug_log_path: currentDebugLogPath(debug),
|
|
116
|
-
url: `http://127.0.0.1:${started.port}`,
|
|
117
|
-
codex_home: codex?.outDir ?? null,
|
|
118
|
-
codex_export_command: codex?.exportCommand ?? null,
|
|
119
|
-
codex_env: codex ? buildCodexEnvBundle(codex.outDir).env : null,
|
|
120
|
-
codex_default_model: codex?.defaultModel ?? null,
|
|
121
|
-
codex_model_count: codex?.modelCount ?? null,
|
|
122
|
-
pi_home: pi?.outDir ?? null,
|
|
123
|
-
pi_config_path: pi?.configPath ?? null,
|
|
124
|
-
pi_mirror_path: pi?.mirrorPath ?? null,
|
|
125
|
-
pi_backup_path: pi?.backupPath ?? null,
|
|
126
|
-
pi_model_count: pi?.modelCount ?? null,
|
|
127
|
-
claude_export_command: claude.command,
|
|
128
|
-
claude_env: claude.bundle.env,
|
|
129
|
-
claude_defaults: claude.defaults
|
|
130
|
-
});
|
|
81
|
+
const started = await spawnDetachedDaemon({ debug });
|
|
82
|
+
await emitDetachedStartOutput(opts, started, debug, "detached");
|
|
131
83
|
return;
|
|
132
84
|
}
|
|
133
85
|
// Foreground path: interactively prompt for login if needed.
|
|
@@ -205,6 +157,46 @@ export function register(program) {
|
|
|
205
157
|
claude_defaults: claude.defaults
|
|
206
158
|
});
|
|
207
159
|
});
|
|
160
|
+
program
|
|
161
|
+
.command("restart")
|
|
162
|
+
.description("Restart the daemon, preserving its current port and debug mode")
|
|
163
|
+
.option("--debug", "Force debug endpoints on for the restarted daemon")
|
|
164
|
+
.option("--no-codex", "Skip generating ~/.copillm/codex/ for Codex CLI")
|
|
165
|
+
.option("--codex-model <id>", "Default Codex model slug")
|
|
166
|
+
.option("--no-pi", "Skip generating the copillm-owned pi models.json for pi coding agent")
|
|
167
|
+
.option("--json", "JSON output")
|
|
168
|
+
.action(async (opts) => {
|
|
169
|
+
const forceDebug = resolveCopillmDebug(opts.debug);
|
|
170
|
+
if (isDevModeActive()) {
|
|
171
|
+
process.stderr.write(`dev mode: isolated COPILLM_HOME ${displayHomePath(getCopillmHome())}\n`);
|
|
172
|
+
}
|
|
173
|
+
// Restart always brings the daemon back up detached, so fail fast on
|
|
174
|
+
// missing credentials rather than letting the detached child die silently.
|
|
175
|
+
const authState = await inspectStoredCredential();
|
|
176
|
+
if (!authState.stored) {
|
|
177
|
+
throw new Error("Not authenticated. Run `copillm auth login` first.");
|
|
178
|
+
}
|
|
179
|
+
// Detect the running daemon's debug mode *before* stopping it — `/_debug`
|
|
180
|
+
// is only reachable while the daemon is up.
|
|
181
|
+
const lockSnapshot = toDaemonLockState(inspectLock());
|
|
182
|
+
const detectedDebug = lockSnapshot.state === "running" ? await probeDebugEndpoint(lockSnapshot.port) : false;
|
|
183
|
+
const decision = resolveRestartDecision({ lock: lockSnapshot, detectedDebug, forceDebug });
|
|
184
|
+
enableRuntimeDebug(decision.debug);
|
|
185
|
+
if (decision.action === "restart" && decision.previousPid !== null) {
|
|
186
|
+
await stopByPid(decision.previousPid);
|
|
187
|
+
}
|
|
188
|
+
else if (decision.clearStaleLock) {
|
|
189
|
+
releaseLock();
|
|
190
|
+
}
|
|
191
|
+
// Mirror `stop`: clearing the Claude gateway cache on the way down keeps
|
|
192
|
+
// Claude Code from pinning a stale model picker across the restart.
|
|
193
|
+
const cache = clearClaudeGatewayCache();
|
|
194
|
+
const started = await spawnDetachedDaemon({ debug: decision.debug, forcePort: decision.forcePort });
|
|
195
|
+
await emitDetachedStartOutput(opts, started, decision.debug, "restarted", {
|
|
196
|
+
previousPid: decision.previousPid,
|
|
197
|
+
claudeCache: cache
|
|
198
|
+
});
|
|
199
|
+
});
|
|
208
200
|
program
|
|
209
201
|
.command("daemon")
|
|
210
202
|
.description("Internal background command")
|
|
@@ -384,6 +376,67 @@ export function register(program) {
|
|
|
384
376
|
}
|
|
385
377
|
});
|
|
386
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Emit the human banner + JSON payload for a daemon that was just brought up in
|
|
381
|
+
* the background. Shared by `copillm start --detach` (`mode: "detached"`) and
|
|
382
|
+
* `copillm restart` (`mode: "restarted"`), so both surface the same codex/pi/
|
|
383
|
+
* claude wiring. `extra` carries restart-only fields (`previous_pid`, the
|
|
384
|
+
* Claude cache-clear result).
|
|
385
|
+
*/
|
|
386
|
+
async function emitDetachedStartOutput(opts, started, debug, mode, extra) {
|
|
387
|
+
const shared = await loadSharedStartContextIfNeeded(opts);
|
|
388
|
+
const codex = opts.codex === false ? null : await refreshCodexHome(started.port, opts.codexModel ?? null, shared);
|
|
389
|
+
const pi = opts.pi === false ? null : await refreshPiHome(started.port, shared);
|
|
390
|
+
const claude = buildClaudeExportCommand(started.port, null);
|
|
391
|
+
const banner = formatStartBanner({
|
|
392
|
+
port: started.port,
|
|
393
|
+
pid: started.pid,
|
|
394
|
+
mode,
|
|
395
|
+
debug,
|
|
396
|
+
debugLogPath: currentDebugLogPath(debug),
|
|
397
|
+
codex,
|
|
398
|
+
pi
|
|
399
|
+
});
|
|
400
|
+
const payload = {
|
|
401
|
+
status: "ok",
|
|
402
|
+
mode,
|
|
403
|
+
pid: started.pid,
|
|
404
|
+
port: started.port,
|
|
405
|
+
debug,
|
|
406
|
+
debug_log_path: currentDebugLogPath(debug),
|
|
407
|
+
url: `http://127.0.0.1:${started.port}`,
|
|
408
|
+
codex_home: codex?.outDir ?? null,
|
|
409
|
+
codex_export_command: codex?.exportCommand ?? null,
|
|
410
|
+
codex_env: codex ? buildCodexEnvBundle(codex.outDir).env : null,
|
|
411
|
+
codex_default_model: codex?.defaultModel ?? null,
|
|
412
|
+
codex_model_count: codex?.modelCount ?? null,
|
|
413
|
+
pi_home: pi?.outDir ?? null,
|
|
414
|
+
pi_config_path: pi?.configPath ?? null,
|
|
415
|
+
pi_mirror_path: pi?.mirrorPath ?? null,
|
|
416
|
+
pi_backup_path: pi?.backupPath ?? null,
|
|
417
|
+
pi_model_count: pi?.modelCount ?? null,
|
|
418
|
+
claude_export_command: claude.command,
|
|
419
|
+
claude_env: claude.bundle.env,
|
|
420
|
+
claude_defaults: claude.defaults
|
|
421
|
+
};
|
|
422
|
+
if (extra?.previousPid !== undefined) {
|
|
423
|
+
payload.previous_pid = extra.previousPid;
|
|
424
|
+
}
|
|
425
|
+
if (extra?.claudeCache !== undefined) {
|
|
426
|
+
payload.claude_cache = extra.claudeCache;
|
|
427
|
+
}
|
|
428
|
+
writeCommandOutput(opts, banner, payload);
|
|
429
|
+
}
|
|
430
|
+
/** Narrow the lock inspection down to the shape `resolveRestartDecision` needs. */
|
|
431
|
+
function toDaemonLockState(lockState) {
|
|
432
|
+
if (lockState.state === "running") {
|
|
433
|
+
return { state: "running", pid: lockState.lock.pid, port: lockState.lock.port };
|
|
434
|
+
}
|
|
435
|
+
if (lockState.state === "stale") {
|
|
436
|
+
return { state: "stale" };
|
|
437
|
+
}
|
|
438
|
+
return { state: "missing" };
|
|
439
|
+
}
|
|
387
440
|
/**
|
|
388
441
|
* Load the shared credential/config/discovery context for `copillm start`'s
|
|
389
442
|
* codex + pi init steps, ONLY when at least one of them is going to run.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision logic for `copillm restart`.
|
|
3
|
+
*
|
|
4
|
+
* `restart` brings the daemon back up on the settings it is *actually* running
|
|
5
|
+
* on — its port and its debug mode — without persisting anything to disk. Port
|
|
6
|
+
* comes straight from the live lock; debug mode is detected at runtime by
|
|
7
|
+
* probing `/_debug` (only mounted when debug is on). The home/dev scope is
|
|
8
|
+
* inherent: the lock is per-`COPILLM_HOME`, so `copillm --dev restart` only
|
|
9
|
+
* ever touches the dev daemon.
|
|
10
|
+
*
|
|
11
|
+
* Keeping the decision pure (no I/O) makes every branch — running, stale,
|
|
12
|
+
* missing, debug-detected, debug-forced — trivially unit-testable.
|
|
13
|
+
*/
|
|
14
|
+
export function resolveRestartDecision(input) {
|
|
15
|
+
const { lock, detectedDebug, forceDebug } = input;
|
|
16
|
+
if (lock.state === "running") {
|
|
17
|
+
return {
|
|
18
|
+
action: "restart",
|
|
19
|
+
previousPid: lock.pid,
|
|
20
|
+
forcePort: lock.port,
|
|
21
|
+
clearStaleLock: false,
|
|
22
|
+
// `--debug` forces it on; otherwise preserve whatever the running daemon had.
|
|
23
|
+
debug: forceDebug || detectedDebug
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
action: "start_fresh",
|
|
28
|
+
previousPid: null,
|
|
29
|
+
forcePort: null,
|
|
30
|
+
clearStaleLock: lock.state === "stale",
|
|
31
|
+
// Nothing was running, so there is no debug state to preserve — only the
|
|
32
|
+
// explicit `--debug` request can turn it on.
|
|
33
|
+
debug: forceDebug
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { waitForDaemonReady } from "./probes.js";
|
|
3
|
+
import { buildSelfSpawnCommand } from "./selfSpawn.js";
|
|
4
|
+
import { daemonSpawnEnv } from "./spawnEnv.js";
|
|
5
|
+
/**
|
|
6
|
+
* Spawn the daemon as a detached background process and wait until it is
|
|
7
|
+
* reachable. Shared by `copillm start --detach` and `copillm restart` so the
|
|
8
|
+
* two paths can never drift in how they launch the background daemon.
|
|
9
|
+
*
|
|
10
|
+
* `forcePort` pins the daemon back onto a specific port (used by `restart` to
|
|
11
|
+
* rebind the port the previous daemon was running on); when null the daemon
|
|
12
|
+
* falls back to the normal configured-port scan.
|
|
13
|
+
*/
|
|
14
|
+
export async function spawnDetachedDaemon(input) {
|
|
15
|
+
const daemonCommand = buildSelfSpawnCommand("daemon", input.debug ? ["--debug"] : []);
|
|
16
|
+
const child = spawn(daemonCommand.command, daemonCommand.args, {
|
|
17
|
+
detached: true,
|
|
18
|
+
stdio: "ignore",
|
|
19
|
+
env: daemonSpawnEnv(input.debug, { port: input.forcePort ?? null })
|
|
20
|
+
});
|
|
21
|
+
child.unref();
|
|
22
|
+
const started = await waitForDaemonReady(child.pid ?? null, 8_000);
|
|
23
|
+
if (!started) {
|
|
24
|
+
throw new Error("Detached daemon start timed out.");
|
|
25
|
+
}
|
|
26
|
+
return started;
|
|
27
|
+
}
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { debugLogPath } from "../../config/home.js";
|
|
2
2
|
import { currentDebugLogPath } from "../shared/debug.js";
|
|
3
|
-
export function daemonSpawnEnv(debug) {
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export function daemonSpawnEnv(debug, options) {
|
|
4
|
+
// Always return a fresh object so callers that inject a port (e.g. `restart`
|
|
5
|
+
// pinning the daemon back onto its previous port) never mutate the live
|
|
6
|
+
// `process.env`.
|
|
7
|
+
const env = debug
|
|
8
|
+
? {
|
|
9
|
+
...process.env,
|
|
10
|
+
COPILLM_LOG_LEVEL: "debug",
|
|
11
|
+
COPILLM_LOG_FILE: currentDebugLogPath(true) ?? debugLogPath()
|
|
12
|
+
}
|
|
13
|
+
: { ...process.env };
|
|
14
|
+
if (options?.port != null) {
|
|
15
|
+
env.COPILLM_PORT = String(options.port);
|
|
6
16
|
}
|
|
7
|
-
return
|
|
8
|
-
...process.env,
|
|
9
|
-
COPILLM_LOG_LEVEL: "debug",
|
|
10
|
-
COPILLM_LOG_FILE: currentDebugLogPath(true) ?? debugLogPath()
|
|
11
|
-
};
|
|
17
|
+
return env;
|
|
12
18
|
}
|
|
@@ -6,7 +6,7 @@ export function displayHomePath(p) {
|
|
|
6
6
|
return p;
|
|
7
7
|
}
|
|
8
8
|
export function formatStartBanner(input) {
|
|
9
|
-
const verb = input.mode === "foreground" ? "listening on" : "running on";
|
|
9
|
+
const verb = input.mode === "foreground" ? "listening on" : input.mode === "restarted" ? "restarted on" : "running on";
|
|
10
10
|
const lines = [];
|
|
11
11
|
const debugSuffix = input.debug ? " [debug]" : "";
|
|
12
12
|
const modeSuffix = input.mode === "already_running" ? " (already running)" : "";
|
package/dist/cli/packageInfo.js
CHANGED