cyber-mux 0.3.0 → 0.4.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/dist/agent.d.mts +66 -0
- package/dist/agent.d.mts.map +1 -0
- package/dist/agent.mjs +58 -0
- package/dist/agent.mjs.map +1 -0
- package/dist/{backend-DnrbmL6N.mjs → backend-Cif257ji.mjs} +326 -6
- package/dist/backend-Cif257ji.mjs.map +1 -0
- package/dist/cli.d.mts.map +1 -1
- package/dist/cli.mjs +324 -19
- package/dist/cli.mjs.map +1 -1
- package/dist/index.d.mts +66 -581
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -3
- package/dist/mux-CoYDrk3v.d.mts +733 -0
- package/dist/mux-CoYDrk3v.d.mts.map +1 -0
- package/dist/template.mjs +1 -1
- package/dist/{worktree-CX88bGzZ.d.mts → worktree-9QDz-iKC.d.mts} +63 -2
- package/dist/{worktree-CX88bGzZ.d.mts.map → worktree-9QDz-iKC.d.mts.map} +1 -1
- package/dist/{worktree-CoQdRgv1.mjs → worktree-hHuFZkpW.mjs} +84 -2
- package/dist/{worktree-CoQdRgv1.mjs.map → worktree-hHuFZkpW.mjs.map} +1 -1
- package/dist/worktree.d.mts +2 -2
- package/dist/worktree.mjs +2 -2
- package/package.json +5 -1
- package/src/agent.ts +95 -0
- package/src/backend.ts +22 -0
- package/src/cli-error.ts +31 -0
- package/src/cli.ts +436 -60
- package/src/index.ts +3 -1
- package/src/mux.herdr.ts +193 -3
- package/src/mux.tmux.ts +10 -0
- package/src/mux.ts +155 -0
- package/src/mux.wezterm.ts +9 -0
- package/src/mux.zellij.ts +9 -0
- package/src/nudge.ts +9 -0
- package/src/ratio.ts +25 -0
- package/src/template-capture.ts +66 -1
- package/src/wait-output.ts +116 -0
- package/src/worktree.ts +105 -0
- package/dist/backend-DnrbmL6N.mjs.map +0 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { a as listWorktreesFromGit, c as
|
|
2
|
-
import {
|
|
1
|
+
import { a as listWorktreesFromGit, c as provisionWorktree, d as resolvePrimaryRoot, f as resolveWorktreePath, i as isWorktreeRemovable, l as pruneWorktrees, m as nodeExec, r as gitWorktreeAdapter, s as normalizeWorktreePath, t as WorktreeGitError, u as removeWorktreeSafely } from "./worktree-hHuFZkpW.mjs";
|
|
2
|
+
import { f as nodeNewId, o as currentPane, r as resolveMuxAdapter, s as probeMultiplexer, t as callerPane, x as envFallback } from "./backend-Cif257ji.mjs";
|
|
3
|
+
import { AgentLifecycleUnsupportedError, deriveAgentWait } from "./agent.mjs";
|
|
3
4
|
import { collectPanes, firstPane, isValidTemplateName, listTemplates, nodeTemplateStore, parseTemplate, resolveTemplate as resolveTemplate$1, resolveTree, templateDirs, validateTemplate } from "./template.mjs";
|
|
4
5
|
import { join, relative, sep } from "node:path";
|
|
5
6
|
import { Command, CommanderError, InvalidArgumentError, Option } from "commander";
|
|
@@ -136,6 +137,29 @@ var AmbiguousPaneError = class extends CliError {
|
|
|
136
137
|
this.name = "AmbiguousPaneError";
|
|
137
138
|
}
|
|
138
139
|
};
|
|
140
|
+
/** The stable code a missing-pane usage error carries — the same code FAMILY as the ambiguity: both
|
|
141
|
+
* are exit-2 usage errors whose fix is a different pane argument, and both hand the caller the live
|
|
142
|
+
* panes to retry with rather than sending them on a second round trip. */
|
|
143
|
+
const MISSING_PANE_CODE = "missing-pane";
|
|
144
|
+
/**
|
|
145
|
+
* A pane verb invoked with no `<pane>` at all — a usage error (exit 2), the same shape the ambiguity
|
|
146
|
+
* uses. The old "having called no backend" guarantee is deliberately gone (CR 95): the backend IS
|
|
147
|
+
* queried so the live panes can be listed as candidates, since the caller's deterministic next move
|
|
148
|
+
* after "missing pane" is `cyber-mux list`. Folding that answer into the error collapses a two-turn
|
|
149
|
+
* correction into one, exactly as the ambiguity report already does.
|
|
150
|
+
*
|
|
151
|
+
* Carries its `candidates` in `extra` under the same key `AmbiguousPaneError` uses, so the one
|
|
152
|
+
* renderer prints them `<id> <label> <cwd>` with no special case. When the backend could NOT be
|
|
153
|
+
* enumerated the list is empty and the help points at `cyber-mux list` instead of the ids.
|
|
154
|
+
*/
|
|
155
|
+
var MissingPaneError = class extends CliError {
|
|
156
|
+
candidates;
|
|
157
|
+
constructor(candidates) {
|
|
158
|
+
super(MISSING_PANE_CODE, "no pane given — this command needs a target pane", candidates.length > 0 ? `pass one of the ids below: ${candidates.map((c) => c.id).join(" ")}` : "list the live panes with: cyber-mux list", 2, { candidates });
|
|
159
|
+
this.candidates = candidates;
|
|
160
|
+
this.name = "MissingPaneError";
|
|
161
|
+
}
|
|
162
|
+
};
|
|
139
163
|
/**
|
|
140
164
|
* The ONE renderer — every coded failure reaches stdout through here, and exits.
|
|
141
165
|
*
|
|
@@ -452,6 +476,54 @@ function convert(node, ctx) {
|
|
|
452
476
|
if (node.ratio !== void 0) split.ratio = node.ratio;
|
|
453
477
|
return split;
|
|
454
478
|
}
|
|
479
|
+
/**
|
|
480
|
+
* A capture asked of a backend that lacks the optional geometry seam it needs — no `describeRegion`
|
|
481
|
+
* for a region capture, no `describeWorkspace` for a `--workspace` one. An absent optional seam member
|
|
482
|
+
* is a refusal, never a guess: a backend that cannot answer cannot be captured and there is nothing to
|
|
483
|
+
* degrade to.
|
|
484
|
+
*
|
|
485
|
+
* PORTABLE and exit-code-free by design. The DECISION to refuse is the library's and lives here, at
|
|
486
|
+
* the one place that sees the adapter — the pure `captureTemplate` above takes rectangles already read
|
|
487
|
+
* and could never make it. How the refusal SURFACES — the exit code, the fix hint, the exact sentence
|
|
488
|
+
* — is the CLI's, which catches this and re-raises its own `backend-unsupported` error. `capability`
|
|
489
|
+
* names which seam was missing so the caller composes the right message without re-deriving it, and
|
|
490
|
+
* the terse `message` is a factual log line, not the user-facing copy.
|
|
491
|
+
*/
|
|
492
|
+
var CaptureUnsupportedError = class extends Error {
|
|
493
|
+
backend;
|
|
494
|
+
capability;
|
|
495
|
+
constructor(backend, capability) {
|
|
496
|
+
super(capability === "region" ? `${backend} cannot report a region's geometry` : `${backend} cannot enumerate a workspace's tabs`);
|
|
497
|
+
this.backend = backend;
|
|
498
|
+
this.capability = capability;
|
|
499
|
+
this.name = "CaptureUnsupportedError";
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
/**
|
|
503
|
+
* Read the live region around `target` through the adapter and derive its template — the
|
|
504
|
+
* surface-independent orchestrator `template save` drives, and the single home of the region-capture
|
|
505
|
+
* refusal. The optional `regions` seam is where a backend says whether it can report geometry at all;
|
|
506
|
+
* a backend without `describeRegion` is refused HERE (`CaptureUnsupportedError`), before any read,
|
|
507
|
+
* because the pure `captureTemplate` it would feed cannot see the adapter and so cannot make that
|
|
508
|
+
* call. A backend that CAN report reads its region and hands the rectangles to the pure capture
|
|
509
|
+
* unchanged.
|
|
510
|
+
*/
|
|
511
|
+
function deriveRegionCapture(adapter, exec, target, opts) {
|
|
512
|
+
const describeRegion = adapter.regions?.describeRegion;
|
|
513
|
+
if (!describeRegion) throw new CaptureUnsupportedError(adapter.name, "region");
|
|
514
|
+
return captureTemplate(describeRegion(exec, target), opts);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Read every tab of the workspace `target` sits in through the adapter and derive a `tabs` template —
|
|
518
|
+
* the `--workspace` orchestrator, and the single home of the workspace-capture refusal. Same shape as
|
|
519
|
+
* `deriveRegionCapture`, one member over: a backend without `describeWorkspace` cannot enumerate a
|
|
520
|
+
* workspace's tabs and is refused HERE (`CaptureUnsupportedError`), before any read.
|
|
521
|
+
*/
|
|
522
|
+
function deriveWorkspaceCapture(adapter, exec, target, opts) {
|
|
523
|
+
const describeWorkspace = adapter.regions?.describeWorkspace;
|
|
524
|
+
if (!describeWorkspace) throw new CaptureUnsupportedError(adapter.name, "workspace");
|
|
525
|
+
return captureWorkspaceTemplate(describeWorkspace(exec, target), opts);
|
|
526
|
+
}
|
|
455
527
|
//#endregion
|
|
456
528
|
//#region src/template-edit.ts
|
|
457
529
|
/**
|
|
@@ -1224,6 +1296,16 @@ function noMux() {
|
|
|
1224
1296
|
function paneNotFound(locator) {
|
|
1225
1297
|
return new CliError("pane-not-found", `pane "${locator}" matched no live pane`, "list the live panes with: cyber-mux list", 1);
|
|
1226
1298
|
}
|
|
1299
|
+
/**
|
|
1300
|
+
* `send text`/`send keys` given a pane but no payload — a usage error (exit 2), the fix is to add the
|
|
1301
|
+
* payload. The payload argument is optional at the parser (`[text]`/`[keys...]`) only so a bare
|
|
1302
|
+
* `send text` resolves its MISSING pane through `resolveTarget` and lists candidates rather than
|
|
1303
|
+
* tripping commander's own payload-missing error first; once a pane is resolved, an absent payload is
|
|
1304
|
+
* caught here so the operation never runs on nothing.
|
|
1305
|
+
*/
|
|
1306
|
+
function missingSendPayload(verb) {
|
|
1307
|
+
return verb === "text" ? new CliError("missing-argument", "send text needs the text to type", "pass the text: cyber-mux send text <pane> <text>", 2) : new CliError("missing-argument", "send keys needs at least one key to press", "pass one or more keys: cyber-mux send keys <pane> <keys...>", 2);
|
|
1308
|
+
}
|
|
1227
1309
|
/** A malformed template name — a usage error (exit 2): the fix is a different name, nothing was
|
|
1228
1310
|
* attempted. The same family a missing required argument is in. */
|
|
1229
1311
|
function invalidTemplateName(name) {
|
|
@@ -1270,6 +1352,17 @@ function reportWorktreeFailure(err) {
|
|
|
1270
1352
|
* "no such pane" message this function's to write.
|
|
1271
1353
|
*/
|
|
1272
1354
|
function resolveTarget(deps, a, locator) {
|
|
1355
|
+
if (!locator) {
|
|
1356
|
+
let listed = [];
|
|
1357
|
+
try {
|
|
1358
|
+
listed = a.listPanes(deps.exec);
|
|
1359
|
+
} catch {}
|
|
1360
|
+
throw new MissingPaneError(listed.map((p) => ({
|
|
1361
|
+
id: p.id,
|
|
1362
|
+
label: p.label ?? null,
|
|
1363
|
+
cwd: p.cwd ?? null
|
|
1364
|
+
})));
|
|
1365
|
+
}
|
|
1273
1366
|
let panes;
|
|
1274
1367
|
try {
|
|
1275
1368
|
panes = a.listPanes(deps.exec);
|
|
@@ -1338,7 +1431,7 @@ function paneVerb(locator, body) {
|
|
|
1338
1431
|
body();
|
|
1339
1432
|
} catch (err) {
|
|
1340
1433
|
if (err instanceof CliError) throw err;
|
|
1341
|
-
throw paneNotFound(locator);
|
|
1434
|
+
throw paneNotFound(locator ?? "");
|
|
1342
1435
|
}
|
|
1343
1436
|
}
|
|
1344
1437
|
/**
|
|
@@ -1557,17 +1650,14 @@ function templateSaveCommand(deps) {
|
|
|
1557
1650
|
const path = join(templateDirs(deps.exec, deps.env)[opts.to], `${name}.json`);
|
|
1558
1651
|
if (!opts.force && deps.store.read(path) !== null) throw new CliError("template-exists", `template "${name}" already exists at ${path} — pass --force to overwrite it`, "re-run with --force to replace it", 1);
|
|
1559
1652
|
const a = adapter(deps);
|
|
1560
|
-
|
|
1561
|
-
if (!opts.workspace && !describeRegion) throw new CliError("backend-unsupported", `${a.name} cannot report a region's geometry — template save needs a backend that can`, "run template save on a backend that reports geometry (tmux or herdr)", 1);
|
|
1562
|
-
const describeWorkspace = a.regions?.describeWorkspace;
|
|
1563
|
-
if (opts.workspace && !describeWorkspace) throw new CliError("backend-unsupported", `${a.name} cannot enumerate a workspace's tabs — template save --workspace needs a backend that can`, "run template save --workspace on a backend that enumerates tabs (tmux or herdr)", 1);
|
|
1653
|
+
if (!(opts.workspace ? a.regions?.describeWorkspace : a.regions?.describeRegion)) throw new CaptureUnsupportedError(a.name, opts.workspace ? "workspace" : "region");
|
|
1564
1654
|
const target = opts.from ? resolveTarget(deps, a, opts.from) : callerPane(a, deps.env);
|
|
1565
1655
|
if (!target) throw new CliError("missing-pane", "template save needs a pane to capture the region around — pass --from <pane>, or run it inside one", "pass --from <pane>, or run template save inside a pane", 2);
|
|
1566
1656
|
const captureOpts = {
|
|
1567
1657
|
name,
|
|
1568
1658
|
description: opts.description ?? CAPTURED_DESCRIPTION
|
|
1569
1659
|
};
|
|
1570
|
-
const { template, warnings } = opts.workspace ?
|
|
1660
|
+
const { template, warnings } = opts.workspace ? deriveWorkspaceCapture(a, deps.exec, target, captureOpts) : deriveRegionCapture(a, deps.exec, target, captureOpts);
|
|
1571
1661
|
deps.store.write(path, `${JSON.stringify(template, null, 2)}\n`);
|
|
1572
1662
|
for (const warning of warnings) process.stderr.write(`${warning}\n`);
|
|
1573
1663
|
const entry = opts.workspace ? null : noteTabsLeftOut(deps, a, target, name);
|
|
@@ -1581,6 +1671,7 @@ function templateSaveCommand(deps) {
|
|
|
1581
1671
|
});
|
|
1582
1672
|
} catch (err) {
|
|
1583
1673
|
if (err instanceof CliError) throw err;
|
|
1674
|
+
if (err instanceof CaptureUnsupportedError) throw backendUnsupported(err);
|
|
1584
1675
|
throw new CliError("unsplittable-region", "this region could not be captured — it is not a tree any sequence of splits could have produced", "template save can only capture a region built by splitting", 1);
|
|
1585
1676
|
}
|
|
1586
1677
|
}));
|
|
@@ -1599,6 +1690,15 @@ function templateSaveCommand(deps) {
|
|
|
1599
1690
|
* right — a window nobody grouped is a workspace of one, and nothing was left out. The `command`
|
|
1600
1691
|
* re-states the caller's own `name` with `--workspace`, the flag that captures every tab.
|
|
1601
1692
|
*/
|
|
1693
|
+
/**
|
|
1694
|
+
* The CLI surface of the library's geometry-capability refusal: exit 1 (a genuine operation failure,
|
|
1695
|
+
* not a usage error), naming the backend, with the flag-specific fix hint. The DECISION to refuse is
|
|
1696
|
+
* the orchestrator's (`CaptureUnsupportedError`); this composes only the presentation from the two
|
|
1697
|
+
* structured fields the error carries, so the sentence lives in one place per surface.
|
|
1698
|
+
*/
|
|
1699
|
+
function backendUnsupported(err) {
|
|
1700
|
+
return err.capability === "region" ? new CliError("backend-unsupported", `${err.backend} cannot report a region's geometry — template save needs a backend that can`, "run template save on a backend that reports geometry (tmux or herdr)", 1) : new CliError("backend-unsupported", `${err.backend} cannot enumerate a workspace's tabs — template save --workspace needs a backend that can`, "run template save --workspace on a backend that enumerates tabs (tmux or herdr)", 1);
|
|
1701
|
+
}
|
|
1602
1702
|
function noteTabsLeftOut(deps, adapter, target, name) {
|
|
1603
1703
|
const describeWorkspace = adapter.regions?.describeWorkspace;
|
|
1604
1704
|
if (!describeWorkspace) return null;
|
|
@@ -1928,22 +2028,26 @@ function openCommand(deps) {
|
|
|
1928
2028
|
* error — a missing required parameter; see the AXI note in `.agents/spec/axi/README.md`). */
|
|
1929
2029
|
function sendCommand(deps) {
|
|
1930
2030
|
const send = new Command("send").description("Drive a pane without taking its turn (text | keys)");
|
|
1931
|
-
send.addCommand(new Command("text").description("Type literal text into a pane, pressing no Enter (a key-named word is typed, not pressed)").argument("
|
|
2031
|
+
send.addCommand(new Command("text").description("Type literal text into a pane, pressing no Enter (a key-named word is typed, not pressed)").argument("[pane]", "Target pane id").argument("[text]", "Literal text to type").addOption(FORMAT_OPTION).action(guarded((pane, text) => {
|
|
1932
2032
|
paneVerb(pane, () => {
|
|
1933
2033
|
const a = adapter(deps);
|
|
1934
|
-
|
|
2034
|
+
const target = resolveTarget(deps, a, pane);
|
|
2035
|
+
if (text === void 0) throw missingSendPayload("text");
|
|
2036
|
+
a.sendText(deps.exec, target, text);
|
|
1935
2037
|
});
|
|
1936
2038
|
})));
|
|
1937
|
-
send.addCommand(new Command("keys").description("Press named keys in a pane, typing nothing (Up, Enter, Escape, C-c, F1 …)").argument("
|
|
2039
|
+
send.addCommand(new Command("keys").description("Press named keys in a pane, typing nothing (Up, Enter, Escape, C-c, F1 …)").argument("[pane]", "Target pane id").argument("[keys...]", "Key names, in order — core vocabulary is portable, anything else is passed to the backend as-is").addOption(FORMAT_OPTION).action(guarded((pane, keys) => {
|
|
1938
2040
|
paneVerb(pane, () => {
|
|
1939
2041
|
const a = adapter(deps);
|
|
1940
|
-
|
|
2042
|
+
const target = resolveTarget(deps, a, pane);
|
|
2043
|
+
if (keys.length === 0) throw missingSendPayload("keys");
|
|
2044
|
+
a.sendKeys(deps.exec, target, keys);
|
|
1941
2045
|
});
|
|
1942
2046
|
})));
|
|
1943
2047
|
return send;
|
|
1944
2048
|
}
|
|
1945
2049
|
function submitCommand(deps) {
|
|
1946
|
-
return new Command("submit").description("Take a pane's turn: type the text if given, then always press Enter (no text = bare-Enter flush)").argument("
|
|
2050
|
+
return new Command("submit").description("Take a pane's turn: type the text if given, then always press Enter (no text = bare-Enter flush)").argument("[pane]", "Target pane id").argument("[text]", "Text to type before Enter; omit to flush an already-staged buffer without retyping it").addOption(FORMAT_OPTION).action(guarded((pane, text) => {
|
|
1947
2051
|
paneVerb(pane, () => {
|
|
1948
2052
|
const a = adapter(deps);
|
|
1949
2053
|
a.submit(deps.exec, resolveTarget(deps, a, pane), text);
|
|
@@ -1951,7 +2055,7 @@ function submitCommand(deps) {
|
|
|
1951
2055
|
}));
|
|
1952
2056
|
}
|
|
1953
2057
|
function readCommand(deps) {
|
|
1954
|
-
return new Command("read").description("Capture a pane's output").argument("
|
|
2058
|
+
return new Command("read").description("Capture a pane's output").argument("[pane]", "Target pane id").option("--lines <n>", "Trailing lines to capture", (v) => Number.parseInt(v, 10)).addOption(FORMAT_OPTION).action(guarded((pane, opts) => {
|
|
1955
2059
|
paneVerb(pane, () => {
|
|
1956
2060
|
const a = adapter(deps);
|
|
1957
2061
|
const t = resolveTarget(deps, a, pane);
|
|
@@ -1960,8 +2064,78 @@ function readCommand(deps) {
|
|
|
1960
2064
|
});
|
|
1961
2065
|
}));
|
|
1962
2066
|
}
|
|
2067
|
+
/** The default deadline a bare `wait` takes. Named, not spelled inline, so the help text and the value
|
|
2068
|
+
* cannot drift. Chosen to be long enough for a harness to boot and print, short enough that an agent
|
|
2069
|
+
* that guessed the wrong pattern gets its answer back inside one turn. */
|
|
2070
|
+
const DEFAULT_WAIT_TIMEOUT_MS = 3e4;
|
|
2071
|
+
/**
|
|
2072
|
+
* The `wait` verb: block until a pane PRINTS something, or the deadline passes.
|
|
2073
|
+
*
|
|
2074
|
+
* Agent-first in the two ways that matter here. First, the verdict is in the EXIT CODE — 0 matched, 1
|
|
2075
|
+
* timed out — so a shell caller branches without parsing anything, exactly as `exists` separates live
|
|
2076
|
+
* from gone. Second, a timeout is a normal answer and prints the same `{matched, output}` payload a
|
|
2077
|
+
* match does, so an agent that guessed wrong sees what the pane actually said instead of an error with
|
|
2078
|
+
* the evidence thrown away.
|
|
2079
|
+
*
|
|
2080
|
+
* It is its own verb rather than `read --wait <pattern>` because the two answer different questions —
|
|
2081
|
+
* `read` reports a snapshot and always succeeds; this one asserts a condition and can fail — and folding
|
|
2082
|
+
* them would make `read`'s exit code depend on a flag.
|
|
2083
|
+
*
|
|
2084
|
+
* The PATTERN is validated before the pane is resolved, which is the one place this verb orders its two
|
|
2085
|
+
* usage errors: a missing pattern is answered without a `listPanes` round trip, and candidate panes are
|
|
2086
|
+
* no use to a caller that has not said what it is waiting for. A missing PANE then takes the same
|
|
2087
|
+
* `MissingPaneError` (candidates listed) every other pane verb takes, from the one chokepoint that owns
|
|
2088
|
+
* it — which is why `[pane]` is optional at the parser and required in fact.
|
|
2089
|
+
*/
|
|
2090
|
+
function waitCommand(deps) {
|
|
2091
|
+
return new Command("wait").description("Block until a pane's output matches, or the timeout elapses (exit 0 = matched, 1 = timed out)").argument("[pane]", "Target pane id").option("--match <text>", "Literal substring to wait for").option("--regex <pattern>", "Regular expression to wait for (backend dialect; prefer --match)").option("--timeout <ms>", `Give up after this many ms (default ${DEFAULT_WAIT_TIMEOUT_MS})`, (v) => Number.parseInt(v, 10), DEFAULT_WAIT_TIMEOUT_MS).option("--lines <n>", "Restrict the searched snapshot to this many trailing lines", (v) => Number.parseInt(v, 10)).addOption(FORMAT_OPTION).action(guardedAsync(async (pane, opts) => {
|
|
2092
|
+
const pattern = waitPattern(opts);
|
|
2093
|
+
if (!Number.isFinite(opts.timeout) || opts.timeout < 0) throw invalidWaitTimeout(opts.timeout);
|
|
2094
|
+
const a = adapter(deps);
|
|
2095
|
+
const t = resolveTarget(deps, a, pane);
|
|
2096
|
+
const result = await a.waitForOutput(deps.exec, t, {
|
|
2097
|
+
...pattern,
|
|
2098
|
+
timeoutMs: opts.timeout,
|
|
2099
|
+
...opts.lines != null ? { lines: opts.lines } : {}
|
|
2100
|
+
}).catch((err) => {
|
|
2101
|
+
if (err instanceof CliError) throw err;
|
|
2102
|
+
throw paneNotFound(pane ?? "");
|
|
2103
|
+
});
|
|
2104
|
+
output({
|
|
2105
|
+
pane: t.id,
|
|
2106
|
+
...result
|
|
2107
|
+
}, () => {
|
|
2108
|
+
printFields({
|
|
2109
|
+
pane: t.id,
|
|
2110
|
+
matched: String(result.matched),
|
|
2111
|
+
line: result.matchedLine ?? null
|
|
2112
|
+
});
|
|
2113
|
+
});
|
|
2114
|
+
if (!result.matched) process.exit(1);
|
|
2115
|
+
}));
|
|
2116
|
+
}
|
|
2117
|
+
/** Exactly one of `--match`/`--regex`, as the seam requires — refused HERE so the caller gets this CLI's
|
|
2118
|
+
* coded usage error (exit 2, the fix is a different invocation) rather than the seam's bare throw. */
|
|
2119
|
+
function waitPattern(opts) {
|
|
2120
|
+
if (opts.match != null && opts.regex != null) throw new CliError("usage-error", "--match and --regex are mutually exclusive", "pass one pattern: cyber-mux wait <pane> --match <text>", 2);
|
|
2121
|
+
if (opts.match != null) return { match: opts.match };
|
|
2122
|
+
if (opts.regex != null) {
|
|
2123
|
+
try {
|
|
2124
|
+
new RegExp(opts.regex);
|
|
2125
|
+
} catch {
|
|
2126
|
+
throw new CliError("usage-error", `--regex is not a valid expression: ${opts.regex}`, "pass a valid pattern, or a literal with --match instead", 2);
|
|
2127
|
+
}
|
|
2128
|
+
return { regex: opts.regex };
|
|
2129
|
+
}
|
|
2130
|
+
throw new CliError("usage-error", "wait needs a pattern — pass --match or --regex", "wait for a literal: cyber-mux wait <pane> --match <text>", 2);
|
|
2131
|
+
}
|
|
2132
|
+
/** A `--timeout` that is not a non-negative number — a usage error: nothing was attempted, and the fix
|
|
2133
|
+
* is a different invocation. */
|
|
2134
|
+
function invalidWaitTimeout(timeout) {
|
|
2135
|
+
return new CliError("usage-error", `--timeout must be a non-negative number of milliseconds — got ${timeout}`, "pass milliseconds: cyber-mux wait <pane> --match <text> --timeout 30000", 2);
|
|
2136
|
+
}
|
|
1963
2137
|
function focusCommand(deps) {
|
|
1964
|
-
return new Command("focus").description("Beam the attached client to a pane").argument("
|
|
2138
|
+
return new Command("focus").description("Beam the attached client to a pane").argument("[pane]", "Target pane id").addOption(FORMAT_OPTION).action(guarded((pane) => {
|
|
1965
2139
|
paneVerb(pane, () => {
|
|
1966
2140
|
const a = adapter(deps);
|
|
1967
2141
|
a.focus(deps.exec, resolveTarget(deps, a, pane));
|
|
@@ -1969,7 +2143,7 @@ function focusCommand(deps) {
|
|
|
1969
2143
|
}));
|
|
1970
2144
|
}
|
|
1971
2145
|
function closeCommand(deps) {
|
|
1972
|
-
return new Command("close").description("Close a pane").argument("
|
|
2146
|
+
return new Command("close").description("Close a pane").argument("[pane]", "Target pane id").addOption(FORMAT_OPTION).action(guarded((pane) => {
|
|
1973
2147
|
paneVerb(pane, () => {
|
|
1974
2148
|
const a = adapter(deps);
|
|
1975
2149
|
a.teardown(deps.exec, resolveTarget(deps, a, pane));
|
|
@@ -1979,7 +2153,8 @@ function closeCommand(deps) {
|
|
|
1979
2153
|
function listCommand(deps) {
|
|
1980
2154
|
return new Command("list").description("Enumerate every live pane the current backend can see").addOption(FORMAT_OPTION).action(guarded(() => {
|
|
1981
2155
|
const panes = adapter(deps).listPanes(deps.exec);
|
|
1982
|
-
|
|
2156
|
+
const hasAgent = panes.some((p) => p.agentStatus !== void 0);
|
|
2157
|
+
const cols = [
|
|
1983
2158
|
{
|
|
1984
2159
|
label: "pane",
|
|
1985
2160
|
get: (p) => p.id
|
|
@@ -1996,11 +2171,16 @@ function listCommand(deps) {
|
|
|
1996
2171
|
label: "cwd",
|
|
1997
2172
|
get: (p) => p.cwd ?? ""
|
|
1998
2173
|
}
|
|
1999
|
-
]
|
|
2174
|
+
];
|
|
2175
|
+
if (hasAgent) cols.push({
|
|
2176
|
+
label: "agent",
|
|
2177
|
+
get: (p) => p.agentStatus ?? ""
|
|
2178
|
+
});
|
|
2179
|
+
output({ panes }, () => printTable(panes, cols));
|
|
2000
2180
|
}));
|
|
2001
2181
|
}
|
|
2002
2182
|
function existsCommand(deps) {
|
|
2003
|
-
return new Command("exists").description("Probe whether a single pane is still live (exit 0 = live, 1 = gone)").argument("
|
|
2183
|
+
return new Command("exists").description("Probe whether a single pane is still live (exit 0 = live, 1 = gone)").argument("[pane]", "Target pane id").addOption(FORMAT_OPTION).action(guarded((pane) => {
|
|
2004
2184
|
const a = adapter(deps);
|
|
2005
2185
|
const t = resolveTarget(deps, a, pane);
|
|
2006
2186
|
const live = a.paneExists(deps.exec, t);
|
|
@@ -2011,6 +2191,87 @@ function existsCommand(deps) {
|
|
|
2011
2191
|
if (!live) process.exit(1);
|
|
2012
2192
|
}));
|
|
2013
2193
|
}
|
|
2194
|
+
/**
|
|
2195
|
+
* The CLI surface of the library's agent-wait refusal: exit 1 (a genuine operation failure, not a
|
|
2196
|
+
* usage error), naming the backend, with a fix hint pointing at the herdr-only constraint. The
|
|
2197
|
+
* DECISION to refuse is the orchestrator's (`AgentLifecycleUnsupportedError`); this composes only the
|
|
2198
|
+
* presentation, so the sentence lives in one place — the exact mirror of how `backendUnsupported`
|
|
2199
|
+
* surfaces `CaptureUnsupportedError` for `template save`.
|
|
2200
|
+
*/
|
|
2201
|
+
function agentUnsupported(err) {
|
|
2202
|
+
return new CliError("backend-unsupported", `${err.backend} cannot wait on agent-lifecycle state — agent wait needs a backend with a native per-pane agent-state feed (herdr)`, "run agent wait on herdr, the only backend with a native agent-state feed", 1);
|
|
2203
|
+
}
|
|
2204
|
+
/**
|
|
2205
|
+
* `agent status <pane>` — a SNAPSHOT that degrades, never refuses. It resolves a pane through the
|
|
2206
|
+
* shared ladder and prints its `agentStatus`, a fact independent of whether the backend can WAIT on it
|
|
2207
|
+
* (that is `agent wait`'s herdr-only capability). A backend with no agent-state feed still answers
|
|
2208
|
+
* truthfully — the pane, with no status — and exits 0: refusing here would turn a caller away from a
|
|
2209
|
+
* fact the backend CAN answer (which pane this is) because of one it can't (what its agent is doing),
|
|
2210
|
+
* and the two are independent.
|
|
2211
|
+
*/
|
|
2212
|
+
function agentStatusCommand(deps) {
|
|
2213
|
+
return new Command("status").description("Print a pane's agent-lifecycle status (herdr); degrades to no status on a backend with no feed").argument("[pane]", "Target pane id or label").addOption(FORMAT_OPTION).action(guarded((pane) => {
|
|
2214
|
+
const a = adapter(deps);
|
|
2215
|
+
const target = resolveTarget(deps, a, pane);
|
|
2216
|
+
let agentStatus;
|
|
2217
|
+
try {
|
|
2218
|
+
agentStatus = a.listPanes(deps.exec).find((p) => p.id === target.id)?.agentStatus;
|
|
2219
|
+
} catch {
|
|
2220
|
+
agentStatus = void 0;
|
|
2221
|
+
}
|
|
2222
|
+
output({
|
|
2223
|
+
pane: target.id,
|
|
2224
|
+
agentStatus: agentStatus ?? null
|
|
2225
|
+
}, () => printFields({
|
|
2226
|
+
pane: target.id,
|
|
2227
|
+
agentStatus
|
|
2228
|
+
}));
|
|
2229
|
+
}));
|
|
2230
|
+
}
|
|
2231
|
+
/**
|
|
2232
|
+
* `agent wait <pane>` — a blocking DRIVE of herdr's native `agent wait`, or a refusal naming the
|
|
2233
|
+
* backend on one without the capability. Unlike `agent status`, waiting has no truthful degrade, so a
|
|
2234
|
+
* backend with no agent-lifecycle primitive is refused (`backend-unsupported`, exit 1) rather than
|
|
2235
|
+
* answered with a guess — the exact mirror of how `template save` refuses a geometry-incapable backend.
|
|
2236
|
+
*
|
|
2237
|
+
* The refusal is checked BEFORE the pane is resolved so a name that resolves nowhere never masks the
|
|
2238
|
+
* herdr-only refusal; the orchestrator (`deriveAgentWait`) re-checks the same seam member, that check
|
|
2239
|
+
* being its own library contract. `--until` is repeatable (omit to take herdr's default set) and
|
|
2240
|
+
* `--timeout` is optional (omit for an indefinite wait).
|
|
2241
|
+
*/
|
|
2242
|
+
function agentWaitCommand(deps) {
|
|
2243
|
+
return new Command("wait").description("Block until a pane's agent reaches a state (herdr); refused on a backend with no agent-state feed").argument("[pane]", "Target pane id or label").option("--until <status...>", "Agent states any of which ends the wait (idle, working, blocked, done, unknown); omit for herdr’s default").option("--timeout <ms>", "Milliseconds before the wait gives up; omit to wait indefinitely", (v) => Number.parseInt(v, 10)).addOption(FORMAT_OPTION).addHelpText("after", "\nagent wait drives a native per-pane agent-state feed, which only herdr has — tmux, wezterm and\nzellij are refused with backend-unsupported. Use agent status for a snapshot that works everywhere.").action(guarded((pane, opts) => {
|
|
2244
|
+
const a = adapter(deps);
|
|
2245
|
+
if (!a.agentLifecycle) throw agentUnsupported(new AgentLifecycleUnsupportedError(a.name));
|
|
2246
|
+
const target = resolveTarget(deps, a, pane);
|
|
2247
|
+
let reached;
|
|
2248
|
+
try {
|
|
2249
|
+
reached = deriveAgentWait(a, deps.exec, target, {
|
|
2250
|
+
until: opts.until,
|
|
2251
|
+
timeoutMs: opts.timeout
|
|
2252
|
+
});
|
|
2253
|
+
} catch (err) {
|
|
2254
|
+
if (err instanceof CliError) throw err;
|
|
2255
|
+
if (err instanceof AgentLifecycleUnsupportedError) throw agentUnsupported(err);
|
|
2256
|
+
throw new CliError("agent-wait-failed", err instanceof Error ? err.message : String(err), "check the pane still exists with: cyber-mux exists <pane>", 1);
|
|
2257
|
+
}
|
|
2258
|
+
output({
|
|
2259
|
+
pane: target.id,
|
|
2260
|
+
agentStatus: reached
|
|
2261
|
+
}, () => console.log(reached));
|
|
2262
|
+
}));
|
|
2263
|
+
}
|
|
2264
|
+
/**
|
|
2265
|
+
* The `agent` group reaches the herdr agent-lifecycle capability: `status` (a snapshot that degrades
|
|
2266
|
+
* truthfully) and `wait` (a blocking drive of herdr's native wait, refused on every backend without
|
|
2267
|
+
* it). The state feed itself is herdr's; this group is how the CLI reads and blocks on it.
|
|
2268
|
+
*/
|
|
2269
|
+
function agentCommand(deps) {
|
|
2270
|
+
const cmd = new Command("agent").description("Inspect and wait on a pane's agent-lifecycle state (herdr)");
|
|
2271
|
+
cmd.addCommand(agentStatusCommand(deps));
|
|
2272
|
+
cmd.addCommand(agentWaitCommand(deps));
|
|
2273
|
+
return cmd;
|
|
2274
|
+
}
|
|
2014
2275
|
function worktreeAddCommand(deps) {
|
|
2015
2276
|
return new Command("add").description("Create a git worktree, and open it when given a placement — grouped where the backend can").requiredOption("--branch <branch>", "Branch to create the worktree on").option("--path <path>", "Where to check out the worktree (default: a sibling of the primary checkout)").option("--base <ref>", "Start point for the new branch (default: the current HEAD)").option("--launch <command>", "Command to run in the opened pane; implies --at workspace").addOption(templateOption()).addOption(AT_OPTION).addOption(ENV_OPTION).addOption(LABEL_OPTION).addOption(FORMAT_OPTION).action((opts) => {
|
|
2016
2277
|
try {
|
|
@@ -2085,6 +2346,47 @@ function worktreeAddCommand(deps) {
|
|
|
2085
2346
|
}
|
|
2086
2347
|
});
|
|
2087
2348
|
}
|
|
2349
|
+
/**
|
|
2350
|
+
* `worktree provision` — reuse a free worktree or create a fresh one, the CLI wiring of the
|
|
2351
|
+
* `provisionWorktree` seam. The verb uses the DEFAULT availability gate (`isWorktreeRemovable`, the
|
|
2352
|
+
* exact set `worktree list` marks `(removable)` and `prune` removes) and offers NO flag to inject a
|
|
2353
|
+
* host predicate — that is the surface divergence from the library seam, which takes one. A host that
|
|
2354
|
+
* must exclude a live-session worktree calls `WorktreeApi.provision` directly. It reports what it did
|
|
2355
|
+
* (`reused` | `created`), the worktree, and on reuse the recycled entry — its prior branch and the
|
|
2356
|
+
* workspace it was open in; `printFields` drops the nullish reuse fields on a create.
|
|
2357
|
+
*/
|
|
2358
|
+
function worktreeProvisionCommand(deps) {
|
|
2359
|
+
return new Command("provision").description("Reuse a free worktree or create a fresh one — the default-gate wiring of the provision seam (a worktree `list` marks \"(removable)\"). Reports whether it reused or created").requiredOption("--branch <branch>", "Branch the provisioned worktree ends up on").option("--base <ref>", "Start point for the fresh branch (default: the resolved default branch, then HEAD)").option("--path <path>", "Where a fresh checkout goes when none is free to reuse (default: a sibling of the primary checkout)").addOption(FORMAT_OPTION).action((opts) => {
|
|
2360
|
+
try {
|
|
2361
|
+
const primaryRoot = resolvePrimaryRoot(deps.exec);
|
|
2362
|
+
const path = opts.path ?? resolveWorktreePath(primaryRoot, opts.branch);
|
|
2363
|
+
const result = provisionWorktree(deps.exec, primaryRoot, { create: {
|
|
2364
|
+
path,
|
|
2365
|
+
branch: opts.branch,
|
|
2366
|
+
base: opts.base
|
|
2367
|
+
} });
|
|
2368
|
+
const reused = result.reused ? {
|
|
2369
|
+
root: result.reused.root,
|
|
2370
|
+
branch: result.reused.branch ?? null,
|
|
2371
|
+
workspace: result.reused.workspace ?? null
|
|
2372
|
+
} : null;
|
|
2373
|
+
output({
|
|
2374
|
+
action: result.action,
|
|
2375
|
+
root: result.worktree.root,
|
|
2376
|
+
branch: result.worktree.branch,
|
|
2377
|
+
reused
|
|
2378
|
+
}, () => printFields({
|
|
2379
|
+
action: result.action,
|
|
2380
|
+
root: result.worktree.root,
|
|
2381
|
+
branch: result.worktree.branch,
|
|
2382
|
+
"reused-from": reused ? reused.branch ?? "(detached)" : void 0,
|
|
2383
|
+
"reused-workspace": reused?.workspace ?? void 0
|
|
2384
|
+
}));
|
|
2385
|
+
} catch (err) {
|
|
2386
|
+
reportWorktreeFailure(err);
|
|
2387
|
+
}
|
|
2388
|
+
});
|
|
2389
|
+
}
|
|
2088
2390
|
function worktreeOpenCommand(deps) {
|
|
2089
2391
|
return new Command("open").description("Open an existing git worktree — groups it with the repo where the backend can bind").argument("<path>", "Worktree path to open").option("--launch <command>", "Command to run in the opened pane").addOption(AT_OPTION).addOption(ENV_OPTION).addOption(LABEL_OPTION).addOption(FORMAT_OPTION).action((path, opts) => {
|
|
2090
2392
|
try {
|
|
@@ -2198,6 +2500,7 @@ function worktreePruneCommand(deps) {
|
|
|
2198
2500
|
function worktreeCommand(deps) {
|
|
2199
2501
|
const cmd = new Command("worktree").description("Git worktree helpers for spawning/tearing down a session");
|
|
2200
2502
|
cmd.addCommand(worktreeAddCommand(deps));
|
|
2503
|
+
cmd.addCommand(worktreeProvisionCommand(deps));
|
|
2201
2504
|
cmd.addCommand(worktreeOpenCommand(deps));
|
|
2202
2505
|
cmd.addCommand(worktreeListCommand(deps));
|
|
2203
2506
|
cmd.addCommand(worktreeRemoveCommand(deps));
|
|
@@ -2273,12 +2576,14 @@ function buildProgram(cliDeps = DEFAULT_DEPS) {
|
|
|
2273
2576
|
program.addCommand(sendCommand(deps));
|
|
2274
2577
|
program.addCommand(submitCommand(deps));
|
|
2275
2578
|
program.addCommand(readCommand(deps));
|
|
2579
|
+
program.addCommand(waitCommand(deps));
|
|
2276
2580
|
program.addCommand(focusCommand(deps));
|
|
2277
2581
|
program.addCommand(closeCommand(deps));
|
|
2278
2582
|
program.addCommand(listCommand(deps));
|
|
2279
2583
|
program.addCommand(existsCommand(deps));
|
|
2280
2584
|
program.addCommand(worktreeCommand(deps));
|
|
2281
2585
|
program.addCommand(templateCommand(deps));
|
|
2586
|
+
program.addCommand(agentCommand(deps));
|
|
2282
2587
|
return exitOverrideTree(program);
|
|
2283
2588
|
}
|
|
2284
2589
|
/** The real CLI entry point — called explicitly by `bin/cyber-mux.mjs`, never as an import-time
|