@mattstack/rt-client 0.10.1 → 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/commands.d.ts +513 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +186 -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/commands.ts +216 -1
- package/src/index.ts +4 -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
|
});
|
|
@@ -286,7 +286,45 @@ var COMMAND_NAMES = [
|
|
|
286
286
|
"pane:directories",
|
|
287
287
|
"pane:spawn",
|
|
288
288
|
"pane:send",
|
|
289
|
-
"pane:focus"
|
|
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"
|
|
290
328
|
];
|
|
291
329
|
// src/relay.ts
|
|
292
330
|
var DEFAULT_WS_URL = "ws://127.0.0.1:9401/ws";
|
|
@@ -413,6 +451,56 @@ function repoNameForPath(repoPath, reposJsonPath) {
|
|
|
413
451
|
return fromDb;
|
|
414
452
|
return repoNameFromJson(repoPath, jsonPath);
|
|
415
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
|
+
}
|
|
416
504
|
// src/settings/resolve.ts
|
|
417
505
|
import { homedir as homedir4 } from "os";
|
|
418
506
|
import { join as join5 } from "path";
|
|
@@ -482,6 +570,15 @@ var REGISTRY = [
|
|
|
482
570
|
migrated: true,
|
|
483
571
|
description: "Per-repo worktree pool config (onDeck size, ready steps, name pool); root/branchFormat/ready computed-or-empty in the reader."
|
|
484
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
|
+
},
|
|
485
582
|
{
|
|
486
583
|
key: "rt.repoIdentityOverrides",
|
|
487
584
|
type: "object",
|
|
@@ -627,6 +724,24 @@ var REGISTRY = [
|
|
|
627
724
|
migrated: true,
|
|
628
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."
|
|
629
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
|
+
},
|
|
630
745
|
{
|
|
631
746
|
key: "rt.hooks",
|
|
632
747
|
type: "object",
|
|
@@ -636,6 +751,21 @@ var REGISTRY = [
|
|
|
636
751
|
migrated: true,
|
|
637
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."
|
|
638
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
|
+
},
|
|
639
769
|
{
|
|
640
770
|
key: "mattstack.integrations",
|
|
641
771
|
type: "object",
|
|
@@ -1296,8 +1426,24 @@ function expandCtxFrom(opts) {
|
|
|
1296
1426
|
teamsDir: teamsDir()
|
|
1297
1427
|
};
|
|
1298
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
|
+
}
|
|
1299
1445
|
function warnInvalid(key, entry) {
|
|
1300
|
-
|
|
1446
|
+
emitSettingsWarning(`rt: ignoring "${key}" from the ${entry.scope} scope (${entry.file ?? "no file"}): ${entry.reason}`);
|
|
1301
1447
|
}
|
|
1302
1448
|
function getSetting(key, opts = {}) {
|
|
1303
1449
|
const def = getDef(key);
|
|
@@ -1332,7 +1478,7 @@ function listSettings(opts = {}) {
|
|
|
1332
1478
|
listed.value = expandVariables(resolution.value, ctx);
|
|
1333
1479
|
} catch (err) {
|
|
1334
1480
|
listed.expandError = err.message;
|
|
1335
|
-
|
|
1481
|
+
emitSettingsWarning(`rt: showing "${def.key}" unexpanded — ${listed.expandError}`);
|
|
1336
1482
|
}
|
|
1337
1483
|
}
|
|
1338
1484
|
out.push(listed);
|
|
@@ -1360,7 +1506,7 @@ function listUnregistered(stores, opts) {
|
|
|
1360
1506
|
scan("machine", stores.machine.file, stores.machine.global);
|
|
1361
1507
|
scan("machine.repo", stores.machine.file, repoSection(stores.machine));
|
|
1362
1508
|
return [...found.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, hit]) => {
|
|
1363
|
-
|
|
1509
|
+
emitSettingsWarning(`rt: unregistered setting "${key}" in ${hit.file} — ignoring it (this rt may be older than the store)`);
|
|
1364
1510
|
return {
|
|
1365
1511
|
key,
|
|
1366
1512
|
value: hit.value,
|
|
@@ -1585,21 +1731,42 @@ async function runCapture(argv, opts = {}) {
|
|
|
1585
1731
|
} catch {
|
|
1586
1732
|
return { stdout: "", stderr: "", exitCode: -1 };
|
|
1587
1733
|
}
|
|
1588
|
-
const
|
|
1734
|
+
const timeoutMs = opts.timeoutMs ?? 1e4;
|
|
1735
|
+
let killTimer;
|
|
1736
|
+
const term = setTimeout(() => {
|
|
1589
1737
|
try {
|
|
1590
|
-
proc.kill();
|
|
1738
|
+
proc.kill("SIGTERM");
|
|
1591
1739
|
} catch {}
|
|
1592
|
-
|
|
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
|
+
});
|
|
1593
1765
|
try {
|
|
1594
|
-
|
|
1595
|
-
const stderrPromise = captureStderr ? new Response(proc.stderr).text() : Promise.resolve("");
|
|
1596
|
-
const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
1597
|
-
const exitCode = await proc.exited;
|
|
1598
|
-
return { stdout, stderr, exitCode };
|
|
1599
|
-
} catch {
|
|
1600
|
-
return { stdout: "", stderr: "", exitCode: -1 };
|
|
1766
|
+
return await Promise.race([captured, deadline]);
|
|
1601
1767
|
} finally {
|
|
1602
|
-
clearTimeout(
|
|
1768
|
+
clearTimeout(term);
|
|
1769
|
+
clearTimeout(deadlineTimer);
|
|
1603
1770
|
}
|
|
1604
1771
|
}
|
|
1605
1772
|
|
|
@@ -1722,6 +1889,7 @@ export {
|
|
|
1722
1889
|
validateValue,
|
|
1723
1890
|
unsetSetting,
|
|
1724
1891
|
subscribe,
|
|
1892
|
+
setSettingsWarnSink,
|
|
1725
1893
|
setSetting,
|
|
1726
1894
|
serializeIdentity,
|
|
1727
1895
|
rtCommand,
|
|
@@ -1741,6 +1909,7 @@ export {
|
|
|
1741
1909
|
paneFocus,
|
|
1742
1910
|
paneDirectories,
|
|
1743
1911
|
paneAccounts,
|
|
1912
|
+
openSmartPane,
|
|
1744
1913
|
normalizeRemote,
|
|
1745
1914
|
listTeams,
|
|
1746
1915
|
listSettings,
|
|
@@ -1754,6 +1923,7 @@ export {
|
|
|
1754
1923
|
expandVariables,
|
|
1755
1924
|
eventsHead,
|
|
1756
1925
|
deriveRepoIdentity,
|
|
1926
|
+
decidePlacement,
|
|
1757
1927
|
daemonHealth,
|
|
1758
1928
|
createRelay,
|
|
1759
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/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
|
|
|
@@ -226,6 +226,127 @@ export interface AgentRecord {
|
|
|
226
226
|
createdAt: number; lastResumedAt?: number; finishedAt?: number;
|
|
227
227
|
}
|
|
228
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
|
+
|
|
229
350
|
export interface Commands {
|
|
230
351
|
"project-mrs:read": { payload: { repoName: string; maxAgeMs?: number; demand?: DemandDecl }; data: ProjectMRsData };
|
|
231
352
|
"discussions:read": { payload: { repoName: string; iid: number }; data: DiscussionsData };
|
|
@@ -355,6 +476,60 @@ export interface Commands {
|
|
|
355
476
|
};
|
|
356
477
|
"pane:send": { payload: { paneId: string; text: string; callerPane?: string }; data: PaneSendResult };
|
|
357
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 };
|
|
358
533
|
}
|
|
359
534
|
|
|
360
535
|
export type CommandName = keyof Commands;
|
|
@@ -400,4 +575,44 @@ export const COMMAND_NAMES: readonly CommandName[] = [
|
|
|
400
575
|
"pane:spawn",
|
|
401
576
|
"pane:send",
|
|
402
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",
|
|
403
618
|
];
|
package/src/index.ts
CHANGED
|
@@ -86,9 +86,12 @@ export { daemonHealth } from "./health.ts";
|
|
|
86
86
|
|
|
87
87
|
export { repoNameForPath } from "./repos.ts";
|
|
88
88
|
|
|
89
|
+
export { decidePlacement, openSmartPane } from "./smart-pane.ts";
|
|
90
|
+
export type { Placement, PlacementOpts, HerdrCall } from "./smart-pane.ts";
|
|
91
|
+
|
|
89
92
|
// ─── Settings (RT-50) ────────────────────────────────────────────────────────
|
|
90
93
|
|
|
91
|
-
export { getSetting, listSettings, explainSetting, expandVariables, SCOPE_ORDER } from "./settings/resolve.ts";
|
|
94
|
+
export { getSetting, listSettings, explainSetting, expandVariables, SCOPE_ORDER, setSettingsWarnSink } from "./settings/resolve.ts";
|
|
92
95
|
export type {
|
|
93
96
|
Scope,
|
|
94
97
|
Provenance,
|