@aron98/load-lock 1.4.0 → 1.6.0
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 +61 -14
- package/dist/adapters/opencode/plugin.js +15 -1
- package/dist/adapters/opencode/tui.js +7 -2
- package/dist/cli/copies.d.ts +3 -10
- package/dist/cli/copies.js +32 -44
- package/dist/cli/hook.js +48 -4
- package/dist/cli/install.d.ts +2 -0
- package/dist/cli/install.js +68 -14
- package/dist/cli/main.js +108 -211
- package/dist/cli/service.d.ts +56 -0
- package/dist/cli/service.js +261 -0
- package/dist/cli/setup.d.ts +131 -0
- package/dist/cli/setup.js +942 -0
- package/dist/cli/update.d.ts +57 -0
- package/dist/cli/update.js +287 -0
- package/dist/core/daemon.d.ts +29 -3
- package/dist/core/daemon.js +111 -11
- package/dist/core/persist.d.ts +3 -0
- package/dist/core/persist.js +49 -10
- package/dist/core/state.d.ts +9 -0
- package/dist/core/state.js +41 -3
- package/dist/dashboard/feed.d.ts +23 -0
- package/dist/dashboard/feed.js +64 -2
- package/dist/dashboard/frame.js +2 -0
- package/dist/dashboard/view.d.ts +11 -0
- package/dist/dashboard/view.js +13 -0
- package/dist/mcp/server.d.ts +11 -3
- package/dist/mcp/server.js +38 -6
- package/dist/policy/text.d.ts +13 -3
- package/dist/policy/text.js +14 -3
- package/dist/shared/agent-guide.d.ts +3 -0
- package/dist/shared/agent-guide.js +16 -0
- package/dist/shared/client.js +16 -4
- package/dist/shared/drift.d.ts +23 -0
- package/dist/shared/drift.js +95 -0
- package/dist/shared/identity.d.ts +51 -0
- package/dist/shared/identity.js +116 -0
- package/dist/shared/installs.d.ts +39 -0
- package/dist/shared/installs.js +101 -0
- package/dist/shared/pkginfo.d.ts +25 -0
- package/dist/shared/pkginfo.js +70 -0
- package/dist/shared/protocol.d.ts +22 -0
- package/dist/shared/protocol.js +15 -0
- package/dist/shared/runtimes.d.ts +41 -0
- package/dist/shared/runtimes.js +81 -0
- package/dist/shared/version.d.ts +6 -0
- package/dist/shared/version.js +9 -1
- package/dist/test/ace-mcp.test.d.ts +1 -0
- package/dist/test/ace-mcp.test.js +157 -0
- package/dist/test/ace-opencode.test.d.ts +1 -0
- package/dist/test/ace-opencode.test.js +287 -0
- package/dist/test/check-pins.test.d.ts +1 -0
- package/dist/test/check-pins.test.js +108 -0
- package/dist/test/compat.test.d.ts +1 -0
- package/dist/test/compat.test.js +190 -0
- package/dist/test/hook.test.js +179 -1
- package/dist/test/identity.test.d.ts +1 -0
- package/dist/test/identity.test.js +155 -0
- package/dist/test/install.test.js +135 -1
- package/dist/test/launcher.test.d.ts +1 -0
- package/dist/test/launcher.test.js +222 -0
- package/dist/test/mcp.test.js +76 -7
- package/dist/test/plugin.test.d.ts +1 -0
- package/dist/test/plugin.test.js +144 -0
- package/dist/test/reconnect.test.d.ts +1 -0
- package/dist/test/reconnect.test.js +389 -0
- package/dist/test/runtimes.test.d.ts +1 -0
- package/dist/test/runtimes.test.js +282 -0
- package/dist/test/setup.test.d.ts +1 -0
- package/dist/test/setup.test.js +354 -0
- package/dist/test/update-command.test.d.ts +1 -0
- package/dist/test/update-command.test.js +679 -0
- package/dist/test/update.test.js +33 -1
- package/docs/ADAPTERS.md +3 -2
- package/docs/AGENT_GUIDE.md +48 -2
- package/docs/ARCHITECTURE.md +122 -7
- package/docs/COMPATIBILITY.md +156 -13
- package/docs/OPERATIONS.md +407 -21
- package/package.json +3 -2
- package/skills/load-lock/SKILL.md +62 -0
package/dist/cli/main.js
CHANGED
|
@@ -2,25 +2,29 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* load-lock CLI: inspection, recovery, scriptable flows, execution helper.
|
|
4
4
|
*
|
|
5
|
-
* Exit codes: 0 ok;
|
|
6
|
-
* (queued/registered); 69
|
|
5
|
+
* Exit codes: 0 ok; 1 update available (`update --check`); 64 usage; 65
|
|
6
|
+
* protocol/data error; 68 not granted yet (queued/registered); 69
|
|
7
|
+
* offer/claim failed; 70 internal.
|
|
7
8
|
*/
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
9
|
-
import { existsSync, readFileSync
|
|
10
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
10
11
|
import { join } from "node:path";
|
|
11
|
-
import { connect } from "node:net";
|
|
12
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
13
12
|
import { LLClient, withClient } from "../shared/client.js";
|
|
14
13
|
import { ProtocolError } from "../shared/protocol.js";
|
|
15
14
|
import { waitForTurn } from "../shared/wait-turn.js";
|
|
16
15
|
import { waitForTrackedJob } from "../shared/wait-completion.js";
|
|
17
16
|
import { stateDir, socketPath } from "../core/persist.js";
|
|
17
|
+
import { loadRuntimes, readDaemonInfo } from "../shared/runtimes.js";
|
|
18
18
|
import { isWindows, probeTransport, killProcessGroup } from "../shared/transport.js";
|
|
19
19
|
import { packageVersion, compareVersions } from "../shared/version.js";
|
|
20
20
|
import { findInstalledCopies } from "./copies.js";
|
|
21
21
|
import { installCommand, uninstallCommand } from "./install.js";
|
|
22
22
|
import { hookCommand } from "./hook.js";
|
|
23
23
|
import { topCommand } from "./top.js";
|
|
24
|
+
import { awaitDaemonUpgraded, serviceRestart, serviceStart, serviceStop, ServiceError } from "./service.js";
|
|
25
|
+
import { updateCommand } from "./update.js";
|
|
26
|
+
import { applyPlan, claudeCodeMode, formatInventory, formatPlan, inventory, inventoryHarness, planConverge, selfCopy } from "./setup.js";
|
|
27
|
+
import { sessionIdentity } from "../shared/identity.js";
|
|
24
28
|
// ---------------------------------------------------------------- arg parsing
|
|
25
29
|
const argv = process.argv.slice(2);
|
|
26
30
|
let flags = {};
|
|
@@ -35,7 +39,7 @@ for (let i = 0; i < argv.length; i++) {
|
|
|
35
39
|
const eq = a.indexOf("=");
|
|
36
40
|
if (eq !== -1)
|
|
37
41
|
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
38
|
-
else if (["json", "follow", "force", "admin", "quiet"].includes(a.slice(2)))
|
|
42
|
+
else if (["json", "follow", "force", "admin", "quiet", "check", "version", "yes"].includes(a.slice(2)))
|
|
39
43
|
flags[a.slice(2)] = true;
|
|
40
44
|
else if (i + 1 < argv.length && !argv[i + 1].startsWith("--"))
|
|
41
45
|
flags[a.slice(2)] = argv[++i];
|
|
@@ -62,90 +66,24 @@ function out(v, human) {
|
|
|
62
66
|
else
|
|
63
67
|
process.stdout.write((human ? human() : JSON.stringify(v, null, 2)) + "\n");
|
|
64
68
|
}
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return JSON.parse(readFileSync(join(dir, "daemon.json"), "utf8"));
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
69
|
+
// Session-derived default identity (issue #49): one id per harness session,
|
|
70
|
+
// so CLI-driven agents on one machine no longer share `cli:$USER@$HOSTNAME`
|
|
71
|
+
// (that remains the fallback when no session can be found).
|
|
72
|
+
let identity;
|
|
73
|
+
function sessionId() {
|
|
74
|
+
return (identity ??= sessionIdentity({
|
|
75
|
+
fallback: "cli:" + (process.env.USER || "user") + "@" + (process.env.HOSTNAME || "localhost"),
|
|
76
|
+
}));
|
|
78
77
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
* send clientVersion, so asking a daemon for its identity never orders it
|
|
82
|
-
* to self-restart (used by `service stop` and post-restart confirmation).
|
|
83
|
-
* Best-effort: resolves null on any failure.
|
|
84
|
-
*/
|
|
85
|
-
function rawHello(sockPath, timeoutMs = 2000) {
|
|
86
|
-
return new Promise((resolve) => {
|
|
87
|
-
let settled = false;
|
|
88
|
-
let buf = "";
|
|
89
|
-
const done = (v) => {
|
|
90
|
-
if (settled)
|
|
91
|
-
return;
|
|
92
|
-
settled = true;
|
|
93
|
-
try {
|
|
94
|
-
sock.destroy();
|
|
95
|
-
}
|
|
96
|
-
catch {
|
|
97
|
-
/* ignore */
|
|
98
|
-
}
|
|
99
|
-
resolve(v);
|
|
100
|
-
};
|
|
101
|
-
const sock = connect(sockPath, () => {
|
|
102
|
-
sock.write(JSON.stringify({ id: 1, method: "hello", params: {} }) + "\n");
|
|
103
|
-
});
|
|
104
|
-
sock.on("data", (chunk) => {
|
|
105
|
-
buf += chunk.toString("utf8");
|
|
106
|
-
const idx = buf.indexOf("\n");
|
|
107
|
-
if (idx === -1)
|
|
108
|
-
return;
|
|
109
|
-
try {
|
|
110
|
-
const msg = JSON.parse(buf.slice(0, idx));
|
|
111
|
-
done(msg.ok ? msg.result : null);
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
done(null);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
sock.on("error", () => done(null));
|
|
118
|
-
const timer = setTimeout(() => done(null), timeoutMs);
|
|
119
|
-
timer.unref?.();
|
|
120
|
-
});
|
|
78
|
+
function defaultAgent() {
|
|
79
|
+
return sessionId().id;
|
|
121
80
|
}
|
|
122
|
-
/**
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (Date.now() >= deadline)
|
|
129
|
-
return false;
|
|
130
|
-
await delay(100);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* After ordering a self-restart, wait for the daemon to come back running
|
|
135
|
-
* at least `installed` (the version of this CLI copy). Resolves its hello
|
|
136
|
-
* result, or null if it did not come back (or an older/pre-handshake
|
|
137
|
-
* daemon answered again — e.g. the daemon's copy is not actually updated).
|
|
138
|
-
*/
|
|
139
|
-
async function awaitDaemonUpgraded(sockPath, timeoutMs, installed) {
|
|
140
|
-
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
141
|
-
for (;;) {
|
|
142
|
-
const h = await rawHello(sockPath, 1500);
|
|
143
|
-
if (h && typeof h.daemonVersion === "string" && compareVersions(h.daemonVersion, installed) >= 0)
|
|
144
|
-
return h;
|
|
145
|
-
if (Date.now() >= deadline)
|
|
146
|
-
return null;
|
|
147
|
-
await delay(250);
|
|
148
|
-
}
|
|
81
|
+
/** Readable holder label for a session-derived id (explicit ids label themselves). */
|
|
82
|
+
function defaultLabel() {
|
|
83
|
+
if (S("agent"))
|
|
84
|
+
return undefined;
|
|
85
|
+
const s = sessionId();
|
|
86
|
+
return s.label !== s.id ? s.label : undefined;
|
|
149
87
|
}
|
|
150
88
|
const HELP_GROUPS = [
|
|
151
89
|
{
|
|
@@ -177,12 +115,14 @@ const HELP_GROUPS = [
|
|
|
177
115
|
{ names: ["config", "get-config"], desc: "current configuration" },
|
|
178
116
|
{ names: ["configure"], desc: "change configuration (compare-and-swap): --reason required" },
|
|
179
117
|
{ names: ["dashboard", "top"], desc: "full-screen live dashboard, read-only (never acquires): [--once]" },
|
|
118
|
+
{ names: ["version"], desc: "print this CLI copy's version" },
|
|
180
119
|
],
|
|
181
120
|
},
|
|
182
121
|
{
|
|
183
122
|
title: "service & diagnostics",
|
|
184
123
|
items: [
|
|
185
|
-
{ names: ["service"], desc: "daemon lifecycle: start | stop | status (stop/start take --timeout <ms>)" },
|
|
124
|
+
{ names: ["service"], desc: "daemon lifecycle: start | stop | restart | status (stop/start/restart take --timeout <ms> for the stop phase)" },
|
|
125
|
+
{ names: ["update"], desc: "check npm for a newer version, install it via the detected package manager, then `service restart` (--check dry-run; refuses repo checkouts / opencode-managed copies)" },
|
|
186
126
|
{ names: ["doctor"], desc: "health report: state dir, socket, daemon, versions + copies, machine" },
|
|
187
127
|
{ names: ["recover"], desc: "list held permits with diagnostics; recover force-release --id <request-id>" },
|
|
188
128
|
],
|
|
@@ -190,9 +130,11 @@ const HELP_GROUPS = [
|
|
|
190
130
|
{
|
|
191
131
|
title: "setup & docs",
|
|
192
132
|
items: [
|
|
193
|
-
{ names: ["install"], desc: "install the load-lock adapter for a harness: --harness <name>" },
|
|
133
|
+
{ names: ["install"], desc: "install the load-lock adapter for a harness: --harness <name> (claude-code: with the plugin enabled, moves the loose wiring out; --legacy keeps it)" },
|
|
194
134
|
{ names: ["uninstall"], desc: "remove what install added: --harness <name>" },
|
|
135
|
+
{ names: ["setup"], desc: "inventory every load-lock integration across all harnesses and plan the recommended layout (dry run); --yes applies it: [--project <dir>] [--json]" },
|
|
195
136
|
{ names: ["hook"], desc: "harness hook handlers (called by integrations, not humans)" },
|
|
137
|
+
{ names: ["mcp"], desc: "run the MCP stdio server (same as the load-lock-mcp binary; started by harnesses, not humans)" },
|
|
196
138
|
{ names: ["agent-guide"], desc: "print the full agent guide" },
|
|
197
139
|
{ names: ["help"], desc: "this usage; also bare load-lock, --help, -h; help <command> for one line" },
|
|
198
140
|
],
|
|
@@ -216,10 +158,15 @@ function usage() {
|
|
|
216
158
|
for (const item of g.items)
|
|
217
159
|
lines.push(helpLine(item));
|
|
218
160
|
}
|
|
219
|
-
lines.push("", "exit codes: 0 ok, 64 usage, 65 protocol, 68 not granted yet, 69 offer/claim failed, 70 internal");
|
|
161
|
+
lines.push("", "exit codes: 0 ok, 1 update available (update --check), 64 usage, 65 protocol, 68 not granted yet, 69 offer/claim failed, 70 internal");
|
|
220
162
|
process.stdout.write(lines.join("\n") + "\n");
|
|
221
163
|
}
|
|
222
164
|
async function main() {
|
|
165
|
+
// `--version` prints this copy's version (same as the `version` command).
|
|
166
|
+
if (f["version"] === true) {
|
|
167
|
+
process.stdout.write(packageVersion() + "\n");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
223
170
|
// `--help`, `-h` and a bare `load-lock` all print the same usage as `help`.
|
|
224
171
|
if (f["help"] !== undefined || cmd === "-h" || (positional.length === 0 && argv.length === 0)) {
|
|
225
172
|
usage();
|
|
@@ -342,7 +289,7 @@ async function main() {
|
|
|
342
289
|
const r = await withClient((c) => c.request("acquire", {
|
|
343
290
|
pool: S("pool", "heavy"),
|
|
344
291
|
agentId: S("agent", defaultAgent()),
|
|
345
|
-
agentLabel: S("label") ||
|
|
292
|
+
agentLabel: S("label") || defaultLabel(),
|
|
346
293
|
labelPrefix: S("label-prefix") || undefined,
|
|
347
294
|
cwd: S("cwd", process.cwd()),
|
|
348
295
|
opKey: S("op-key") || fail("acquire: --op-key is required"),
|
|
@@ -452,130 +399,29 @@ async function main() {
|
|
|
452
399
|
// --------------------------------------------------------------- service
|
|
453
400
|
case "service": {
|
|
454
401
|
const sub = rest[0] ?? "status";
|
|
402
|
+
const phase = (e) => {
|
|
403
|
+
if (e instanceof ServiceError)
|
|
404
|
+
fail(e.message, e.exitCode);
|
|
405
|
+
throw e;
|
|
406
|
+
};
|
|
455
407
|
if (sub === "stop") {
|
|
456
|
-
//
|
|
457
|
-
//
|
|
458
|
-
|
|
459
|
-
// daemon.json (crash leftovers) is cleaned up either way.
|
|
460
|
-
const dir = stateDir();
|
|
461
|
-
const sock = socketPath();
|
|
462
|
-
const timeoutMs = N("timeout", 5000);
|
|
463
|
-
const info = readDaemonInfo(dir);
|
|
464
|
-
const answering = await probeTransport(sock);
|
|
465
|
-
if (!answering) {
|
|
466
|
-
// Nothing is listening: no daemon to stop. Clean crash leftovers.
|
|
467
|
-
try {
|
|
468
|
-
if (!isWindows() && existsSync(sock))
|
|
469
|
-
unlinkSync(sock);
|
|
470
|
-
}
|
|
471
|
-
catch {
|
|
472
|
-
/* ignore */
|
|
473
|
-
}
|
|
474
|
-
try {
|
|
475
|
-
rmSync(join(dir, "daemon.json"), { force: true });
|
|
476
|
-
}
|
|
477
|
-
catch {
|
|
478
|
-
/* ignore */
|
|
479
|
-
}
|
|
480
|
-
process.stdout.write("load-lock: no running daemon found (stale files cleaned)\n");
|
|
481
|
-
return;
|
|
482
|
-
}
|
|
483
|
-
// Identify the daemon: prefer daemon.json, else ask it directly
|
|
484
|
-
// (version-less hello — stopping must not order a self-restart).
|
|
485
|
-
let pid = typeof info?.pid === "number" ? info.pid : undefined;
|
|
486
|
-
if (!pid) {
|
|
487
|
-
const hello = await rawHello(sock, 1500);
|
|
488
|
-
if (hello && typeof hello.pid === "number")
|
|
489
|
-
pid = hello.pid;
|
|
490
|
-
}
|
|
491
|
-
if (!pid) {
|
|
492
|
-
fail(`the socket ${sock} answers but no pid could be identified (daemon did not respond and daemon.json names none) ` +
|
|
493
|
-
`— cannot stop safely; run \`load-lock doctor\``, 70);
|
|
494
|
-
}
|
|
495
|
-
let how = "SIGTERM";
|
|
496
|
-
let stopped = false;
|
|
497
|
-
try {
|
|
498
|
-
process.kill(pid, "SIGTERM");
|
|
499
|
-
}
|
|
500
|
-
catch (e) {
|
|
501
|
-
if (e?.code === "ESRCH")
|
|
502
|
-
stopped = true; // already gone
|
|
503
|
-
else
|
|
504
|
-
fail(`cannot signal daemon pid ${pid}: ${e?.message ?? e}`, 70);
|
|
505
|
-
}
|
|
506
|
-
if (!stopped)
|
|
507
|
-
stopped = await waitSocketFree(sock, timeoutMs);
|
|
508
|
-
if (!stopped) {
|
|
509
|
-
how = "SIGKILL";
|
|
510
|
-
try {
|
|
511
|
-
process.kill(pid, "SIGKILL");
|
|
512
|
-
}
|
|
513
|
-
catch {
|
|
514
|
-
/* already dead */
|
|
515
|
-
}
|
|
516
|
-
stopped = await waitSocketFree(sock, timeoutMs);
|
|
517
|
-
}
|
|
518
|
-
if (!stopped) {
|
|
519
|
-
fail(`daemon pid ${pid} did not stop within ${timeoutMs}ms even after SIGKILL — run \`load-lock doctor\``, 70);
|
|
520
|
-
}
|
|
521
|
-
try {
|
|
522
|
-
if (!isWindows() && existsSync(sock))
|
|
523
|
-
unlinkSync(sock);
|
|
524
|
-
}
|
|
525
|
-
catch {
|
|
526
|
-
/* ignore */
|
|
527
|
-
}
|
|
528
|
-
try {
|
|
529
|
-
rmSync(join(dir, "daemon.json"), { force: true });
|
|
530
|
-
}
|
|
531
|
-
catch {
|
|
532
|
-
/* ignore */
|
|
533
|
-
}
|
|
534
|
-
process.stdout.write(`load-lock: daemon (pid ${pid}) stopped${how === "SIGKILL" ? " (required SIGKILL after SIGTERM)" : ""}\n`);
|
|
408
|
+
// Hardened stop: SIGTERM → bounded wait → SIGKILL → verified-free
|
|
409
|
+
// endpoint; crash leftovers cleaned either way.
|
|
410
|
+
process.stdout.write(`load-lock: ${await serviceStop(N("timeout", 5000)).catch(phase)}\n`);
|
|
535
411
|
return;
|
|
536
412
|
}
|
|
537
413
|
if (sub === "start") {
|
|
538
414
|
// Connecting auto-spawns a detached daemon (see shared/client.ts);
|
|
539
415
|
// hello confirms it is up. Refusals name the conflicting pid.
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
const up = await awaitDaemonUpgraded(sock, 8000, installed);
|
|
550
|
-
if (up) {
|
|
551
|
-
process.stdout.write(`load-lock: daemon self-restarted ${r.pid} -> ${up.pid} (v${dv} -> v${up.daemonVersion}), running on ${sock}\n`);
|
|
552
|
-
}
|
|
553
|
-
else {
|
|
554
|
-
process.stdout.write(`load-lock: daemon running on pid ${r.pid} (${sock}), daemon v${dv} — older than this CLI v${installed}; ` +
|
|
555
|
-
`self-restart ordered but not confirmed (its copy may not be updated in place — see docs/OPERATIONS.md)\n`);
|
|
556
|
-
}
|
|
557
|
-
return;
|
|
558
|
-
}
|
|
559
|
-
process.stdout.write(`load-lock: daemon running on pid ${r.pid} (${sock})${dv ? `, daemon v${dv}` : ""}\n`);
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
catch (e) {
|
|
563
|
-
if (await probeTransport(sock)) {
|
|
564
|
-
// Something owns the endpoint. Name it.
|
|
565
|
-
const hello = await rawHello(sock, 1500);
|
|
566
|
-
if (hello && typeof hello.pid === "number") {
|
|
567
|
-
fail(`start refused: socket answered from pid ${hello.pid} — another daemon owns ${sock}; ` +
|
|
568
|
-
`if it is stale, run \`load-lock service stop\` first`, 70);
|
|
569
|
-
}
|
|
570
|
-
const info = readDaemonInfo(stateDir());
|
|
571
|
-
if (info && typeof info.pid === "number") {
|
|
572
|
-
fail(`socket ${sock} answers but the daemon did not respond (daemon.json names pid ${info.pid}) — likely hung; ` +
|
|
573
|
-
`run \`load-lock service stop\``, 70);
|
|
574
|
-
}
|
|
575
|
-
fail(`start refused: ${sock} is in use but nothing responsive owns it — run \`load-lock doctor\``, 70);
|
|
576
|
-
}
|
|
577
|
-
throw e;
|
|
578
|
-
}
|
|
416
|
+
process.stdout.write(`load-lock: ${await serviceStart(N("timeout", 10_000)).catch(phase)}\n`);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
if (sub === "restart") {
|
|
420
|
+
// Hardened stop, then honest start — both phases reported; a
|
|
421
|
+
// failure names the phase and reason (issue #38).
|
|
422
|
+
const r = await serviceRestart(N("timeout", 5000), 10_000).catch(phase);
|
|
423
|
+
process.stdout.write(`load-lock: ${r.stop}\nload-lock: ${r.start}\n`);
|
|
424
|
+
return;
|
|
579
425
|
}
|
|
580
426
|
if (sub === "status") {
|
|
581
427
|
const r = (await withClient((c) => c.request("hello", {})));
|
|
@@ -595,9 +441,23 @@ async function main() {
|
|
|
595
441
|
});
|
|
596
442
|
return;
|
|
597
443
|
}
|
|
598
|
-
fail(`service: unknown subcommand "${sub}" (use start | stop | status)`);
|
|
444
|
+
fail(`service: unknown subcommand "${sub}" (use start | stop | restart | status)`);
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
// ---------------------------------------------------------------- update
|
|
448
|
+
case "update": {
|
|
449
|
+
// issue #38: registry check → classify self → apply via the detected
|
|
450
|
+
// package manager (or refuse honestly) → `service restart` on
|
|
451
|
+
// success. Exit code comes from updateCommand (1 = update available
|
|
452
|
+
// under --check; 70 = failure/refusal in apply mode).
|
|
453
|
+
const code = await updateCommand({ check: B("check"), json: B("json") });
|
|
454
|
+
if (code !== 0)
|
|
455
|
+
process.exit(code);
|
|
599
456
|
return;
|
|
600
457
|
}
|
|
458
|
+
case "version":
|
|
459
|
+
process.stdout.write(packageVersion() + "\n");
|
|
460
|
+
return;
|
|
601
461
|
case "doctor": {
|
|
602
462
|
const report = {};
|
|
603
463
|
report.node = process.version;
|
|
@@ -652,12 +512,25 @@ async function main() {
|
|
|
652
512
|
// when their versions drift apart.
|
|
653
513
|
const copies = findInstalledCopies();
|
|
654
514
|
report.copies = copies;
|
|
515
|
+
// Runtime copies the daemon has seen via the handshake (issue #52),
|
|
516
|
+
// and the copy the daemon itself runs from.
|
|
517
|
+
const dinfo = readDaemonInfo(dir);
|
|
518
|
+
if (dinfo?.packageRoot)
|
|
519
|
+
report.daemonRuntime = { path: dinfo.packageRoot, version: dinfo.version ?? null };
|
|
520
|
+
report.knownRuntimes = loadRuntimes(dir);
|
|
655
521
|
const versions = new Set(copies.map((c) => c.version).filter((v) => v !== null));
|
|
656
522
|
if (versions.size > 1) {
|
|
657
523
|
warnings.push(`multiple load-lock versions installed (${[...versions].sort().join(", ")}): ` +
|
|
658
524
|
copies.map((c) => `${c.source}=${c.version ?? "?"} (${c.path})`).join("; ") +
|
|
659
525
|
` — see docs/COMPATIBILITY.md "Multiple copies of load-lock" for which copy is authoritative for what`);
|
|
660
526
|
}
|
|
527
|
+
// Claude Code wiring mode (#62): plugin, loose install, both or none.
|
|
528
|
+
const ccMode = claudeCodeMode(inventoryHarness("claude-code", { project: process.cwd() }));
|
|
529
|
+
report.claudeCodeMode = ccMode;
|
|
530
|
+
if (ccMode === "both") {
|
|
531
|
+
warnings.push("Claude Code: the plugin and the loose `load-lock install` wiring are both active (harmless, but two MCP servers); " +
|
|
532
|
+
"`load-lock install --harness claude-code` or `load-lock setup --yes` moves the loose entries out");
|
|
533
|
+
}
|
|
661
534
|
report.warnings = warnings;
|
|
662
535
|
out(report, () => JSON.stringify(report, null, 2));
|
|
663
536
|
return;
|
|
@@ -694,15 +567,39 @@ async function main() {
|
|
|
694
567
|
json: B("json"),
|
|
695
568
|
global: true,
|
|
696
569
|
binDir: import.meta.dirname ? join(import.meta.dirname, "..") : undefined,
|
|
570
|
+
legacy: B("legacy"),
|
|
697
571
|
});
|
|
698
572
|
return;
|
|
699
573
|
case "uninstall":
|
|
700
574
|
await uninstallCommand({ harness: S("harness"), project: S("project", process.cwd()) });
|
|
701
575
|
return;
|
|
576
|
+
case "setup": {
|
|
577
|
+
const project = S("project", process.cwd());
|
|
578
|
+
const inv = inventory({ project });
|
|
579
|
+
const plan = planConverge(inv, selfCopy());
|
|
580
|
+
if (!B("yes")) {
|
|
581
|
+
out({ inventory: inv, plan }, () => formatInventory(inv) + "\n" + formatPlan(plan));
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const binDir = import.meta.dirname ? join(import.meta.dirname, "..") : undefined;
|
|
585
|
+
const applied = await applyPlan(inv, plan, { project, binDir });
|
|
586
|
+
const after = inventory({ project });
|
|
587
|
+
out({ inventory: after, plan, applied }, () => formatInventory(after) + "\n" + formatPlan(plan, applied));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
702
590
|
// ------------------------------------------------------------------ hooks
|
|
703
591
|
case "hook":
|
|
704
592
|
await hookCommand(rest);
|
|
705
593
|
return;
|
|
594
|
+
// ------------------------------------------------------------ mcp server
|
|
595
|
+
// Same server as the `load-lock-mcp` bin, reachable through the default
|
|
596
|
+
// `load-lock` bin so `npx -y @aron98/load-lock@<pin> mcp` works (ACE, #65).
|
|
597
|
+
// Imported lazily: other commands never load the MCP SDK.
|
|
598
|
+
case "mcp": {
|
|
599
|
+
const { runStdioMcp } = await import("../mcp/server.js");
|
|
600
|
+
await runStdioMcp();
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
706
603
|
case "agent-guide": {
|
|
707
604
|
const p = join(import.meta.dirname ?? ".", "..", "..", "docs", "AGENT_GUIDE.md");
|
|
708
605
|
process.stdout.write(readFileSync(existsSync(p) ? p : join(import.meta.dirname ?? ".", "..", "..", "docs", "AGENT_GUIDE.md"), "utf8"));
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/** A service-phase failure with the exit code the CLI should use. */
|
|
2
|
+
export declare class ServiceError extends Error {
|
|
3
|
+
readonly exitCode: number;
|
|
4
|
+
constructor(message: string, exitCode?: number);
|
|
5
|
+
}
|
|
6
|
+
/** daemon.json contents, or null when missing/unreadable. */
|
|
7
|
+
export declare function readDaemonInfo(dir: string): {
|
|
8
|
+
pid?: number;
|
|
9
|
+
startedAt?: number;
|
|
10
|
+
socket?: string;
|
|
11
|
+
} | null;
|
|
12
|
+
/**
|
|
13
|
+
* Version-less hello over a raw socket connection: deliberately does NOT
|
|
14
|
+
* send clientVersion, so asking a daemon for its identity never orders it
|
|
15
|
+
* to self-restart (used by `service stop` and post-restart confirmation).
|
|
16
|
+
* Best-effort: resolves null on any failure.
|
|
17
|
+
*/
|
|
18
|
+
export declare function rawHello(sockPath: string, timeoutMs?: number): Promise<{
|
|
19
|
+
pid?: number;
|
|
20
|
+
daemonVersion?: string;
|
|
21
|
+
protocolVersion?: number;
|
|
22
|
+
} | null>;
|
|
23
|
+
/**
|
|
24
|
+
* After ordering a self-restart, wait for the daemon to come back running
|
|
25
|
+
* at least `installed` (the version of this CLI copy). Resolves its hello
|
|
26
|
+
* result, or null if it did not come back (or an older/pre-handshake
|
|
27
|
+
* daemon answered again — e.g. the daemon's copy is not actually updated).
|
|
28
|
+
*/
|
|
29
|
+
export declare function awaitDaemonUpgraded(sockPath: string, timeoutMs: number, installed: string): Promise<{
|
|
30
|
+
pid?: number;
|
|
31
|
+
daemonVersion?: string;
|
|
32
|
+
} | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Hardened stop: signal the daemon, wait bounded for its socket to go
|
|
35
|
+
* silent, escalate to SIGKILL, and only report success once the endpoint
|
|
36
|
+
* is verifiably free. A leftover silent socket file or daemon.json (crash
|
|
37
|
+
* leftovers) is cleaned up either way. Returns the report line; throws
|
|
38
|
+
* ServiceError on failure.
|
|
39
|
+
*/
|
|
40
|
+
export declare function serviceStop(timeoutMs: number): Promise<string>;
|
|
41
|
+
/**
|
|
42
|
+
* Honest start: connecting auto-spawns a detached daemon (see
|
|
43
|
+
* shared/client.ts); hello confirms it is up. Refusals name the
|
|
44
|
+
* conflicting pid. Returns the report line; throws ServiceError on
|
|
45
|
+
* failure.
|
|
46
|
+
*/
|
|
47
|
+
export declare function serviceStart(timeoutMs: number): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* `service restart` (issue #38): hardened stop, then honest start — both
|
|
50
|
+
* phases reported; a failure names the phase and reason. Stopping when
|
|
51
|
+
* nothing runs is fine: the start phase simply spawns a fresh daemon.
|
|
52
|
+
*/
|
|
53
|
+
export declare function serviceRestart(stopTimeoutMs?: number, startTimeoutMs?: number): Promise<{
|
|
54
|
+
stop: string;
|
|
55
|
+
start: string;
|
|
56
|
+
}>;
|