@mattstack/rt-client 0.10.0 → 0.11.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/client.d.ts +3 -0
- package/dist/commands.d.ts +523 -1
- package/dist/index.d.ts +5 -3
- package/dist/index.js +191 -16
- package/dist/settings/exec.d.ts +2 -0
- package/dist/settings/resolve.d.ts +5 -0
- package/dist/smart-pane.d.ts +48 -0
- package/dist/transport.d.ts +7 -0
- package/package.json +4 -1
- package/src/client.ts +9 -0
- package/src/commands.ts +219 -1
- package/src/index.ts +6 -1
- package/src/settings/exec.ts +47 -13
- package/src/settings/registry-defs.ts +43 -0
- package/src/settings/resolve.ts +24 -3
- package/src/smart-pane.ts +94 -0
- package/src/transport.ts +5 -1
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ async function rtCommand(cmd, payload, opts = {}) {
|
|
|
14
14
|
const res = await fetch(`http://localhost/${cmd}`, {
|
|
15
15
|
unix: sockPath,
|
|
16
16
|
method: "POST",
|
|
17
|
-
headers: { "Content-Type": "application/json" },
|
|
17
|
+
headers: { "Content-Type": "application/json", "X-RT-Client": `rt-client/${process.pid}` },
|
|
18
18
|
body: JSON.stringify(payload),
|
|
19
19
|
signal: AbortSignal.timeout(opts.timeoutMs ?? 15000)
|
|
20
20
|
});
|
|
@@ -242,6 +242,9 @@ function paneSend(a, o = {}) {
|
|
|
242
242
|
payload.callerPane = a.callerPane;
|
|
243
243
|
return rtCommand("pane:send", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30000 });
|
|
244
244
|
}
|
|
245
|
+
function paneFocus(a, o = {}) {
|
|
246
|
+
return rtCommand("pane:focus", { paneId: a.paneId }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 1e4 });
|
|
247
|
+
}
|
|
245
248
|
// src/commands.ts
|
|
246
249
|
var COMMAND_NAMES = [
|
|
247
250
|
"project-mrs:read",
|
|
@@ -282,7 +285,46 @@ var COMMAND_NAMES = [
|
|
|
282
285
|
"pane:accounts",
|
|
283
286
|
"pane:directories",
|
|
284
287
|
"pane:spawn",
|
|
285
|
-
"pane:send"
|
|
288
|
+
"pane:send",
|
|
289
|
+
"pane:focus",
|
|
290
|
+
"cache:read",
|
|
291
|
+
"branch:enrich",
|
|
292
|
+
"cache:refresh",
|
|
293
|
+
"daemon:log-level",
|
|
294
|
+
"ping",
|
|
295
|
+
"status",
|
|
296
|
+
"tray:status",
|
|
297
|
+
"tcc:check",
|
|
298
|
+
"repos",
|
|
299
|
+
"ports",
|
|
300
|
+
"notifications",
|
|
301
|
+
"discussions:refresh",
|
|
302
|
+
"discussions:resolve",
|
|
303
|
+
"discussions:reply",
|
|
304
|
+
"discussions:diffs",
|
|
305
|
+
"mr:action",
|
|
306
|
+
"mr:fetch-job-detail",
|
|
307
|
+
"mr:fetch-job-trace",
|
|
308
|
+
"endpoint:claim",
|
|
309
|
+
"endpoint:lookup",
|
|
310
|
+
"endpoint:release",
|
|
311
|
+
"endpoint:status",
|
|
312
|
+
"repos:locate",
|
|
313
|
+
"freshness:reconcile",
|
|
314
|
+
"hooks:repair",
|
|
315
|
+
"hooks:watch",
|
|
316
|
+
"sdm:catalog",
|
|
317
|
+
"sdm:snapshot",
|
|
318
|
+
"sdm:recents",
|
|
319
|
+
"sdm:reconnect",
|
|
320
|
+
"system-processes",
|
|
321
|
+
"worktree:provision",
|
|
322
|
+
"worktree:create",
|
|
323
|
+
"worktree:dispose",
|
|
324
|
+
"worktree:list",
|
|
325
|
+
"worktree:restore",
|
|
326
|
+
"worktree:freshen",
|
|
327
|
+
"worktree:adopt"
|
|
286
328
|
];
|
|
287
329
|
// src/relay.ts
|
|
288
330
|
var DEFAULT_WS_URL = "ws://127.0.0.1:9401/ws";
|
|
@@ -409,6 +451,56 @@ function repoNameForPath(repoPath, reposJsonPath) {
|
|
|
409
451
|
return fromDb;
|
|
410
452
|
return repoNameFromJson(repoPath, jsonPath);
|
|
411
453
|
}
|
|
454
|
+
// src/smart-pane.ts
|
|
455
|
+
var DEFAULT_MIN_COLS = 50;
|
|
456
|
+
var DEFAULT_MIN_ROWS = 14;
|
|
457
|
+
function decidePlacement(rect, opts = {}) {
|
|
458
|
+
const minCols = opts.minCols ?? DEFAULT_MIN_COLS;
|
|
459
|
+
const minRows = opts.minRows ?? DEFAULT_MIN_ROWS;
|
|
460
|
+
if (!rect || rect.width <= 0 || rect.height <= 0)
|
|
461
|
+
return { kind: "split", direction: "right" };
|
|
462
|
+
const canRight = rect.width >= 2 * minCols;
|
|
463
|
+
const canDown = rect.height >= 2 * minRows;
|
|
464
|
+
if (!canRight && !canDown)
|
|
465
|
+
return { kind: "tab" };
|
|
466
|
+
if (canRight && canDown) {
|
|
467
|
+
return rect.width > 2 * rect.height ? { kind: "split", direction: "right" } : { kind: "split", direction: "down" };
|
|
468
|
+
}
|
|
469
|
+
return canRight ? { kind: "split", direction: "right" } : { kind: "split", direction: "down" };
|
|
470
|
+
}
|
|
471
|
+
async function anchorRect(herdr, anchorPaneId) {
|
|
472
|
+
const r = await herdr("pane.layout", { pane_id: anchorPaneId });
|
|
473
|
+
if (!r.ok)
|
|
474
|
+
return null;
|
|
475
|
+
const pane = (r.result?.layout?.panes ?? []).find((p) => p.pane_id === anchorPaneId);
|
|
476
|
+
return pane?.rect ? { width: pane.rect.width, height: pane.rect.height } : null;
|
|
477
|
+
}
|
|
478
|
+
async function openSmartPane(herdr, anchorPaneId, opts = {}) {
|
|
479
|
+
const focus = opts.focus ?? true;
|
|
480
|
+
const placement = decidePlacement(await anchorRect(herdr, anchorPaneId), opts);
|
|
481
|
+
let paneId;
|
|
482
|
+
if (placement.kind === "split") {
|
|
483
|
+
const s = await herdr("pane.split", { pane_id: anchorPaneId, direction: placement.direction, focus });
|
|
484
|
+
if (!s.ok)
|
|
485
|
+
throw new Error(`pane.split failed: ${s.message}`);
|
|
486
|
+
paneId = s.result?.pane?.pane_id;
|
|
487
|
+
if (!paneId)
|
|
488
|
+
throw new Error("pane.split returned no pane_id");
|
|
489
|
+
} else {
|
|
490
|
+
const workspaceId = anchorPaneId.split(":")[0];
|
|
491
|
+
const t = await herdr("tab.create", { workspace_id: workspaceId, focus });
|
|
492
|
+
if (!t.ok)
|
|
493
|
+
throw new Error(`tab.create failed: ${t.message}`);
|
|
494
|
+
paneId = t.result?.root_pane?.pane_id;
|
|
495
|
+
if (!paneId)
|
|
496
|
+
throw new Error("tab.create returned no pane_id");
|
|
497
|
+
}
|
|
498
|
+
if (opts.command) {
|
|
499
|
+
await herdr("pane.send_text", { pane_id: paneId, text: opts.command });
|
|
500
|
+
await herdr("pane.send_keys", { pane_id: paneId, keys: ["enter"] });
|
|
501
|
+
}
|
|
502
|
+
return { paneId, placement };
|
|
503
|
+
}
|
|
412
504
|
// src/settings/resolve.ts
|
|
413
505
|
import { homedir as homedir4 } from "os";
|
|
414
506
|
import { join as join5 } from "path";
|
|
@@ -478,6 +570,15 @@ var REGISTRY = [
|
|
|
478
570
|
migrated: true,
|
|
479
571
|
description: "Per-repo worktree pool config (onDeck size, ready steps, name pool); root/branchFormat/ready computed-or-empty in the reader."
|
|
480
572
|
},
|
|
573
|
+
{
|
|
574
|
+
key: "rt.worktreeReadyApproval",
|
|
575
|
+
type: "string",
|
|
576
|
+
scopes: ALL_SCOPES,
|
|
577
|
+
merge: "replace",
|
|
578
|
+
repoScoped: true,
|
|
579
|
+
migrated: true,
|
|
580
|
+
description: "Per-repo user approval of a team-authored `ready` shell ladder, as its content hash (RT-89). The reader trusts only user/machine scopes so a team store can never approve its own shell; a hash mismatch after a team edit re-holds the ladder until `rt worktree ready-approve` records the new one."
|
|
581
|
+
},
|
|
481
582
|
{
|
|
482
583
|
key: "rt.repoIdentityOverrides",
|
|
483
584
|
type: "object",
|
|
@@ -623,6 +724,24 @@ var REGISTRY = [
|
|
|
623
724
|
migrated: true,
|
|
624
725
|
description: "Age floor in days for the log janitor pruning every surface's rotated log files under ~/.mattstack/rt/logs (default 14). A fresh key, not an ownership-latch port, so a default is fine here."
|
|
625
726
|
},
|
|
727
|
+
{
|
|
728
|
+
key: "rt.logLevel",
|
|
729
|
+
type: "string",
|
|
730
|
+
scopes: ["machine", "user"],
|
|
731
|
+
default: "info",
|
|
732
|
+
merge: "replace",
|
|
733
|
+
migrated: true,
|
|
734
|
+
description: "Daemon log level (trace|debug|info|warn|error). RT_LOG_LEVEL env wins, then this setting, then info (lib/daemon-logger.ts resolveDaemonLogLevel). A fresh key, not an ownership-latch port, so a default is fine here."
|
|
735
|
+
},
|
|
736
|
+
{
|
|
737
|
+
key: "rt.apiPort",
|
|
738
|
+
type: "number",
|
|
739
|
+
scopes: ["machine", "user"],
|
|
740
|
+
default: 9401,
|
|
741
|
+
merge: "replace",
|
|
742
|
+
migrated: true,
|
|
743
|
+
description: "TCP port for the daemon's local HTTP/WS API. Escape hatch when 9401 is held: RT_API_PORT env wins, then this setting, then 9401 (lib/daemon-config.ts resolveApiPort(), read at bind time by lib/daemon/api-server.ts)."
|
|
744
|
+
},
|
|
626
745
|
{
|
|
627
746
|
key: "rt.hooks",
|
|
628
747
|
type: "object",
|
|
@@ -632,6 +751,21 @@ var REGISTRY = [
|
|
|
632
751
|
migrated: true,
|
|
633
752
|
description: "Per-repo git hook enable/disable state ({enabled, hooks: {<hookName>: boolean}}); ownership-latch port of repos/<repo>/hooks.json, store wins per field once it owns the key — including per-hook-name entries inside the nested hooks map, each defaulting to enabled when absent. The installed git-hook shim still greps repos/<repo>/hooks.json with zero process spawns (a hook fires on every git operation); that file is now a DERIVED CACHE this key writes through, kept current by commands/hooks.ts's regenerateHooksCache at every write seam."
|
|
634
753
|
},
|
|
754
|
+
{
|
|
755
|
+
key: "rt.daemonPath",
|
|
756
|
+
type: "string",
|
|
757
|
+
scopes: ["machine"],
|
|
758
|
+
merge: "replace",
|
|
759
|
+
description: "Absolute colon-separated PATH the daemon uses for every child it spawns, instead of probing your login shell. Set this when the daemon can't find node/git/bun/pnpm (e.g. a fish shell, a blocking .zshrc, or PATH exports that live only in .zshrc). Machine-scoped: it never travels to another machine."
|
|
760
|
+
},
|
|
761
|
+
{
|
|
762
|
+
key: "rt.trustedBrowserOrigins",
|
|
763
|
+
type: "array",
|
|
764
|
+
scopes: ["user", "machine"],
|
|
765
|
+
default: [],
|
|
766
|
+
merge: "replace",
|
|
767
|
+
description: "Browser Origins (scheme://host:port, exact string match) trusted to read the :9401 daemon API and subscribe to /ws without presenting the local api-token -- e.g. a locally-hosted console or chat-viewer dev server. Empty by default: every current mattstack consumer (the CLI, the Swift tray, rt-client from Bun/Node processes, the VS Code extension) is a non-browser client (sends no Origin header at all) and is unaffected either way."
|
|
768
|
+
},
|
|
635
769
|
{
|
|
636
770
|
key: "mattstack.integrations",
|
|
637
771
|
type: "object",
|
|
@@ -1292,8 +1426,24 @@ function expandCtxFrom(opts) {
|
|
|
1292
1426
|
teamsDir: teamsDir()
|
|
1293
1427
|
};
|
|
1294
1428
|
}
|
|
1429
|
+
var warnSink = null;
|
|
1430
|
+
var warnedOnce = new Set;
|
|
1431
|
+
function setSettingsWarnSink(sink) {
|
|
1432
|
+
warnSink = sink;
|
|
1433
|
+
warnedOnce.clear();
|
|
1434
|
+
}
|
|
1435
|
+
function emitSettingsWarning(msg) {
|
|
1436
|
+
if (warnSink) {
|
|
1437
|
+
if (warnedOnce.has(msg))
|
|
1438
|
+
return;
|
|
1439
|
+
warnedOnce.add(msg);
|
|
1440
|
+
warnSink(msg);
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
console.warn(msg);
|
|
1444
|
+
}
|
|
1295
1445
|
function warnInvalid(key, entry) {
|
|
1296
|
-
|
|
1446
|
+
emitSettingsWarning(`rt: ignoring "${key}" from the ${entry.scope} scope (${entry.file ?? "no file"}): ${entry.reason}`);
|
|
1297
1447
|
}
|
|
1298
1448
|
function getSetting(key, opts = {}) {
|
|
1299
1449
|
const def = getDef(key);
|
|
@@ -1328,7 +1478,7 @@ function listSettings(opts = {}) {
|
|
|
1328
1478
|
listed.value = expandVariables(resolution.value, ctx);
|
|
1329
1479
|
} catch (err) {
|
|
1330
1480
|
listed.expandError = err.message;
|
|
1331
|
-
|
|
1481
|
+
emitSettingsWarning(`rt: showing "${def.key}" unexpanded — ${listed.expandError}`);
|
|
1332
1482
|
}
|
|
1333
1483
|
}
|
|
1334
1484
|
out.push(listed);
|
|
@@ -1356,7 +1506,7 @@ function listUnregistered(stores, opts) {
|
|
|
1356
1506
|
scan("machine", stores.machine.file, stores.machine.global);
|
|
1357
1507
|
scan("machine.repo", stores.machine.file, repoSection(stores.machine));
|
|
1358
1508
|
return [...found.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, hit]) => {
|
|
1359
|
-
|
|
1509
|
+
emitSettingsWarning(`rt: unregistered setting "${key}" in ${hit.file} — ignoring it (this rt may be older than the store)`);
|
|
1360
1510
|
return {
|
|
1361
1511
|
key,
|
|
1362
1512
|
value: hit.value,
|
|
@@ -1581,21 +1731,42 @@ async function runCapture(argv, opts = {}) {
|
|
|
1581
1731
|
} catch {
|
|
1582
1732
|
return { stdout: "", stderr: "", exitCode: -1 };
|
|
1583
1733
|
}
|
|
1584
|
-
const
|
|
1734
|
+
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
1735
|
+
let killTimer;
|
|
1736
|
+
const term = setTimeout(() => {
|
|
1585
1737
|
try {
|
|
1586
|
-
proc.kill();
|
|
1738
|
+
proc.kill("SIGTERM");
|
|
1587
1739
|
} catch {}
|
|
1588
|
-
|
|
1740
|
+
killTimer = setTimeout(() => {
|
|
1741
|
+
try {
|
|
1742
|
+
proc.kill("SIGKILL");
|
|
1743
|
+
} catch {}
|
|
1744
|
+
}, 2000);
|
|
1745
|
+
killTimer.unref?.();
|
|
1746
|
+
}, timeoutMs);
|
|
1747
|
+
const captured = (async () => {
|
|
1748
|
+
try {
|
|
1749
|
+
const stdoutPromise = new Response(proc.stdout).text();
|
|
1750
|
+
const stderrPromise = captureStderr ? new Response(proc.stderr).text() : Promise.resolve("");
|
|
1751
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
1752
|
+
stdoutPromise,
|
|
1753
|
+
stderrPromise,
|
|
1754
|
+
proc.exited
|
|
1755
|
+
]);
|
|
1756
|
+
return { stdout, stderr, exitCode };
|
|
1757
|
+
} catch {
|
|
1758
|
+
return { stdout: "", stderr: "", exitCode: -1 };
|
|
1759
|
+
}
|
|
1760
|
+
})();
|
|
1761
|
+
let deadlineTimer;
|
|
1762
|
+
const deadline = new Promise((resolve) => {
|
|
1763
|
+
deadlineTimer = setTimeout(() => resolve({ stdout: "", stderr: "", exitCode: -1, timedOut: true }), timeoutMs);
|
|
1764
|
+
});
|
|
1589
1765
|
try {
|
|
1590
|
-
|
|
1591
|
-
const stderrPromise = captureStderr ? new Response(proc.stderr).text() : Promise.resolve("");
|
|
1592
|
-
const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
1593
|
-
const exitCode = await proc.exited;
|
|
1594
|
-
return { stdout, stderr, exitCode };
|
|
1595
|
-
} catch {
|
|
1596
|
-
return { stdout: "", stderr: "", exitCode: -1 };
|
|
1766
|
+
return await Promise.race([captured, deadline]);
|
|
1597
1767
|
} finally {
|
|
1598
|
-
clearTimeout(
|
|
1768
|
+
clearTimeout(term);
|
|
1769
|
+
clearTimeout(deadlineTimer);
|
|
1599
1770
|
}
|
|
1600
1771
|
}
|
|
1601
1772
|
|
|
@@ -1718,6 +1889,7 @@ export {
|
|
|
1718
1889
|
validateValue,
|
|
1719
1890
|
unsetSetting,
|
|
1720
1891
|
subscribe,
|
|
1892
|
+
setSettingsWarnSink,
|
|
1721
1893
|
setSetting,
|
|
1722
1894
|
serializeIdentity,
|
|
1723
1895
|
rtCommand,
|
|
@@ -1734,8 +1906,10 @@ export {
|
|
|
1734
1906
|
paneSend,
|
|
1735
1907
|
panePeek,
|
|
1736
1908
|
paneList,
|
|
1909
|
+
paneFocus,
|
|
1737
1910
|
paneDirectories,
|
|
1738
1911
|
paneAccounts,
|
|
1912
|
+
openSmartPane,
|
|
1739
1913
|
normalizeRemote,
|
|
1740
1914
|
listTeams,
|
|
1741
1915
|
listSettings,
|
|
@@ -1749,6 +1923,7 @@ export {
|
|
|
1749
1923
|
expandVariables,
|
|
1750
1924
|
eventsHead,
|
|
1751
1925
|
deriveRepoIdentity,
|
|
1926
|
+
decidePlacement,
|
|
1752
1927
|
daemonHealth,
|
|
1753
1928
|
createRelay,
|
|
1754
1929
|
clearIdentityMemo,
|
package/dist/settings/exec.d.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface RunResult {
|
|
|
11
11
|
stdout: string;
|
|
12
12
|
stderr: string;
|
|
13
13
|
exitCode: number;
|
|
14
|
+
/** Set true only when the deadline fired before the child settled. */
|
|
15
|
+
timedOut?: boolean;
|
|
14
16
|
}
|
|
15
17
|
/**
|
|
16
18
|
* Run argv and capture stdout. Never throws: spawn failures and timeouts
|
|
@@ -120,6 +120,11 @@ export interface ExpandCtx {
|
|
|
120
120
|
* mutates its input.
|
|
121
121
|
*/
|
|
122
122
|
export declare function expandVariables(value: unknown, ctx: ExpandCtx): unknown;
|
|
123
|
+
/** The daemon binds a deduped log.warn here so a hot-path getSetting on a
|
|
124
|
+
* disallowed-scope key warns once, not every tick. Default: console.warn
|
|
125
|
+
* (CLI/test behavior unchanged). null restores the default. */
|
|
126
|
+
export declare function setSettingsWarnSink(sink: ((msg: string) => void) | null): void;
|
|
127
|
+
export declare function emitSettingsWarning(msg: string): void;
|
|
123
128
|
/**
|
|
124
129
|
* Resolves one key across the whole ladder. Throws for an unregistered key —
|
|
125
130
|
* an explicit get of something rt has never heard of is a caller bug, not a
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Layout-smart herdr pane placement. `decidePlacement` is pure (geometry ->
|
|
3
|
+
* where a new pane should go); `openSmartPane` reads an anchor pane's real
|
|
4
|
+
* rect, decides, and performs the herdr call through an injected caller so it
|
|
5
|
+
* works over any transport (socket API or a CLI adapter).
|
|
6
|
+
*/
|
|
7
|
+
export type Placement = {
|
|
8
|
+
kind: "split";
|
|
9
|
+
direction: "right" | "down";
|
|
10
|
+
} | {
|
|
11
|
+
kind: "tab";
|
|
12
|
+
};
|
|
13
|
+
export interface PlacementOpts {
|
|
14
|
+
minCols?: number;
|
|
15
|
+
minRows?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface HerdrCall {
|
|
18
|
+
(method: string, params: Record<string, unknown>): Promise<{
|
|
19
|
+
ok: true;
|
|
20
|
+
result: any;
|
|
21
|
+
} | {
|
|
22
|
+
ok: false;
|
|
23
|
+
code: string;
|
|
24
|
+
message: string;
|
|
25
|
+
}>;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Where to put a new pane relative to an anchor whose rect (in cells) is given.
|
|
29
|
+
* Cells are ~2:1 (height:width in px), so a pane is visually wider than tall
|
|
30
|
+
* when width > 2*height; split the longer visual axis so both halves stay
|
|
31
|
+
* usable. A null/degenerate rect falls back to a right split (old behavior).
|
|
32
|
+
*/
|
|
33
|
+
export declare function decidePlacement(rect: {
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
} | null, opts?: PlacementOpts): Placement;
|
|
37
|
+
/**
|
|
38
|
+
* Open a herdr pane placed intelligently next to `anchorPaneId` (split right or
|
|
39
|
+
* down, or a new tab when crowded). Optionally run `command` in it. Returns the
|
|
40
|
+
* new pane id and the placement chosen. Throws on a herdr failure.
|
|
41
|
+
*/
|
|
42
|
+
export declare function openSmartPane(herdr: HerdrCall, anchorPaneId: string, opts?: {
|
|
43
|
+
command?: string;
|
|
44
|
+
focus?: boolean;
|
|
45
|
+
} & PlacementOpts): Promise<{
|
|
46
|
+
paneId: string;
|
|
47
|
+
placement: Placement;
|
|
48
|
+
}>;
|
package/dist/transport.d.ts
CHANGED
|
@@ -2,6 +2,13 @@ export interface RtResponse<T = unknown> {
|
|
|
2
2
|
ok: boolean;
|
|
3
3
|
data?: T;
|
|
4
4
|
error?: string;
|
|
5
|
+
/** Structured form of `error` on a handler throw (R035): `code` defaults
|
|
6
|
+
* to "handler-threw" when the thrown error carries none. Additive; older
|
|
7
|
+
* daemons and the reject path never set this. */
|
|
8
|
+
failure?: {
|
|
9
|
+
code: string;
|
|
10
|
+
message: string;
|
|
11
|
+
};
|
|
5
12
|
}
|
|
6
13
|
export interface RtClientOptions {
|
|
7
14
|
sockPath?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mattstack/rt-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": {
|
|
@@ -51,5 +51,8 @@
|
|
|
51
51
|
},
|
|
52
52
|
"publishConfig": {
|
|
53
53
|
"access": "public"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"typescript": "^5.9.2"
|
|
54
57
|
}
|
|
55
58
|
}
|
package/src/client.ts
CHANGED
|
@@ -388,3 +388,12 @@ export function paneSend(
|
|
|
388
388
|
if (a.callerPane !== undefined) payload.callerPane = a.callerPane;
|
|
389
389
|
return rtCommand<Commands["pane:send"]["data"]>("pane:send", payload, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 30_000 });
|
|
390
390
|
}
|
|
391
|
+
|
|
392
|
+
/** Brings a herdr pane to the front. The daemon routes this to the tray, which
|
|
393
|
+
owns the herdr focus and the native terminal-window raise. */
|
|
394
|
+
export function paneFocus(
|
|
395
|
+
a: Commands["pane:focus"]["payload"],
|
|
396
|
+
o: RtClientOptions = {},
|
|
397
|
+
): Promise<RtResponse<Commands["pane:focus"]["data"]>> {
|
|
398
|
+
return rtCommand<Commands["pane:focus"]["data"]>("pane:focus", { paneId: a.paneId }, { sockPath: o.sockPath, timeoutMs: o.timeoutMs ?? 10_000 });
|
|
399
|
+
}
|
package/src/commands.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* its functions against this map so a new command only needs an entry here
|
|
5
5
|
* plus one function, never a change to the transport itself.
|
|
6
6
|
*/
|
|
7
|
-
import type { PullRequest, MRDetail } from "@mattstack/glance";
|
|
7
|
+
import type { PullRequest, MRDetail, Pipeline } from "@mattstack/glance";
|
|
8
8
|
|
|
9
9
|
export type Discussion = MRDetail["discussions"][number];
|
|
10
10
|
|
|
@@ -166,6 +166,7 @@ export interface InviteResult { paneId: string; delivered: "accepted" | "queued"
|
|
|
166
166
|
/** Duplicated shape on purpose: mirrors lib/daemon/inject.ts's InjectResult. */
|
|
167
167
|
export type PaneDelivery = "accepted" | "queued" | "refused";
|
|
168
168
|
export interface PaneSendResult { paneId: string; delivered: PaneDelivery; reason?: string }
|
|
169
|
+
export interface PaneFocusResult { paneId: string; focused: boolean }
|
|
169
170
|
|
|
170
171
|
// SKILLS-53: one judgment, computed once in rt, so the console and the tray
|
|
171
172
|
// never derive two verdicts that can disagree.
|
|
@@ -225,6 +226,127 @@ export interface AgentRecord {
|
|
|
225
226
|
createdAt: number; lastResumedAt?: number; finishedAt?: number;
|
|
226
227
|
}
|
|
227
228
|
|
|
229
|
+
// ─── The daemon's remaining out-of-process commands (R013/R016) ──
|
|
230
|
+
// rt CLI <-> daemon, tray <-> daemon, and VS Code extension <-> daemon are
|
|
231
|
+
// all separate OS processes, so any command reachable from one counts as
|
|
232
|
+
// "external" here even when the only known caller today is rt's own CLI.
|
|
233
|
+
|
|
234
|
+
/** Duplicated shape on purpose (see EventsBusEvent above): mirrors lib/daemon/health.ts's HealthSnapshot. */
|
|
235
|
+
export type HealthLevel = "ok" | "degraded" | "unhealthy";
|
|
236
|
+
export interface HealthMetrics { rss: number; heapUsed: number; external: number; uptimeMs: number; wsClients: number; watchers: number }
|
|
237
|
+
export interface HealthEventLoop { maxLagMs: number; lastStallAt: number | null; lastStallCmd: string | null; stalls: number }
|
|
238
|
+
export interface DaemonIdentity { flavor: "dev" | "prod"; version: string; sourceRev: string | null; startedAt: number }
|
|
239
|
+
|
|
240
|
+
export interface PingData extends DaemonIdentity {
|
|
241
|
+
uptime: number;
|
|
242
|
+
pid: number;
|
|
243
|
+
health: HealthLevel;
|
|
244
|
+
eventLoop: HealthEventLoop;
|
|
245
|
+
heartbeatSeq: number;
|
|
246
|
+
supervision: { bootAttempts: number; lastReadyAt: number | null; recentFailures: unknown[]; lastExit: unknown };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export interface StatusData {
|
|
250
|
+
pid: number; uptime: number; watchedRepos: number; cacheEntries: number;
|
|
251
|
+
portsCached: number; portCacheAge: number | null;
|
|
252
|
+
freshness: unknown; identity: DaemonIdentity;
|
|
253
|
+
health: { level: HealthLevel; reasons: string[] }; metrics: HealthMetrics; eventLoop: HealthEventLoop;
|
|
254
|
+
worktreePool: { dormant: true; repos: string[]; message: string } | { dormant: false };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export interface TrayStatusData {
|
|
258
|
+
pid: number; uptime: number; memoryUsage: number; watchedRepos: number; cacheEntries: number;
|
|
259
|
+
portsCached: number; portCacheAge: number | null; lastRefresh: number | null;
|
|
260
|
+
portsByRepo: Record<string, number>; pendingNotifications: number;
|
|
261
|
+
health: { level: HealthLevel; reasons: string[] }; metrics: HealthMetrics; eventLoop: HealthEventLoop;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Duplicated shape on purpose: mirrors lib/port-scanner.ts's PortEntry. */
|
|
265
|
+
export interface PortEntry {
|
|
266
|
+
port: number; pid: number; command: string; cwd: string;
|
|
267
|
+
repo: string | null; worktree: string | null; branch: string | null;
|
|
268
|
+
relativeDir: string; uptime: string;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export interface PortsData {
|
|
272
|
+
ports: PortEntry[];
|
|
273
|
+
grouped: Record<string, Record<string, PortEntry[]>>;
|
|
274
|
+
updatedAt: number;
|
|
275
|
+
age: number | null;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Duplicated shape on purpose: mirrors lib/state/notifier-store.ts's NotificationEvent. */
|
|
279
|
+
export interface RtNotificationEvent {
|
|
280
|
+
id: string; title: string; message: string; url?: string;
|
|
281
|
+
category: string; timestamp: number; pids?: number[];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface ReposData {
|
|
285
|
+
repos: Record<string, { path: string; worktrees: Array<{ path: string; branch: string }> }>;
|
|
286
|
+
watched: string[];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export interface TccCheckData {
|
|
290
|
+
blocked: Array<{ name: string; path: string; error: string }>;
|
|
291
|
+
accessible: string[];
|
|
292
|
+
totalRepos: number;
|
|
293
|
+
daemonPid: number;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export interface WorktreeTreeRow {
|
|
297
|
+
name: string; kind: string; state: string; path: string; branch: string | null;
|
|
298
|
+
repoName: string; mr: { iid: number; state: string; title: string } | null;
|
|
299
|
+
duplicateBranch?: true;
|
|
300
|
+
[extra: string]: unknown;
|
|
301
|
+
}
|
|
302
|
+
export interface WorktreeListData {
|
|
303
|
+
trees: WorktreeTreeRow[];
|
|
304
|
+
dormant?: true; dormantRepos?: string[]; message?: string;
|
|
305
|
+
readyHeld?: true; readyHeldRepos?: string[];
|
|
306
|
+
}
|
|
307
|
+
export interface WorktreeProvisionData {
|
|
308
|
+
tree: string; path: string; branch: string; wasOnDeck: boolean;
|
|
309
|
+
readyAt: string | null; branchState: "new" | "tracking-remote" | "existing-clean" | "diverged" | "behind";
|
|
310
|
+
readyFailed?: true; failedStep?: string;
|
|
311
|
+
}
|
|
312
|
+
export interface WorktreeCreateData { tree: string; path: string }
|
|
313
|
+
export interface WorktreeDisposeData {
|
|
314
|
+
disposed: string[];
|
|
315
|
+
refused: Array<{ tree: string; reason: string }>;
|
|
316
|
+
recoverable: Array<{ tree: string; path: string; until: string }>;
|
|
317
|
+
}
|
|
318
|
+
export interface WorktreeRestoreData {
|
|
319
|
+
restored: true; path: string; tree: string; readyFailed?: true; failedStep?: string;
|
|
320
|
+
}
|
|
321
|
+
export interface WorktreeFreshenData { ran: string[] }
|
|
322
|
+
export interface WorktreeAdoptData {
|
|
323
|
+
main: string; claimed: string[]; unmanaged: string[]; disposed: string[];
|
|
324
|
+
refused: Array<{ tree: string; reason: string }>;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Duplicated shape on purpose: mirrors lib/endpoint/store.ts's EndpointClaim. */
|
|
328
|
+
export interface EndpointClaim { worktree: string; role: string; port: number; ts: number }
|
|
329
|
+
export interface EndpointRoleRef { port: number; url: string; running: boolean }
|
|
330
|
+
export interface EndpointClaimData { role: string; port: number; url: string; refs: Record<string, EndpointRoleRef> }
|
|
331
|
+
export interface EndpointLookupData { claimed: boolean; port: number | null; url: string | null; running: boolean }
|
|
332
|
+
export interface EndpointReleaseData { released: number }
|
|
333
|
+
export interface EndpointStatusData { repos: Record<string, Array<EndpointClaim & { running: boolean }>> }
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Duplicated shape on purpose: mirrors @mattstack/glance's `JobDetail`
|
|
337
|
+
* (types.ts), which the package does not re-export from its index.
|
|
338
|
+
*/
|
|
339
|
+
export type MrJobDetail = { type: "trace"; content: string } | { type: "bridge"; downstreamPipeline: Pipeline };
|
|
340
|
+
|
|
341
|
+
export interface DiscussionsWriteData { discussions: Discussion[]; fetchedAt: number }
|
|
342
|
+
export interface DiscussionsDiffsData { diffs: Array<{ newPath: string; diff: string }>; truncated: boolean }
|
|
343
|
+
|
|
344
|
+
export type MRActionName =
|
|
345
|
+
| "merge" | "rebase" | "approve" | "unapprove"
|
|
346
|
+
| "setAutoMerge" | "cancelAutoMerge"
|
|
347
|
+
| "retryJob" | "retryPipeline"
|
|
348
|
+
| "toggleDraft" | "requestReReview";
|
|
349
|
+
|
|
228
350
|
export interface Commands {
|
|
229
351
|
"project-mrs:read": { payload: { repoName: string; maxAgeMs?: number; demand?: DemandDecl }; data: ProjectMRsData };
|
|
230
352
|
"discussions:read": { payload: { repoName: string; iid: number }; data: DiscussionsData };
|
|
@@ -353,6 +475,61 @@ export interface Commands {
|
|
|
353
475
|
data: { pane: ChatPane; ready: boolean };
|
|
354
476
|
};
|
|
355
477
|
"pane:send": { payload: { paneId: string; text: string; callerPane?: string }; data: PaneSendResult };
|
|
478
|
+
"pane:focus": { payload: { paneId: string }; data: PaneFocusResult };
|
|
479
|
+
|
|
480
|
+
// ─── R013/R016 ────────────────────────────────────────────────
|
|
481
|
+
"cache:read": { payload: { branches?: string[]; maxAgeMs?: number; repoIdentity?: string }; data: Record<string, BranchEnrichment> };
|
|
482
|
+
/** `source` ("cache"|"fresh"|"empty") rides alongside `data` on the wire, not nested under it. */
|
|
483
|
+
"branch:enrich": { payload: { branch: string; repoPath?: string; remoteUrl?: string; repoIdentity?: string }; data: BranchEnrichment | null };
|
|
484
|
+
/** Fire-and-forget kickoff; wire reply is `{ok, message}`, not `{ok,data}`. */
|
|
485
|
+
"cache:refresh": { payload: Record<string, never>; data: { message: string } };
|
|
486
|
+
"daemon:log-level": { payload: { level?: "trace" | "debug" | "info" | "warn" | "error" }; data: { level: string } };
|
|
487
|
+
"ping": { payload: Record<string, never>; data: PingData };
|
|
488
|
+
"status": { payload: Record<string, never>; data: StatusData };
|
|
489
|
+
"tray:status": { payload: Record<string, never>; data: TrayStatusData };
|
|
490
|
+
"tcc:check": { payload: Record<string, never>; data: TccCheckData };
|
|
491
|
+
"repos": { payload: Record<string, never>; data: ReposData };
|
|
492
|
+
"ports": { payload: { repo?: string; refresh?: boolean }; data: PortsData };
|
|
493
|
+
"notifications": { payload: Record<string, never>; data: RtNotificationEvent[] };
|
|
494
|
+
|
|
495
|
+
"discussions:refresh": { payload: { repoName: string; iid: number }; data: DiscussionsWriteData };
|
|
496
|
+
"discussions:resolve": { payload: { repoName: string; iid: number; discussionId: string; resolved?: boolean }; data: DiscussionsWriteData };
|
|
497
|
+
"discussions:reply": { payload: { repoName: string; iid: number; discussionId: string; body: string }; data: DiscussionsWriteData };
|
|
498
|
+
"discussions:diffs": { payload: { repoName: string; iid: number }; data: DiscussionsDiffsData };
|
|
499
|
+
|
|
500
|
+
/** Wire reply is `{ok:true}` on success (no `data`); a failure is `{ok:false,error}`. */
|
|
501
|
+
"mr:action": { payload: { repoName: string; iid: number; action: MRActionName; args?: unknown[] }; data: Record<string, never> };
|
|
502
|
+
"mr:fetch-job-detail": { payload: { repoName: string; iid: number; jobId: number; pipelineId?: number }; data: MrJobDetail };
|
|
503
|
+
"mr:fetch-job-trace": { payload: { repoName: string; iid: number; jobId: number }; data: string };
|
|
504
|
+
|
|
505
|
+
"endpoint:claim": { payload: { repo: string; worktree: string; role: string; pid?: number }; data: EndpointClaimData };
|
|
506
|
+
"endpoint:lookup": { payload: { repo: string; worktree: string; role: string }; data: EndpointLookupData };
|
|
507
|
+
"endpoint:release": { payload: { repo: string; worktree: string; role?: string }; data: EndpointReleaseData };
|
|
508
|
+
"endpoint:status": { payload: { repo?: string }; data: EndpointStatusData };
|
|
509
|
+
|
|
510
|
+
"repos:locate": { payload: { newPath: string; repo?: string; dryRun?: boolean }; data: unknown };
|
|
511
|
+
"freshness:reconcile": { payload: Record<string, never>; data: unknown };
|
|
512
|
+
|
|
513
|
+
/** Wire reply on success is always `{ok:true, repaired}` (no `data`
|
|
514
|
+
* wrapper) — `data` here documents the extra field the same way PingData
|
|
515
|
+
* does for `ping`, not the literal wire nesting (R3). */
|
|
516
|
+
"hooks:repair": { payload: { repo: string }; data: { repaired: boolean } };
|
|
517
|
+
"hooks:watch": { payload: { repo: string }; data: Record<string, never> };
|
|
518
|
+
|
|
519
|
+
"sdm:catalog": { payload: { refresh?: boolean }; data: unknown };
|
|
520
|
+
"sdm:snapshot": { payload: { force?: boolean }; data: unknown };
|
|
521
|
+
"sdm:recents": { payload: Record<string, never>; data: unknown };
|
|
522
|
+
"sdm:reconnect": { payload: { key: string }; data: unknown };
|
|
523
|
+
|
|
524
|
+
"system-processes": { payload: Record<string, never>; data: unknown };
|
|
525
|
+
|
|
526
|
+
"worktree:provision": { payload: { repoName: string; branch?: string; ticket?: string; ticketTitle?: string; disposal?: "job" | "merge"; owner?: string }; data: WorktreeProvisionData };
|
|
527
|
+
"worktree:create": { payload: { repoName: string; onDeck?: boolean }; data: WorktreeCreateData };
|
|
528
|
+
"worktree:dispose": { payload: { repoName?: string; owner?: string; tree?: string; force?: boolean; callerPid?: number }; data: WorktreeDisposeData };
|
|
529
|
+
"worktree:list": { payload: { repoName?: string }; data: WorktreeListData };
|
|
530
|
+
"worktree:restore": { payload: { repoName: string; tree: string }; data: WorktreeRestoreData };
|
|
531
|
+
"worktree:freshen": { payload: { repoName?: string; tree?: string }; data: WorktreeFreshenData };
|
|
532
|
+
"worktree:adopt": { payload: { repoName: string; claim?: boolean }; data: WorktreeAdoptData };
|
|
356
533
|
}
|
|
357
534
|
|
|
358
535
|
export type CommandName = keyof Commands;
|
|
@@ -397,4 +574,45 @@ export const COMMAND_NAMES: readonly CommandName[] = [
|
|
|
397
574
|
"pane:directories",
|
|
398
575
|
"pane:spawn",
|
|
399
576
|
"pane:send",
|
|
577
|
+
"pane:focus",
|
|
578
|
+
|
|
579
|
+
// ─── R013/R016 ────────────────────────────────────────────────
|
|
580
|
+
"cache:read",
|
|
581
|
+
"branch:enrich",
|
|
582
|
+
"cache:refresh",
|
|
583
|
+
"daemon:log-level",
|
|
584
|
+
"ping",
|
|
585
|
+
"status",
|
|
586
|
+
"tray:status",
|
|
587
|
+
"tcc:check",
|
|
588
|
+
"repos",
|
|
589
|
+
"ports",
|
|
590
|
+
"notifications",
|
|
591
|
+
"discussions:refresh",
|
|
592
|
+
"discussions:resolve",
|
|
593
|
+
"discussions:reply",
|
|
594
|
+
"discussions:diffs",
|
|
595
|
+
"mr:action",
|
|
596
|
+
"mr:fetch-job-detail",
|
|
597
|
+
"mr:fetch-job-trace",
|
|
598
|
+
"endpoint:claim",
|
|
599
|
+
"endpoint:lookup",
|
|
600
|
+
"endpoint:release",
|
|
601
|
+
"endpoint:status",
|
|
602
|
+
"repos:locate",
|
|
603
|
+
"freshness:reconcile",
|
|
604
|
+
"hooks:repair",
|
|
605
|
+
"hooks:watch",
|
|
606
|
+
"sdm:catalog",
|
|
607
|
+
"sdm:snapshot",
|
|
608
|
+
"sdm:recents",
|
|
609
|
+
"sdm:reconnect",
|
|
610
|
+
"system-processes",
|
|
611
|
+
"worktree:provision",
|
|
612
|
+
"worktree:create",
|
|
613
|
+
"worktree:dispose",
|
|
614
|
+
"worktree:list",
|
|
615
|
+
"worktree:restore",
|
|
616
|
+
"worktree:freshen",
|
|
617
|
+
"worktree:adopt",
|
|
400
618
|
];
|