cyberlegion 0.1.0 → 0.3.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/bin/cyberlegion.mjs +2 -1
- package/dist/cli.d.mts +12 -1
- package/dist/cli.mjs +328 -265
- package/dist/index.d.mts +105 -286
- package/dist/index.mjs +4 -28
- package/dist/watch-BZmEAqUo.mjs +1791 -0
- package/package.json +7 -5
- package/readme.md +35 -30
- package/dist/watch-BuYsdwhx.mjs +0 -1609
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { A as
|
|
2
|
+
import { A as bumpLastSeen, B as registerStanding, C as nudgeUnit, D as selectSessionAdapter, E as spawnAndWake, F as loadAgent, G as resolveStandingOwner, H as resolvePresence, I as prune, J as listAgentDefs, K as touch, L as realExec, M as clearPresence, N as detectHarness, O as DELIVERY_DOORBELL, P as listAgents, R as reconcile, S as focusUnit, V as resolveAgent, W as resolveSelfId, Y as resolveAgentDef, _ as install, a as emit, b as resolveSpawnLaunch, c as toonList, d as deleteMessage, et as resolveRoot, f as inbox, g as send, h as resolveBody, i as injectInbox, j as claimPresence, k as wakeRecipient, l as toonObject, m as readAck, n as awaitReply, o as fail, p as peek, q as normalizeMuxEnv, r as FileStore, s as nextStep, t as watchMail, u as ack, v as decommission, w as readUnit, x as clearUnit, z as register } from "./watch-BZmEAqUo.mjs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
4
|
import { Command, Option } from "commander";
|
|
5
|
+
import { currentPane, probeMultiplexer } from "cyber-mux";
|
|
5
6
|
//#region src/admin.ts
|
|
6
7
|
/**
|
|
7
8
|
* Merge one store's registry + mailboxes + briefs into another (skip agent ids already present at
|
|
@@ -37,6 +38,41 @@ function migrateStore(from, to) {
|
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
//#endregion
|
|
41
|
+
//#region src/cli-input.ts
|
|
42
|
+
/**
|
|
43
|
+
* Translate `unit spawn`'s options into the spawn input and its wake decision — resolving an
|
|
44
|
+
* `--agent`/`--agent-file` def into the harness and composed launch command, with an explicit
|
|
45
|
+
* `--harness` overriding the def's own.
|
|
46
|
+
*
|
|
47
|
+
* Throws when no harness can be resolved, so the CLI's own `fail()` still renders it.
|
|
48
|
+
*/
|
|
49
|
+
function spawnCommandInput(opts) {
|
|
50
|
+
const { harness, command } = resolveSpawnLaunch({
|
|
51
|
+
agent: opts.agent,
|
|
52
|
+
agentFile: opts.agentFile,
|
|
53
|
+
harness: opts.harness
|
|
54
|
+
});
|
|
55
|
+
if (!harness) throw new Error("unit spawn needs --harness, or --agent/--agent-file resolving one");
|
|
56
|
+
return {
|
|
57
|
+
input: {
|
|
58
|
+
harness,
|
|
59
|
+
command,
|
|
60
|
+
task: opts.task,
|
|
61
|
+
briefFile: opts.briefFile,
|
|
62
|
+
handle: opts.handle,
|
|
63
|
+
branch: opts.branch,
|
|
64
|
+
worktreePath: opts.worktreePath,
|
|
65
|
+
cwd: opts.cwd,
|
|
66
|
+
at: opts.at
|
|
67
|
+
},
|
|
68
|
+
noWake: opts.wake === false
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** What `unit read` prints: the raw scrape, or the JSON envelope under `--format json`. */
|
|
72
|
+
function readCommandOutput(format, result) {
|
|
73
|
+
return format === "json" ? JSON.stringify(result, null, 2) : result.output;
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
40
76
|
//#region src/cli.ts
|
|
41
77
|
const VERSION = "0.0.0";
|
|
42
78
|
function ctxOf(opts) {
|
|
@@ -50,55 +86,18 @@ function formatOf(opts) {
|
|
|
50
86
|
}
|
|
51
87
|
function requireSelf(ctx) {
|
|
52
88
|
const id = resolveSelfId(ctx);
|
|
53
|
-
if (!id) fail("no identity in this session — run `cyberlegion
|
|
89
|
+
if (!id) fail("no identity in this session — run `cyberlegion unit register` first");
|
|
54
90
|
bumpLastSeen(ctx, id);
|
|
55
91
|
return id;
|
|
56
92
|
}
|
|
57
|
-
function resolveTarget(ctx, ref) {
|
|
58
|
-
const agent = resolveAgent(ctx.store, ref);
|
|
59
|
-
const pane = agent.tmux?.pane ?? ctx.store.findPaneByAgentId(agent.id);
|
|
60
|
-
if (!pane) fail(`unit "${ref}" has no known session pane`);
|
|
61
|
-
return { id: pane };
|
|
62
|
-
}
|
|
63
93
|
function withGlobals(cmd) {
|
|
64
94
|
return cmd.option("--space <path>", "isolate the hub root (overrides the global hub / $CYBERLEGION_ROOT)").addOption(new Option("--format <format>", "output format").choices(["toon", "json"]).default("toon"));
|
|
65
95
|
}
|
|
66
96
|
const program = new Command();
|
|
67
|
-
program.name("cyberlegion").description("Harness-agnostic agent session spawning
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
handle: opts.handle,
|
|
72
|
-
harness: opts.harness
|
|
73
|
-
});
|
|
74
|
-
emit(formatOf(opts), {
|
|
75
|
-
toon: toonObject({
|
|
76
|
-
id: rec.id,
|
|
77
|
-
handle: rec.handle,
|
|
78
|
-
harness: rec.harness ?? "-",
|
|
79
|
-
status: rec.status
|
|
80
|
-
}),
|
|
81
|
-
json: rec
|
|
82
|
-
});
|
|
83
|
-
});
|
|
84
|
-
withGlobals(identity.command("whoami")).description("print this session own identity").action((opts) => {
|
|
85
|
-
const ctx = ctxOf(opts);
|
|
86
|
-
const id = resolveSelfId(ctx);
|
|
87
|
-
if (!id) fail("no identity in this session — run `cyberlegion identity register` first");
|
|
88
|
-
const rec = loadAgent(ctx.store, id);
|
|
89
|
-
if (!rec) fail(`registered self id "${id}" has no agent record`);
|
|
90
|
-
emit(formatOf(opts), {
|
|
91
|
-
toon: toonObject({
|
|
92
|
-
id: rec.id,
|
|
93
|
-
handle: rec.handle,
|
|
94
|
-
harness: rec.harness ?? "-",
|
|
95
|
-
status: rec.status
|
|
96
|
-
}),
|
|
97
|
-
json: rec
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
withGlobals(identity.command("owner")).description("mint (or refresh) a standing, session-independent owner inbox").option("--handle <name>", "the owner handle to claim").action((opts) => {
|
|
101
|
-
const ctx = ctxOf(opts);
|
|
97
|
+
program.name("cyberlegion").description("Harness-agnostic agent session spawning and messaging over the filesystem").version(VERSION).enablePositionalOptions();
|
|
98
|
+
const unit = program.command("unit").description("legion units — register, discover, spawn, and reap");
|
|
99
|
+
/** The standing-owner branch of `unit register --standing` (folds the old `identity owner`). */
|
|
100
|
+
function runStanding(ctx, opts) {
|
|
102
101
|
if (!opts.handle) {
|
|
103
102
|
const standing = listAgents(ctx.store).filter((a) => a.kind === "standing");
|
|
104
103
|
emit(formatOf(opts), {
|
|
@@ -119,7 +118,7 @@ withGlobals(identity.command("owner")).description("mint (or refresh) a standing
|
|
|
119
118
|
key: "status",
|
|
120
119
|
get: (a) => a.status
|
|
121
120
|
}
|
|
122
|
-
], `${standing.length}
|
|
121
|
+
], `${standing.length} standing`),
|
|
123
122
|
json: standing
|
|
124
123
|
});
|
|
125
124
|
return;
|
|
@@ -136,13 +135,94 @@ withGlobals(identity.command("owner")).description("mint (or refresh) a standing
|
|
|
136
135
|
}),
|
|
137
136
|
json: rec
|
|
138
137
|
});
|
|
138
|
+
}
|
|
139
|
+
withGlobals(unit.command("register")).description("register or refresh this session identity (or --standing: a session-independent owner inbox)").option("--handle <name>", "human handle for this agent").option("--harness <h>", "claude | cursor | codex (else auto-detected)").option("--standing", "mint a standing, session-independent owner inbox (bare, with no --handle: list them)").action((opts) => {
|
|
140
|
+
const ctx = ctxOf(opts);
|
|
141
|
+
if (opts.standing) {
|
|
142
|
+
runStanding(ctx, opts);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const rec = register(ctx, {
|
|
146
|
+
handle: opts.handle,
|
|
147
|
+
harness: opts.harness
|
|
148
|
+
});
|
|
149
|
+
emit(formatOf(opts), {
|
|
150
|
+
toon: toonObject({
|
|
151
|
+
id: rec.id,
|
|
152
|
+
handle: rec.handle,
|
|
153
|
+
harness: rec.harness ?? "-",
|
|
154
|
+
status: rec.status
|
|
155
|
+
}),
|
|
156
|
+
json: rec
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
withGlobals(unit.command("claim")).description("bind the caller's unit as a standing owner's presence; --show reads it, --clear unbinds").argument("<handle>", "standing owner handle").option("--clear", "unbind the presence (a no-op when nothing is bound)").option("--show", "print the bound presence instead of claiming").action((handle, opts) => {
|
|
160
|
+
const ctx = ctxOf(opts);
|
|
161
|
+
if (opts.show) {
|
|
162
|
+
let presence;
|
|
163
|
+
try {
|
|
164
|
+
presence = resolvePresence(ctx.store, handle);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
167
|
+
}
|
|
168
|
+
emit(formatOf(opts), {
|
|
169
|
+
toon: toonObject({ presence: presence?.id ?? "none" }),
|
|
170
|
+
json: { presence: presence?.id ?? null }
|
|
171
|
+
});
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
if (opts.clear) {
|
|
175
|
+
try {
|
|
176
|
+
clearPresence(ctx, handle);
|
|
177
|
+
} catch (err) {
|
|
178
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
179
|
+
}
|
|
180
|
+
emit(formatOf(opts), {
|
|
181
|
+
toon: toonObject({ presence: "none" }),
|
|
182
|
+
json: { presence: null }
|
|
183
|
+
});
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
let rec;
|
|
187
|
+
try {
|
|
188
|
+
rec = claimPresence(ctx, handle);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
191
|
+
}
|
|
192
|
+
emit(formatOf(opts), {
|
|
193
|
+
toon: toonObject({
|
|
194
|
+
owner: rec.handle,
|
|
195
|
+
presence: rec.presence
|
|
196
|
+
}),
|
|
197
|
+
json: {
|
|
198
|
+
owner: rec.handle,
|
|
199
|
+
presence: rec.presence
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
withGlobals(unit.command("whoami")).description("print this session own identity").action((opts) => {
|
|
204
|
+
const ctx = ctxOf(opts);
|
|
205
|
+
const id = resolveSelfId(ctx);
|
|
206
|
+
if (!id) fail("no identity in this session — run `cyberlegion unit register` first");
|
|
207
|
+
const rec = loadAgent(ctx.store, id);
|
|
208
|
+
if (!rec) fail(`registered self id "${id}" has no agent record`);
|
|
209
|
+
emit(formatOf(opts), {
|
|
210
|
+
toon: toonObject({
|
|
211
|
+
id: rec.id,
|
|
212
|
+
handle: rec.handle,
|
|
213
|
+
harness: rec.harness ?? "-",
|
|
214
|
+
status: rec.status
|
|
215
|
+
}),
|
|
216
|
+
json: rec
|
|
217
|
+
});
|
|
139
218
|
});
|
|
140
219
|
function runWho(opts) {
|
|
141
220
|
const ctx = ctxOf(opts);
|
|
142
221
|
touch(ctx);
|
|
222
|
+
if (opts.reconcile) reconcile(ctx, { adopt: true });
|
|
143
223
|
const agents = listAgents(ctx.store).filter((a) => opts.all || a.status !== "exited");
|
|
144
224
|
emit(formatOf(opts), {
|
|
145
|
-
toon: toonList("
|
|
225
|
+
toon: toonList("units", agents, [
|
|
146
226
|
{
|
|
147
227
|
key: "id",
|
|
148
228
|
get: (a) => a.id
|
|
@@ -158,14 +238,18 @@ function runWho(opts) {
|
|
|
158
238
|
{
|
|
159
239
|
key: "status",
|
|
160
240
|
get: (a) => a.status
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
key: "pane",
|
|
244
|
+
get: (a) => a.pane?.id ?? "-"
|
|
161
245
|
}
|
|
162
|
-
], `${agents.length}
|
|
246
|
+
], `${agents.length} units`),
|
|
163
247
|
json: agents
|
|
164
248
|
});
|
|
165
|
-
if (agents.length === 0) nextStep("cyberlegion
|
|
249
|
+
if (agents.length === 0) nextStep("cyberlegion unit register to join");
|
|
166
250
|
}
|
|
167
|
-
withGlobals(
|
|
168
|
-
withGlobals(
|
|
251
|
+
withGlobals(unit.command("who")).description("list the addressable units").option("--all", "include exited units").option("--reconcile", "live-probe the current mux: cull dead-pane records and adopt unbound harness-bearing panes before listing").action(runWho);
|
|
252
|
+
withGlobals(unit.command("prune")).description("mark dead units exited and sweep").action((opts) => {
|
|
169
253
|
const ctx = ctxOf(opts);
|
|
170
254
|
touch(ctx);
|
|
171
255
|
const changed = prune(ctx);
|
|
@@ -180,60 +264,44 @@ withGlobals(identity.command("prune")).description("mark dead agents exited and
|
|
|
180
264
|
json: changed
|
|
181
265
|
});
|
|
182
266
|
});
|
|
183
|
-
const session = program.command("session").description("warm peer session lifecycle over a multiplexer");
|
|
184
267
|
function defineSpawn(cmd) {
|
|
185
|
-
return withGlobals(cmd).description("launch a new peer session in its own git worktree (tmux or herdr)").option("--harness <h>", "claude | cursor | codex (required unless --agent/--agent-file resolves one)").option("--agent <name>", "resolve an agent def (.agents/agents/<name>.md) for harness/model/instructions").option("--agent-file <path>", "read an exact agent def file instead of resolving by name").option("--task <text>", "brief text, or - for stdin").option("--brief-file <path>", "read the brief from a file").option("--handle <name>", "handle for the new peer").option("--branch <name>", "branch for the new worktree (default cyberlegion/unit-<id>)").option("--worktree-path <path>", "where to check out the new worktree").option("--cwd <path>", "spawn the session in an existing directory; create no worktree (mutually exclusive with --branch/--worktree-path)").addOption(new Option("--at <placement>", "where to open the new session").choices([
|
|
268
|
+
return withGlobals(cmd).description("launch a new peer session in its own git worktree (tmux or herdr)").option("--harness <h>", "claude | cursor | codex (required unless --agent/--agent-file resolves one)").option("--agent <name>", "resolve an agent def (.agents/agents/<name>.md) for harness/model/instructions").option("--agent-file <path>", "read an exact agent def file instead of resolving by name").option("--task <text>", "brief text, or - for stdin").option("--brief-file <path>", "read the brief from a file").option("--handle <name>", "handle for the new peer").option("--branch <name>", "branch for the new worktree (default cyberlegion/unit-<id>)").option("--worktree-path <path>", "where to check out the new worktree").option("--cwd <path>", "spawn the session in an existing directory; create no worktree (mutually exclusive with --branch/--worktree-path)").addOption(new Option("--at <placement>", "where to open the new session (default: new-worktree → workspace, --cwd → tab)").choices([
|
|
186
269
|
"pane:right",
|
|
187
270
|
"pane:down",
|
|
188
271
|
"tab",
|
|
189
|
-
"window",
|
|
190
272
|
"workspace"
|
|
191
|
-
]).
|
|
273
|
+
])).option("--no-wake", "suppress the first-turn doorbell (spawn idle; the caller drives the first turn itself)").action(async (opts) => {
|
|
192
274
|
const ctx = ctxOf(opts);
|
|
193
275
|
touch(ctx);
|
|
194
|
-
let
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
def = resolveAgentDef({
|
|
200
|
-
name: opts.agent,
|
|
201
|
-
file: opts.agentFile
|
|
202
|
-
});
|
|
203
|
-
} catch (err) {
|
|
204
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
205
|
-
}
|
|
206
|
-
const realized = realizeLaunch(def, { harness: opts.harness });
|
|
207
|
-
harness = realized.harness;
|
|
208
|
-
command = realized.command;
|
|
276
|
+
let spawnInput;
|
|
277
|
+
try {
|
|
278
|
+
spawnInput = spawnCommandInput(opts);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
209
281
|
}
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
harness,
|
|
213
|
-
command,
|
|
214
|
-
task: opts.task,
|
|
215
|
-
briefFile: opts.briefFile,
|
|
216
|
-
handle: opts.handle,
|
|
217
|
-
branch: opts.branch,
|
|
218
|
-
worktreePath: opts.worktreePath,
|
|
219
|
-
cwd: opts.cwd,
|
|
220
|
-
at: opts.at
|
|
221
|
-
});
|
|
282
|
+
const res = await spawnAndWake(ctx, spawnInput.input, { noWake: spawnInput.noWake });
|
|
283
|
+
if (res.warning) console.error(`first-turn doorbell not confirmed (peer still spawned; nudge it manually): ${res.warning}`);
|
|
222
284
|
emit(formatOf(opts), {
|
|
223
285
|
toon: toonObject({
|
|
224
286
|
spawned: res.agent.id,
|
|
225
287
|
handle: res.agent.handle,
|
|
226
288
|
harness: res.agent.harness,
|
|
227
289
|
worktree: res.agent.worktree?.root,
|
|
228
|
-
pane: res.pane
|
|
290
|
+
pane: res.pane,
|
|
291
|
+
rung: res.rung
|
|
229
292
|
}),
|
|
230
|
-
json:
|
|
293
|
+
json: {
|
|
294
|
+
agent: res.agent,
|
|
295
|
+
pane: res.pane,
|
|
296
|
+
launch: res.launch,
|
|
297
|
+
rung: res.rung
|
|
298
|
+
}
|
|
231
299
|
});
|
|
232
|
-
nextStep(`cyberlegion
|
|
300
|
+
nextStep(`cyberlegion unit read ${res.agent.id}`);
|
|
233
301
|
});
|
|
234
302
|
}
|
|
235
|
-
defineSpawn(
|
|
236
|
-
withGlobals(
|
|
303
|
+
defineSpawn(unit.command("spawn"));
|
|
304
|
+
withGlobals(unit.command("close")).description("tear down a unit's worktree + session and reap its state (the inverse of spawn)").argument("<id>", "unit id, handle, or worktree branch/CR ref").option("--force", "discard uncommitted changes in the worktree (never overrides refusing the primary checkout)").action((ref, opts) => {
|
|
237
305
|
const ctx = ctxOf(opts);
|
|
238
306
|
touch(ctx);
|
|
239
307
|
const agent = resolveAgent(ctx.store, ref);
|
|
@@ -250,79 +318,73 @@ withGlobals(session.command("close")).description("tear down a unit's worktree +
|
|
|
250
318
|
json: res
|
|
251
319
|
});
|
|
252
320
|
});
|
|
253
|
-
withGlobals(
|
|
254
|
-
const ctx = ctxOf(opts);
|
|
255
|
-
touch(ctx);
|
|
256
|
-
const agents = listAgents(ctx.store).filter((a) => a.status !== "exited");
|
|
257
|
-
emit(formatOf(opts), {
|
|
258
|
-
toon: toonList("sessions", agents, [
|
|
259
|
-
{
|
|
260
|
-
key: "id",
|
|
261
|
-
get: (a) => a.id
|
|
262
|
-
},
|
|
263
|
-
{
|
|
264
|
-
key: "handle",
|
|
265
|
-
get: (a) => a.handle
|
|
266
|
-
},
|
|
267
|
-
{
|
|
268
|
-
key: "status",
|
|
269
|
-
get: (a) => a.status
|
|
270
|
-
},
|
|
271
|
-
{
|
|
272
|
-
key: "pane",
|
|
273
|
-
get: (a) => a.tmux?.pane ?? "-"
|
|
274
|
-
}
|
|
275
|
-
], `${agents.length} sessions`),
|
|
276
|
-
json: agents
|
|
277
|
-
});
|
|
278
|
-
});
|
|
279
|
-
withGlobals(session.command("focus")).description("move input focus to a peer's session").argument("<ref>", "unit id, handle, or worktree branch/CR ref").action((ref, opts) => {
|
|
321
|
+
withGlobals(unit.command("focus")).description("move input focus to a peer's session").argument("<ref>", "unit id, handle, or worktree branch/CR ref").action((ref, opts) => {
|
|
280
322
|
const ctx = ctxOf(opts);
|
|
281
323
|
touch(ctx);
|
|
282
|
-
const
|
|
283
|
-
selectSessionAdapter(ctx.env ?? process.env).focus(realExec, target);
|
|
324
|
+
const { pane } = focusUnit(ctx, ref);
|
|
284
325
|
emit(formatOf(opts), {
|
|
285
326
|
toon: toonObject({
|
|
286
327
|
focused: ref,
|
|
287
|
-
pane
|
|
328
|
+
pane
|
|
288
329
|
}),
|
|
289
330
|
json: {
|
|
290
331
|
ref,
|
|
291
|
-
pane
|
|
332
|
+
pane
|
|
292
333
|
}
|
|
293
334
|
});
|
|
294
335
|
});
|
|
295
|
-
withGlobals(
|
|
336
|
+
withGlobals(unit.command("nudge")).description("ring a peer's session (a doorbell that tells them to check their mail)").argument("<ref>", "unit id, handle, or worktree branch/CR ref").option("--message <text>", "the doorbell text delivered to the peer session", DELIVERY_DOORBELL).action(async (ref, opts) => {
|
|
296
337
|
const ctx = ctxOf(opts);
|
|
297
338
|
touch(ctx);
|
|
298
|
-
const
|
|
299
|
-
selectSessionAdapter(ctx.env ?? process.env).send(realExec, target, "");
|
|
339
|
+
const { pane, message, resubmits } = await nudgeUnit(ctx, ref, { message: opts.message });
|
|
300
340
|
emit(formatOf(opts), {
|
|
301
341
|
toon: toonObject({
|
|
302
342
|
nudged: ref,
|
|
303
|
-
pane
|
|
343
|
+
pane
|
|
304
344
|
}),
|
|
305
345
|
json: {
|
|
306
346
|
ref,
|
|
307
|
-
pane
|
|
347
|
+
pane,
|
|
348
|
+
message,
|
|
349
|
+
resubmits
|
|
308
350
|
}
|
|
309
351
|
});
|
|
310
352
|
});
|
|
311
|
-
withGlobals(
|
|
353
|
+
withGlobals(unit.command("read")).description("scrape a peer's session screen").argument("<ref>", "unit id, handle, or worktree branch/CR ref").option("--lines <n>", "trailing lines to capture", (v) => Number.parseInt(v, 10)).action((ref, opts) => {
|
|
312
354
|
const ctx = ctxOf(opts);
|
|
313
355
|
touch(ctx);
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
if (formatOf(opts) === "json") console.log(JSON.stringify({
|
|
356
|
+
const { pane, output } = readUnit(ctx, ref, { lines: opts.lines });
|
|
357
|
+
console.log(readCommandOutput(formatOf(opts), {
|
|
317
358
|
ref,
|
|
318
|
-
pane
|
|
319
|
-
output
|
|
320
|
-
}
|
|
321
|
-
|
|
359
|
+
pane,
|
|
360
|
+
output
|
|
361
|
+
}));
|
|
362
|
+
});
|
|
363
|
+
withGlobals(unit.command("clear")).description("reset a warm peer's context to cold by injecting its own harness fresh-context command — keeps the pane/session warm; tears down nothing").argument("<ref>", "unit id, handle, or worktree branch/CR ref").action((ref, opts) => {
|
|
364
|
+
const ctx = ctxOf(opts);
|
|
365
|
+
touch(ctx);
|
|
366
|
+
let res;
|
|
367
|
+
try {
|
|
368
|
+
res = clearUnit(ctx, ref);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
371
|
+
}
|
|
372
|
+
emit(formatOf(opts), {
|
|
373
|
+
toon: toonObject({
|
|
374
|
+
cleared: ref,
|
|
375
|
+
pane: res.pane,
|
|
376
|
+
command: res.command
|
|
377
|
+
}),
|
|
378
|
+
json: {
|
|
379
|
+
cleared: ref,
|
|
380
|
+
pane: res.pane,
|
|
381
|
+
command: res.command
|
|
382
|
+
}
|
|
383
|
+
});
|
|
322
384
|
});
|
|
323
385
|
const mail = program.command("mail").description("durable inter-agent messaging");
|
|
324
386
|
function defineSend(cmd) {
|
|
325
|
-
return withGlobals(cmd).description("send a message to a peer (by handle or id)").requiredOption("--to <peer>", "recipient handle or id").option("--from <id>", "sender id (else this session own identity)").option("--subject <s>", "subject").option("--body <text>", "message body").option("--body-file <path>", "read body from a file, or - for stdin").option("--thread <id>", "thread id").option("--reply-to <msg>", "message id this replies to").action((opts) => {
|
|
387
|
+
return withGlobals(cmd).description("send a message to a peer (by handle or id)").requiredOption("--to <peer>", "recipient handle or id").option("--from <id>", "sender id (else this session own identity)").option("--subject <s>", "subject").option("--body <text>", "message body").option("--body-file <path>", "read body from a file, or - for stdin").option("--thread <id>", "thread id").option("--reply-to <msg>", "message id this replies to").option("--no-nudge", "suppress the delivery doorbell (do not wake the recipient's pane)").action(async (opts) => {
|
|
326
388
|
const ctx = ctxOf(opts);
|
|
327
389
|
const fromId = opts.from ?? requireSelf(ctx);
|
|
328
390
|
const body = resolveBody(opts.body, opts.bodyFile);
|
|
@@ -334,21 +396,30 @@ function defineSend(cmd) {
|
|
|
334
396
|
thread: opts.thread,
|
|
335
397
|
replyTo: opts.replyTo
|
|
336
398
|
});
|
|
399
|
+
const wake = await wakeRecipient(ctx.store, () => selectSessionAdapter(ctx.env ?? process.env), realExec, {
|
|
400
|
+
toId: msg.to,
|
|
401
|
+
fromId,
|
|
402
|
+
noNudge: opts.nudge === false
|
|
403
|
+
});
|
|
404
|
+
if (wake.warning) console.error(`delivery doorbell not confirmed (message still delivered): ${wake.warning}`);
|
|
337
405
|
emit(formatOf(opts), {
|
|
338
406
|
toon: toonObject({
|
|
339
407
|
sent: msg.id,
|
|
340
408
|
to: opts.to,
|
|
341
|
-
subject: msg.subject
|
|
409
|
+
subject: msg.subject,
|
|
410
|
+
rung: wake.rung
|
|
342
411
|
}),
|
|
343
|
-
json:
|
|
412
|
+
json: {
|
|
413
|
+
...msg,
|
|
414
|
+
rung: wake.rung
|
|
415
|
+
}
|
|
344
416
|
});
|
|
345
417
|
});
|
|
346
418
|
}
|
|
347
419
|
defineSend(mail.command("send"));
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const meId = opts.owner ? resolveStandingOwner(ctx.store, opts.owner) : requireSelf(ctx);
|
|
420
|
+
/** List a resolved inbox (the caller's own or an --owner mailbox) as TOON with an aggregate;
|
|
421
|
+
* `readCmd` is the follow-up read command prefix surfaced as the next step. */
|
|
422
|
+
function emitInbox(ctx, opts, meId, readCmd) {
|
|
352
423
|
const items = inbox({ store: ctx.store }, {
|
|
353
424
|
meId,
|
|
354
425
|
unread: opts.unread,
|
|
@@ -378,12 +449,34 @@ function runInbox(opts) {
|
|
|
378
449
|
json: items
|
|
379
450
|
});
|
|
380
451
|
const firstUnread = items.find((m) => !m.read);
|
|
381
|
-
if (firstUnread) nextStep(
|
|
452
|
+
if (firstUnread) nextStep(`${readCmd} ${firstUnread.id}`);
|
|
453
|
+
}
|
|
454
|
+
function runInbox(opts) {
|
|
455
|
+
const ctx = ctxOf(opts);
|
|
456
|
+
touch(ctx);
|
|
457
|
+
emitInbox(ctx, opts, opts.owner ? resolveStandingOwner(ctx.store, opts.owner) : requireSelf(ctx), "cyberlegion mail read");
|
|
382
458
|
}
|
|
383
459
|
withGlobals(mail.command("inbox")).description("list your mail").option("--unread", "only un-acked mail").option("--from <id>", "filter by sender").option("--thread <id>", "filter to messages carrying this thread id").option("--owner <handle>", "target a standing owner's mailbox instead of this session's own").action(runInbox);
|
|
384
|
-
withGlobals(mail.command("read")).description("
|
|
460
|
+
withGlobals(mail.command("read")).description("read a message; --ack consumes it in the same step, otherwise it only peeks").argument("<msg-id>", "message id").option("--ack", "acknowledge the message in the same step (idempotent — no error if already acked)").option("--owner <handle>", "read a standing owner's mailbox instead of this session's own").action((msgId, opts) => {
|
|
385
461
|
const ctx = ctxOf(opts);
|
|
386
462
|
const meId = opts.owner ? resolveStandingOwner(ctx.store, opts.owner) : requireSelf(ctx);
|
|
463
|
+
if (opts.ack) {
|
|
464
|
+
const { msg, acked } = readAck({ store: ctx.store }, meId, msgId);
|
|
465
|
+
emit(formatOf(opts), {
|
|
466
|
+
toon: toonObject({
|
|
467
|
+
id: msg.id,
|
|
468
|
+
from: msg.fromHandle,
|
|
469
|
+
subject: msg.subject,
|
|
470
|
+
body: msg.body,
|
|
471
|
+
acked
|
|
472
|
+
}),
|
|
473
|
+
json: {
|
|
474
|
+
...msg,
|
|
475
|
+
acked
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
387
480
|
const msg = peek({ store: ctx.store }, meId, msgId);
|
|
388
481
|
if (!msg) fail(`"${msgId}" is not a message in this inbox`);
|
|
389
482
|
emit(formatOf(opts), {
|
|
@@ -473,96 +566,6 @@ withGlobals(mail.command("hook")).description("emit the harness hook injection p
|
|
|
473
566
|
const payload = injectInbox(ctx, opts.event);
|
|
474
567
|
if (payload) console.log(JSON.stringify(payload));
|
|
475
568
|
});
|
|
476
|
-
const dispatch = program.command("dispatch").description("delegate work and await a result (result-slot primitives)");
|
|
477
|
-
function definePrepOptions(cmd) {
|
|
478
|
-
return withGlobals(cmd).option("--agent <name>", "resolve an agent def to build the instruction/launch from").option("--agent-file <path>", "read an exact agent def file instead of resolving by name").option("--role <name>", "role label folded into the generic instruction when no agent def is given").option("--brief-text <text>", "brief body text").option("--brief-file <path>", "read the brief from a file, or - for stdin").option("--verdict-schema <path>", "JSON schema file (required keys + primitive types) the result must satisfy").option("--thread <id>", "thread id (defaults to the minted dispatch id)");
|
|
479
|
-
}
|
|
480
|
-
function dispatchResultFields(r) {
|
|
481
|
-
return {
|
|
482
|
-
id: r.id,
|
|
483
|
-
verdict: r.verdict != null ? JSON.stringify(r.verdict) : void 0,
|
|
484
|
-
body: r.body,
|
|
485
|
-
ts: r.ts
|
|
486
|
-
};
|
|
487
|
-
}
|
|
488
|
-
definePrepOptions(dispatch.command("prep")).description("allocate an id + brief + result slot and return the envelope — spawns nothing, never invokes Task").action((opts) => {
|
|
489
|
-
const ctx = ctxOf(opts);
|
|
490
|
-
let envelope;
|
|
491
|
-
try {
|
|
492
|
-
envelope = prep({ store: ctx.store }, {
|
|
493
|
-
agent: opts.agent,
|
|
494
|
-
agentFile: opts.agentFile,
|
|
495
|
-
role: opts.role,
|
|
496
|
-
briefText: opts.briefText,
|
|
497
|
-
briefFile: opts.briefFile,
|
|
498
|
-
thread: opts.thread
|
|
499
|
-
});
|
|
500
|
-
} catch (err) {
|
|
501
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
502
|
-
}
|
|
503
|
-
emit(formatOf(opts), {
|
|
504
|
-
toon: toonObject({ ...envelope }),
|
|
505
|
-
json: envelope
|
|
506
|
-
});
|
|
507
|
-
nextStep(`spawn a subagent with the instruction, then \`cyberlegion dispatch collect ${envelope.id}\` — or on the channel path, \`cyberlegion mail await --thread ${envelope.thread}\``);
|
|
508
|
-
});
|
|
509
|
-
definePrepOptions(dispatch.command("channel")).description("prep + spawn a peer session + (--wait) await the mail-thread reply — the one CLI-driven convenience (needs --agent or --agent-file to realize the peer launch)").addOption(new Option("--at <placement>", "where to open the new session").choices([
|
|
510
|
-
"pane:right",
|
|
511
|
-
"pane:down",
|
|
512
|
-
"tab",
|
|
513
|
-
"window",
|
|
514
|
-
"workspace"
|
|
515
|
-
]).default("pane:right")).option("--wait", "block for the mail-thread reply and print the validated result").option("--timeout <ms>", "give up after this many ms with no reply (0 = wait forever)", (v) => Number.parseInt(v, 10), 6e5).option("--max-wait <s>", "self-cap for one internal poll cycle, in seconds (re-arm by re-running with --wait)", (v) => Number.parseInt(v, 10), 240).action(async (opts) => {
|
|
516
|
-
const ctx = ctxOf(opts);
|
|
517
|
-
touch(ctx);
|
|
518
|
-
let result;
|
|
519
|
-
try {
|
|
520
|
-
result = await channel(ctx, {
|
|
521
|
-
agent: opts.agent,
|
|
522
|
-
agentFile: opts.agentFile,
|
|
523
|
-
role: opts.role,
|
|
524
|
-
briefText: opts.briefText,
|
|
525
|
-
briefFile: opts.briefFile,
|
|
526
|
-
verdictSchema: opts.verdictSchema,
|
|
527
|
-
thread: opts.thread,
|
|
528
|
-
at: opts.at,
|
|
529
|
-
wait: opts.wait,
|
|
530
|
-
timeoutMs: opts.timeout,
|
|
531
|
-
maxWaitS: opts.maxWait
|
|
532
|
-
});
|
|
533
|
-
} catch (err) {
|
|
534
|
-
if (err instanceof DispatchWaitingError) {
|
|
535
|
-
console.error(err.message);
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
539
|
-
}
|
|
540
|
-
if ("ts" in result) {
|
|
541
|
-
emit(formatOf(opts), {
|
|
542
|
-
toon: toonObject(dispatchResultFields(result)),
|
|
543
|
-
json: result
|
|
544
|
-
});
|
|
545
|
-
return;
|
|
546
|
-
}
|
|
547
|
-
emit(formatOf(opts), {
|
|
548
|
-
toon: toonObject({ ...result }),
|
|
549
|
-
json: result
|
|
550
|
-
});
|
|
551
|
-
nextStep(`cyberlegion mail await --thread ${result.thread} to collect the reply later`);
|
|
552
|
-
});
|
|
553
|
-
withGlobals(dispatch.command("collect")).description("read + validate a subagent's result file (the subagent path's counterpart to mail await)").argument("<id>", "dispatch id").option("--verdict-schema <path>", "JSON schema file the result must satisfy").action((id, opts) => {
|
|
554
|
-
const ctx = ctxOf(opts);
|
|
555
|
-
let result;
|
|
556
|
-
try {
|
|
557
|
-
result = collect({ store: ctx.store }, id, opts.verdictSchema);
|
|
558
|
-
} catch (err) {
|
|
559
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
560
|
-
}
|
|
561
|
-
emit(formatOf(opts), {
|
|
562
|
-
toon: toonObject(dispatchResultFields(result)),
|
|
563
|
-
json: result
|
|
564
|
-
});
|
|
565
|
-
});
|
|
566
569
|
const agent = program.command("agent").description("resolve reusable agent definitions");
|
|
567
570
|
const INSTRUCTIONS_PREVIEW_LEN = 200;
|
|
568
571
|
function truncated(text, full) {
|
|
@@ -654,31 +657,11 @@ withGlobals(agent.command("path")).description("print the resolved def file path
|
|
|
654
657
|
json: { path: def.path }
|
|
655
658
|
});
|
|
656
659
|
});
|
|
657
|
-
const
|
|
658
|
-
withGlobals(
|
|
659
|
-
const results = install(opts.agent, opts.dir);
|
|
660
|
-
emit(formatOf(opts), {
|
|
661
|
-
toon: toonList("hooks", results, [
|
|
662
|
-
{
|
|
663
|
-
key: "event",
|
|
664
|
-
get: (r) => r.event
|
|
665
|
-
},
|
|
666
|
-
{
|
|
667
|
-
key: "status",
|
|
668
|
-
get: (r) => r.status
|
|
669
|
-
},
|
|
670
|
-
{
|
|
671
|
-
key: "file",
|
|
672
|
-
get: (r) => r.file
|
|
673
|
-
}
|
|
674
|
-
], `${results.length} hooks`),
|
|
675
|
-
json: results
|
|
676
|
-
});
|
|
677
|
-
});
|
|
678
|
-
withGlobals(admin.command("doctor")).description("probe harness, multiplexer (ancestry-discovered), hub root, and self-id").action((opts) => {
|
|
660
|
+
const mux = program.command("mux").description("the unit-agnostic pane layer — multiplexer detection and diagnostics");
|
|
661
|
+
withGlobals(mux.command("doctor")).description("probe harness, multiplexer (ancestry-discovered), hub root, and self-id").action((opts) => {
|
|
679
662
|
const ctx = ctxOf(opts);
|
|
680
663
|
const harness = detectHarness(void 0, ctx) ?? "unknown";
|
|
681
|
-
const probe = probeMultiplexer(ctx.exec ?? realExec, ctx.env ?? process.env);
|
|
664
|
+
const probe = probeMultiplexer(ctx.exec ?? realExec, normalizeMuxEnv(ctx.env ?? process.env));
|
|
682
665
|
const selfId = resolveSelfId(ctx) ?? "-";
|
|
683
666
|
emit(formatOf(opts), {
|
|
684
667
|
toon: toonObject({
|
|
@@ -698,19 +681,46 @@ withGlobals(admin.command("doctor")).description("probe harness, multiplexer (an
|
|
|
698
681
|
selfId
|
|
699
682
|
}
|
|
700
683
|
});
|
|
701
|
-
if (probe.mux !== "none") nextStep(`export
|
|
684
|
+
if (probe.mux !== "none") nextStep(`export CYBER_MUX=${probe.mux}${probe.pane ? ` CYBER_MUX_PANE=${probe.pane}` : ""} — pin the fast-path, skip ancestry discovery on later calls`);
|
|
702
685
|
});
|
|
703
|
-
withGlobals(
|
|
704
|
-
|
|
686
|
+
withGlobals(mux.command("mode")).description("report the detected session-backend mode").action((opts) => {
|
|
687
|
+
const ctx = ctxOf(opts);
|
|
688
|
+
let mode = "none";
|
|
705
689
|
try {
|
|
706
|
-
|
|
690
|
+
mode = selectSessionAdapter(ctx.env ?? process.env).name;
|
|
707
691
|
} catch {}
|
|
708
692
|
emit(formatOf(opts), {
|
|
709
|
-
toon: toonObject({ mode
|
|
710
|
-
json: { mode
|
|
693
|
+
toon: toonObject({ mode }),
|
|
694
|
+
json: { mode }
|
|
711
695
|
});
|
|
712
696
|
});
|
|
713
|
-
withGlobals(
|
|
697
|
+
withGlobals(program.command("attach")).description("bind this pane as the hub's main pane (the owner's live presence); --show reads it, --clear unbinds").option("--clear", "unbind the main pane (a no-op when nothing is bound)").option("--show", "print the bound main pane instead of binding").action((opts) => {
|
|
698
|
+
const ctx = ctxOf(opts);
|
|
699
|
+
if (opts.show) {
|
|
700
|
+
const pane = ctx.store.getMainPane();
|
|
701
|
+
emit(formatOf(opts), {
|
|
702
|
+
toon: toonObject({ mainPane: pane ?? "none" }),
|
|
703
|
+
json: { mainPane: pane ?? null }
|
|
704
|
+
});
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (opts.clear) {
|
|
708
|
+
ctx.store.setMainPane(null);
|
|
709
|
+
emit(formatOf(opts), {
|
|
710
|
+
toon: toonObject({ mainPane: "none" }),
|
|
711
|
+
json: { mainPane: null }
|
|
712
|
+
});
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const cur = currentPane(normalizeMuxEnv(ctx.env ?? process.env));
|
|
716
|
+
if (!cur) fail("no multiplexer pane to bind — run this from inside a tmux or herdr pane");
|
|
717
|
+
ctx.store.setMainPane(cur.pane);
|
|
718
|
+
emit(formatOf(opts), {
|
|
719
|
+
toon: toonObject({ mainPane: cur.pane }),
|
|
720
|
+
json: { mainPane: cur.pane }
|
|
721
|
+
});
|
|
722
|
+
});
|
|
723
|
+
withGlobals(program.command("admin").description("hub-state maintenance").command("migrate")).description("merge one hub root state into another (e.g. an old project-local root into the global hub)").requiredOption("--from <path>", "source hub root").requiredOption("--to <path>", "destination hub root").action((opts) => {
|
|
714
724
|
const res = migrateStore(new FileStore(resolve(opts.from)), new FileStore(resolve(opts.to)));
|
|
715
725
|
emit(formatOf(opts), {
|
|
716
726
|
toon: toonObject({
|
|
@@ -721,10 +731,51 @@ withGlobals(admin.command("migrate")).description("merge one hub root state into
|
|
|
721
731
|
json: res
|
|
722
732
|
});
|
|
723
733
|
});
|
|
734
|
+
withGlobals(program.command("init")).description("resolve this session harness, register the Legion surfacing hook, and advise owner binding").option("--agent <h>", "claude | cursor | codex (else auto-detected)").option("--dir <path>", "project dir to write config into", process.cwd()).option("--pin <version>", "version to pin the registered npx hook command to (e.g. the bundled plugin version)").action((opts) => {
|
|
735
|
+
const ctx = ctxOf(opts);
|
|
736
|
+
let harness;
|
|
737
|
+
try {
|
|
738
|
+
const detected = detectHarness(opts.agent, ctx);
|
|
739
|
+
if (!detected) fail("could not detect harness — pass --agent claude|cursor|codex");
|
|
740
|
+
harness = detected;
|
|
741
|
+
} catch (err) {
|
|
742
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
743
|
+
}
|
|
744
|
+
let results;
|
|
745
|
+
try {
|
|
746
|
+
results = install(harness, opts.dir, opts.pin);
|
|
747
|
+
} catch (err) {
|
|
748
|
+
fail(err instanceof Error ? err.message : String(err));
|
|
749
|
+
}
|
|
750
|
+
emit(formatOf(opts), {
|
|
751
|
+
toon: toonList("hooks", results, [
|
|
752
|
+
{
|
|
753
|
+
key: "event",
|
|
754
|
+
get: (r) => r.event
|
|
755
|
+
},
|
|
756
|
+
{
|
|
757
|
+
key: "status",
|
|
758
|
+
get: (r) => r.status
|
|
759
|
+
},
|
|
760
|
+
{
|
|
761
|
+
key: "file",
|
|
762
|
+
get: (r) => r.file
|
|
763
|
+
}
|
|
764
|
+
], `harness ${harness}, ${results.length} hooks`),
|
|
765
|
+
json: {
|
|
766
|
+
harness,
|
|
767
|
+
hooks: results
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
if (!listAgents(ctx.store).some((a) => a.kind === "standing")) {
|
|
771
|
+
nextStep("cyberlegion unit register --standing --handle <name> to mint the durable owner inbox");
|
|
772
|
+
nextStep("cyberlegion attach to bind this pane as the owner live presence");
|
|
773
|
+
}
|
|
774
|
+
});
|
|
724
775
|
defineSpawn(program.command("spawn"));
|
|
725
776
|
defineSend(program.command("send"));
|
|
726
777
|
withGlobals(program.command("inbox")).description("list your mail (alias of `mail inbox`)").option("--unread", "only un-acked mail").option("--from <id>", "filter by sender").option("--thread <id>", "filter to messages carrying this thread id").action(runInbox);
|
|
727
|
-
withGlobals(program.command("who")).description("list the addressable
|
|
778
|
+
withGlobals(program.command("who")).description("list the addressable units (alias of `unit who`)").option("--all", "include exited units").option("--reconcile", "live-probe the current mux: cull dead-pane records and adopt unbound harness-bearing panes before listing").action(runWho);
|
|
728
779
|
withGlobals(program).action((opts) => {
|
|
729
780
|
const ctx = ctxOf(opts);
|
|
730
781
|
touch(ctx);
|
|
@@ -752,12 +803,24 @@ withGlobals(program).action((opts) => {
|
|
|
752
803
|
units
|
|
753
804
|
}
|
|
754
805
|
});
|
|
755
|
-
if (!id) nextStep("cyberlegion
|
|
806
|
+
if (!id) nextStep("cyberlegion unit register to join");
|
|
756
807
|
else if (unread > 0) nextStep("cyberlegion mail inbox --unread");
|
|
757
808
|
});
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
809
|
+
/**
|
|
810
|
+
* Run the CLI. Exported rather than executed at module scope so the command bodies are reachable
|
|
811
|
+
* from a test: with a bare `program.parseAsync(process.argv)` here, importing this module ran the
|
|
812
|
+
* whole CLI, so every `.action()` body — and every option wire inside it — was unreachable, and a
|
|
813
|
+
* flag that stopped being forwarded changed real behavior with the suite green.
|
|
814
|
+
*
|
|
815
|
+
* `bin/cyberlegion.mjs` calls this; nothing else should.
|
|
816
|
+
*/
|
|
817
|
+
async function runCli(argv = process.argv) {
|
|
818
|
+
try {
|
|
819
|
+
await program.parseAsync(argv);
|
|
820
|
+
} catch (err) {
|
|
821
|
+
console.error(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }));
|
|
822
|
+
process.exit(1);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
762
825
|
//#endregion
|
|
763
|
-
export {};
|
|
826
|
+
export { runCli };
|