@mattstack/rt-client 0.2.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/README.md +5 -2
- package/dist/client.d.ts +93 -2
- package/dist/commands.d.ts +318 -1
- package/dist/index.d.ts +12 -2
- package/dist/index.js +1385 -9
- package/dist/repos.d.ts +9 -3
- package/dist/settings/exec.d.ts +26 -0
- package/dist/settings/identity.d.ts +80 -0
- package/dist/settings/paths.d.ts +42 -0
- package/dist/settings/registry-defs.d.ts +12 -0
- package/dist/settings/registry-machinery.d.ts +59 -0
- package/dist/settings/resolve.d.ts +141 -0
- package/dist/settings/stores.d.ts +53 -0
- package/dist/settings/write.d.ts +110 -0
- package/dist/transport.d.ts +7 -0
- package/package.json +10 -2
- package/src/client.ts +161 -2
- package/src/commands.ts +155 -1
- package/src/index.ts +62 -1
- package/src/repos.ts +89 -14
- package/src/settings/exec.ts +67 -0
- package/src/settings/identity.ts +218 -0
- package/src/settings/paths.ts +80 -0
- package/src/settings/registry-defs.ts +476 -0
- package/src/settings/registry-machinery.ts +141 -0
- package/src/settings/resolve.ts +608 -0
- package/src/settings/stores.ts +129 -0
- package/src/settings/write.ts +294 -0
- package/src/transport.ts +18 -3
package/src/client.ts
CHANGED
|
@@ -5,7 +5,20 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { rtCommand } from "./transport.ts";
|
|
7
7
|
import type { RtResponse, RtClientOptions } from "./transport.ts";
|
|
8
|
-
import type {
|
|
8
|
+
import type {
|
|
9
|
+
DemandDecl,
|
|
10
|
+
ProjectMRsData,
|
|
11
|
+
DiscussionsData,
|
|
12
|
+
MrByBranchData,
|
|
13
|
+
ForgeSlug,
|
|
14
|
+
ForgeTokenData,
|
|
15
|
+
RunSummary,
|
|
16
|
+
RunDetail,
|
|
17
|
+
WakeMode,
|
|
18
|
+
ChatMember,
|
|
19
|
+
ChatMessage,
|
|
20
|
+
RoomSummary,
|
|
21
|
+
} from "./commands.ts";
|
|
9
22
|
|
|
10
23
|
/**
|
|
11
24
|
* One repo's project open-MR store. A cold repo forces a full paginated sync
|
|
@@ -58,7 +71,7 @@ export function readMrsByBranch(
|
|
|
58
71
|
* side: an untracked repo comes back `ok: false` with the `rt daemon track`
|
|
59
72
|
* command to run, which is the fail-closed shape callers should surface
|
|
60
73
|
* verbatim. Callers keep env-var precedence on their own side; this is the
|
|
61
|
-
* fallback that replaces reading ~/.rt/secrets.json directly.
|
|
74
|
+
* fallback that replaces reading ~/.mattstack/rt/secrets.json directly.
|
|
62
75
|
*/
|
|
63
76
|
export function resolveForgeToken(
|
|
64
77
|
repoName: string,
|
|
@@ -71,3 +84,149 @@ export function resolveForgeToken(
|
|
|
71
84
|
{ sockPath: opts.sockPath, timeoutMs: 10_000 },
|
|
72
85
|
);
|
|
73
86
|
}
|
|
87
|
+
|
|
88
|
+
export function listRuns(
|
|
89
|
+
repo?: string,
|
|
90
|
+
opts: RtClientOptions = {},
|
|
91
|
+
): Promise<RtResponse<{ runs: RunSummary[] }>> {
|
|
92
|
+
const payload: Record<string, unknown> = {};
|
|
93
|
+
if (repo !== undefined) payload.repo = repo;
|
|
94
|
+
return rtCommand<{ runs: RunSummary[] }>("runs:list", payload, { sockPath: opts.sockPath, timeoutMs: 10_000 });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getRun(
|
|
98
|
+
runId: string,
|
|
99
|
+
repo?: string,
|
|
100
|
+
opts: RtClientOptions = {},
|
|
101
|
+
): Promise<RtResponse<RunDetail>> {
|
|
102
|
+
const payload: Record<string, unknown> = { runId };
|
|
103
|
+
if (repo !== undefined) payload.repo = repo;
|
|
104
|
+
return rtCommand<RunDetail>("runs:get", payload, { sockPath: opts.sockPath, timeoutMs: 10_000 });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* SKILLS-54. rt's only write path into run state, so a wedged run can be
|
|
109
|
+
* resolved by a person instead of lying in the data forever. The write happens
|
|
110
|
+
* in the daemon and is attributed there; consumers never touch the run DB.
|
|
111
|
+
*/
|
|
112
|
+
export function abandonRun(
|
|
113
|
+
runId: string,
|
|
114
|
+
repo?: string,
|
|
115
|
+
reason?: string,
|
|
116
|
+
opts: RtClientOptions = {},
|
|
117
|
+
): Promise<RtResponse<{ ok: boolean }>> {
|
|
118
|
+
const payload: Record<string, unknown> = { runId };
|
|
119
|
+
if (repo !== undefined) payload.repo = repo;
|
|
120
|
+
if (reason !== undefined) payload.reason = reason;
|
|
121
|
+
return rtCommand<{ ok: boolean }>("runs:abandon", payload, { sockPath: opts.sockPath, timeoutMs: 10_000 });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ─── Chat (RT-48 Task 6) ──────────────────────────────────────────────────
|
|
125
|
+
// The web viewer's (plan 2's) entire dependency: it reaches the daemon
|
|
126
|
+
// through these wrappers over the unix socket, so no /api/chat/* REST rows
|
|
127
|
+
// ship and needsToken() stays untouched.
|
|
128
|
+
|
|
129
|
+
export function chatJoin(
|
|
130
|
+
a: { room: string; handle: string; wakeOn?: WakeMode; cwd?: string; pane?: string },
|
|
131
|
+
o: RtClientOptions = {},
|
|
132
|
+
): Promise<RtResponse<{ handle: string; memberCount: number; unread: number }>> {
|
|
133
|
+
const payload: Record<string, unknown> = { room: a.room, handle: a.handle };
|
|
134
|
+
if (a.wakeOn !== undefined) payload.wakeOn = a.wakeOn;
|
|
135
|
+
if (a.cwd !== undefined) payload.cwd = a.cwd;
|
|
136
|
+
if (a.pane !== undefined) payload.pane = a.pane;
|
|
137
|
+
return rtCommand<{ handle: string; memberCount: number; unread: number }>("chat:join", payload, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export function chatLeave(
|
|
141
|
+
a: { room: string; handle: string },
|
|
142
|
+
o: RtClientOptions = {},
|
|
143
|
+
): Promise<RtResponse<Record<string, never>>> {
|
|
144
|
+
return rtCommand<Record<string, never>>("chat:leave", { room: a.room, handle: a.handle }, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function chatPost(
|
|
148
|
+
a: { room: string; handle: string; body: string },
|
|
149
|
+
o: RtClientOptions = {},
|
|
150
|
+
): Promise<RtResponse<{ id: number; recipients: string[] }>> {
|
|
151
|
+
return rtCommand<{ id: number; recipients: string[] }>("chat:post", { room: a.room, handle: a.handle, body: a.body }, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export function chatRead(
|
|
155
|
+
a: { handle: string; room?: string; limit?: number; sinceMs?: number },
|
|
156
|
+
o: RtClientOptions = {},
|
|
157
|
+
): Promise<RtResponse<{ rooms: { room: string; messages: ChatMessage[] }[] }>> {
|
|
158
|
+
const payload: Record<string, unknown> = { handle: a.handle };
|
|
159
|
+
if (a.room !== undefined) payload.room = a.room;
|
|
160
|
+
if (a.limit !== undefined) payload.limit = a.limit;
|
|
161
|
+
if (a.sinceMs !== undefined) payload.sinceMs = a.sinceMs;
|
|
162
|
+
return rtCommand<{ rooms: { room: string; messages: ChatMessage[] }[] }>("chat:read", payload, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function chatRooms(
|
|
166
|
+
a: { handle: string },
|
|
167
|
+
o: RtClientOptions = {},
|
|
168
|
+
): Promise<RtResponse<{ rooms: RoomSummary[] }>> {
|
|
169
|
+
return rtCommand<{ rooms: RoomSummary[] }>("chat:rooms", { handle: a.handle }, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function chatWho(
|
|
173
|
+
a: { room: string },
|
|
174
|
+
o: RtClientOptions = {},
|
|
175
|
+
): Promise<RtResponse<{ members: ChatMember[] }>> {
|
|
176
|
+
return rtCommand<{ members: ChatMember[] }>("chat:who", { room: a.room }, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function chatMark(
|
|
180
|
+
a: { handle: string; room?: string },
|
|
181
|
+
o: RtClientOptions = {},
|
|
182
|
+
): Promise<RtResponse<Record<string, never>>> {
|
|
183
|
+
const payload: Record<string, unknown> = { handle: a.handle };
|
|
184
|
+
if (a.room !== undefined) payload.room = a.room;
|
|
185
|
+
return rtCommand<Record<string, never>>("chat:mark", payload, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function chatMessages(
|
|
189
|
+
a: { room: string; before?: number; limit?: number },
|
|
190
|
+
o: RtClientOptions = {},
|
|
191
|
+
): Promise<RtResponse<{ messages: ChatMessage[] }>> {
|
|
192
|
+
const payload: Record<string, unknown> = { room: a.room };
|
|
193
|
+
if (a.before !== undefined) payload.before = a.before;
|
|
194
|
+
if (a.limit !== undefined) payload.limit = a.limit;
|
|
195
|
+
return rtCommand<{ messages: ChatMessage[] }>("chat:messages", payload, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export function chatArm(
|
|
199
|
+
a: { handle: string; room?: string },
|
|
200
|
+
o: RtClientOptions = {},
|
|
201
|
+
): Promise<RtResponse<Record<string, never>>> {
|
|
202
|
+
const payload: Record<string, unknown> = { handle: a.handle };
|
|
203
|
+
if (a.room !== undefined) payload.room = a.room;
|
|
204
|
+
return rtCommand<Record<string, never>>("chat:arm", payload, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function chatTouch(
|
|
208
|
+
a: { handle: string },
|
|
209
|
+
o: RtClientOptions = {},
|
|
210
|
+
): Promise<RtResponse<Record<string, never>>> {
|
|
211
|
+
return rtCommand<Record<string, never>>("chat:touch", { handle: a.handle }, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function chatDisarm(
|
|
215
|
+
a: { handle: string },
|
|
216
|
+
o: RtClientOptions = {},
|
|
217
|
+
): Promise<RtResponse<Record<string, never>>> {
|
|
218
|
+
return rtCommand<Record<string, never>>("chat:disarm", { handle: a.handle }, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function chatUnreadWaking(
|
|
222
|
+
a: { handle: string; room?: string },
|
|
223
|
+
o: RtClientOptions = {},
|
|
224
|
+
): Promise<RtResponse<{ rooms: { room: string; count: number; mentions: number; maxId: number }[] }>> {
|
|
225
|
+
const payload: Record<string, unknown> = { handle: a.handle };
|
|
226
|
+
if (a.room !== undefined) payload.room = a.room;
|
|
227
|
+
return rtCommand<{ rooms: { room: string; count: number; mentions: number; maxId: number }[] }>("chat:unread-waking", payload, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export function eventsHead(o: RtClientOptions = {}): Promise<RtResponse<{ cursor: number }>> {
|
|
231
|
+
return rtCommand<{ cursor: number }>("events:head", {}, { sockPath: o.sockPath, timeoutMs: 10_000 });
|
|
232
|
+
}
|
package/src/commands.ts
CHANGED
|
@@ -52,6 +52,84 @@ export interface ForgeTokenData {
|
|
|
52
52
|
token: string;
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Duplicated shape on purpose (RT-44): rt-client cannot import daemon
|
|
57
|
+
* internals, so this mirrors lib/daemon/events-bus.ts's BusEvent.
|
|
58
|
+
*/
|
|
59
|
+
export interface EventsBusEvent { id: number; topic: string; payload: unknown; emittedAt: number }
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Duplicated shape on purpose, same reasoning as EventsBusEvent above:
|
|
63
|
+
* these mirror lib/state/chat-store.ts's types, which rt-client cannot
|
|
64
|
+
* import (it's outside lib/state/ and outside this package entirely).
|
|
65
|
+
*/
|
|
66
|
+
export type WakeMode = "mention" | "all" | "none";
|
|
67
|
+
|
|
68
|
+
export interface ChatMember {
|
|
69
|
+
room: string;
|
|
70
|
+
handle: string;
|
|
71
|
+
joinedAt: number;
|
|
72
|
+
lastReadId: number;
|
|
73
|
+
wakeOn: WakeMode;
|
|
74
|
+
lastSeenAt?: number;
|
|
75
|
+
armedAt?: number;
|
|
76
|
+
cwd?: string;
|
|
77
|
+
pane?: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface ChatMessage {
|
|
81
|
+
id: number;
|
|
82
|
+
room: string;
|
|
83
|
+
handle: string;
|
|
84
|
+
body: string;
|
|
85
|
+
mentions: string[];
|
|
86
|
+
replyTo?: number;
|
|
87
|
+
postedAt: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface RoomSummary {
|
|
91
|
+
room: string;
|
|
92
|
+
memberCount: number;
|
|
93
|
+
unread: number;
|
|
94
|
+
mentions: number;
|
|
95
|
+
lastPostedAt?: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// SKILLS-53: one judgment, computed once in rt, so the console and the tray
|
|
99
|
+
// never derive two verdicts that can disagree.
|
|
100
|
+
export type Attention = {
|
|
101
|
+
needs: boolean;
|
|
102
|
+
reason: "failed" | "stale" | "stranded" | null;
|
|
103
|
+
evidence: string;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
export interface RunSummary {
|
|
107
|
+
id: string; repo: string; work_type: string; pipeline: string;
|
|
108
|
+
status: string; current_stage: string | null; spawned_by: string | null;
|
|
109
|
+
started_at: number; ended_at: number | null;
|
|
110
|
+
// v2. Null on runs written before schema v2; pack_dirty means the pack tree
|
|
111
|
+
// had uncommitted changes, so the as-run text may exist in no commit.
|
|
112
|
+
pack_commits: string | null; pack_dirty: number;
|
|
113
|
+
attention: Attention;
|
|
114
|
+
/** Max over stage, field, and decision timestamps; falls back to
|
|
115
|
+
`started_at` when the run has produced no events yet. The board orders
|
|
116
|
+
by silence, so this — not `started_at` — is its sort key. */
|
|
117
|
+
last_event_at: number;
|
|
118
|
+
/** Denormalized from the run's `ticket` / `branch` fields so the LIST view
|
|
119
|
+
can render and search them without a detail fetch per row. Null when
|
|
120
|
+
the run has not produced that field yet. */
|
|
121
|
+
ticket: string | null;
|
|
122
|
+
branch: string | null;
|
|
123
|
+
}
|
|
124
|
+
export interface RunStageRow {
|
|
125
|
+
name: string; status: string; attempt: number;
|
|
126
|
+
started_at: number | null; ended_at: number | null;
|
|
127
|
+
reason: string | null; detail_path: string | null;
|
|
128
|
+
}
|
|
129
|
+
export interface RunFieldRow { key: string; value: string; produced_by: string; at: number; }
|
|
130
|
+
export interface RunDecisionRow { contract: string; scope: string; selection: string; decided_by: string; decided_at: number; }
|
|
131
|
+
export interface RunDetail { run: RunSummary; stages: RunStageRow[]; fields: RunFieldRow[]; decisions: RunDecisionRow[]; schemaAhead: boolean; }
|
|
132
|
+
|
|
55
133
|
export interface Commands {
|
|
56
134
|
"project-mrs:read": { payload: { repoName: string; maxAgeMs?: number; demand?: DemandDecl }; data: ProjectMRsData };
|
|
57
135
|
"discussions:read": { payload: { repoName: string; iid: number }; data: DiscussionsData };
|
|
@@ -59,11 +137,67 @@ export interface Commands {
|
|
|
59
137
|
/**
|
|
60
138
|
* The forge token for one tracked repo (MAT-33). Repo-scoped on purpose:
|
|
61
139
|
* rt gates access per repo through repo-tracking.json, and this verb is
|
|
62
|
-
* what lets consumers stop reading ~/.rt/secrets.json directly, which
|
|
140
|
+
* what lets consumers stop reading ~/.mattstack/rt/secrets.json directly, which
|
|
63
141
|
* walked around that grant model entirely. An untracked repo is refused;
|
|
64
142
|
* the caller's env vars keep precedence on the caller's side.
|
|
65
143
|
*/
|
|
66
144
|
"secrets:forge-token": { payload: { repoName: string; forge: ForgeSlug }; data: ForgeTokenData };
|
|
145
|
+
/**
|
|
146
|
+
* A per-`scope` whitelisted subset of secrets, each scope reading its own
|
|
147
|
+
* encrypted domain(s): "extension" (default, so the VS Code extension
|
|
148
|
+
* needs no change) is linearApiKey/gitlabToken from the `rt` domain;
|
|
149
|
+
* "deck" is cfApiToken/cfZoneId from the `deck` domain; "board" is
|
|
150
|
+
* cross-domain — slackToken/slackClientSecret/slackSigningSecret from the
|
|
151
|
+
* `board` domain plus gitlabToken/switchboardToken/switchboardAdminToken
|
|
152
|
+
* from the `rt` domain. `data` is a union of the per-scope shapes, not a
|
|
153
|
+
* merged bag of every key — that makes a caller narrowing on the wrong
|
|
154
|
+
* scope's fields a compile error instead of a silent `undefined`. Every
|
|
155
|
+
* key optional (present only when set). Not a general secrets export —
|
|
156
|
+
* extend a whitelist here, in lockstep with
|
|
157
|
+
* lib/daemon/handlers/secrets.ts and (for "extension")
|
|
158
|
+
* extensions/vscode/rt-context/src/secrets.ts, if a consumer needs another
|
|
159
|
+
* key.
|
|
160
|
+
*
|
|
161
|
+
* `token` is required and checked in the HANDLER (not a transport-layer
|
|
162
|
+
* gate alone), since this verb is reachable over the unauthenticated unix
|
|
163
|
+
* socket too — see lib/daemon/handlers/secrets.ts's doc comment. HTTP
|
|
164
|
+
* callers get it forwarded automatically from their X-RT-Token header;
|
|
165
|
+
* socket callers must read ~/.mattstack/rt/api-token themselves. The gate
|
|
166
|
+
* applies identically to every scope.
|
|
167
|
+
*/
|
|
168
|
+
"secrets:read": {
|
|
169
|
+
payload: { token?: string; scope?: "extension" | "deck" | "board" };
|
|
170
|
+
data:
|
|
171
|
+
| { linearApiKey?: string; gitlabToken?: string }
|
|
172
|
+
| { cfApiToken?: string; cfZoneId?: string }
|
|
173
|
+
| {
|
|
174
|
+
slackToken?: string;
|
|
175
|
+
slackClientSecret?: string;
|
|
176
|
+
slackSigningSecret?: string;
|
|
177
|
+
gitlabToken?: string;
|
|
178
|
+
switchboardToken?: string;
|
|
179
|
+
switchboardAdminToken?: string;
|
|
180
|
+
};
|
|
181
|
+
};
|
|
182
|
+
"events:emit": { payload: { topic: string; payload?: unknown }; data: { id: number } };
|
|
183
|
+
"events:wait": { payload: { pattern: string; after?: number; waitMs?: number }; data: { events: EventsBusEvent[]; cursor: number } };
|
|
184
|
+
"events:list": { payload: { pattern: string; after?: number; limit?: number }; data: { events: EventsBusEvent[]; cursor: number } };
|
|
185
|
+
"events:head": { payload: Record<string, never>; data: { cursor: number } };
|
|
186
|
+
"runs:list": { payload: { repo?: string }; data: { runs: RunSummary[] } };
|
|
187
|
+
"runs:get": { payload: { runId: string; repo?: string }; data: RunDetail };
|
|
188
|
+
"runs:abandon": { payload: { runId: string; repo?: string; reason?: string }; data: { ok: boolean } };
|
|
189
|
+
"chat:join": { payload: { room: string; handle: string; wakeOn?: WakeMode; cwd?: string; pane?: string }; data: { handle: string; memberCount: number; unread: number } };
|
|
190
|
+
"chat:leave": { payload: { room: string; handle: string }; data: Record<string, never> };
|
|
191
|
+
"chat:post": { payload: { room: string; handle: string; body: string }; data: { id: number; recipients: string[] } };
|
|
192
|
+
"chat:read": { payload: { handle: string; room?: string; limit?: number; sinceMs?: number }; data: { rooms: { room: string; messages: ChatMessage[] }[] } };
|
|
193
|
+
"chat:rooms": { payload: { handle: string }; data: { rooms: RoomSummary[] } };
|
|
194
|
+
"chat:who": { payload: { room: string }; data: { members: ChatMember[] } };
|
|
195
|
+
"chat:mark": { payload: { handle: string; room?: string }; data: Record<string, never> };
|
|
196
|
+
"chat:messages": { payload: { room: string; before?: number; limit?: number }; data: { messages: ChatMessage[] } };
|
|
197
|
+
"chat:arm": { payload: { handle: string; room?: string }; data: Record<string, never> };
|
|
198
|
+
"chat:touch": { payload: { handle: string }; data: Record<string, never> };
|
|
199
|
+
"chat:disarm": { payload: { handle: string }; data: Record<string, never> };
|
|
200
|
+
"chat:unread-waking": { payload: { handle: string; room?: string }; data: { rooms: { room: string; count: number; mentions: number; maxId: number }[] } };
|
|
67
201
|
}
|
|
68
202
|
|
|
69
203
|
export type CommandName = keyof Commands;
|
|
@@ -73,4 +207,24 @@ export const COMMAND_NAMES: readonly CommandName[] = [
|
|
|
73
207
|
"discussions:read",
|
|
74
208
|
"mr:by-branch",
|
|
75
209
|
"secrets:forge-token",
|
|
210
|
+
"secrets:read",
|
|
211
|
+
"events:emit",
|
|
212
|
+
"events:wait",
|
|
213
|
+
"events:list",
|
|
214
|
+
"events:head",
|
|
215
|
+
"runs:list",
|
|
216
|
+
"runs:get",
|
|
217
|
+
"runs:abandon",
|
|
218
|
+
"chat:join",
|
|
219
|
+
"chat:leave",
|
|
220
|
+
"chat:post",
|
|
221
|
+
"chat:read",
|
|
222
|
+
"chat:rooms",
|
|
223
|
+
"chat:who",
|
|
224
|
+
"chat:mark",
|
|
225
|
+
"chat:messages",
|
|
226
|
+
"chat:arm",
|
|
227
|
+
"chat:touch",
|
|
228
|
+
"chat:disarm",
|
|
229
|
+
"chat:unread-waking",
|
|
76
230
|
];
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
export { rtCommand, DEFAULT_SOCK } from "./transport.ts";
|
|
2
2
|
export type { RtResponse, RtClientOptions } from "./transport.ts";
|
|
3
3
|
|
|
4
|
-
export {
|
|
4
|
+
export {
|
|
5
|
+
readProjectMRs,
|
|
6
|
+
readDiscussions,
|
|
7
|
+
readMrsByBranch,
|
|
8
|
+
resolveForgeToken,
|
|
9
|
+
listRuns,
|
|
10
|
+
getRun,
|
|
11
|
+
abandonRun,
|
|
12
|
+
chatJoin,
|
|
13
|
+
chatLeave,
|
|
14
|
+
chatPost,
|
|
15
|
+
chatRead,
|
|
16
|
+
chatRooms,
|
|
17
|
+
chatWho,
|
|
18
|
+
chatMark,
|
|
19
|
+
chatMessages,
|
|
20
|
+
chatArm,
|
|
21
|
+
chatTouch,
|
|
22
|
+
chatDisarm,
|
|
23
|
+
chatUnreadWaking,
|
|
24
|
+
eventsHead,
|
|
25
|
+
} from "./client.ts";
|
|
5
26
|
|
|
6
27
|
export { COMMAND_NAMES } from "./commands.ts";
|
|
7
28
|
export type {
|
|
@@ -16,9 +37,49 @@ export type {
|
|
|
16
37
|
CommandName,
|
|
17
38
|
ForgeSlug,
|
|
18
39
|
ForgeTokenData,
|
|
40
|
+
Attention,
|
|
41
|
+
RunSummary,
|
|
42
|
+
RunStageRow,
|
|
43
|
+
RunFieldRow,
|
|
44
|
+
RunDecisionRow,
|
|
45
|
+
RunDetail,
|
|
46
|
+
WakeMode,
|
|
47
|
+
ChatMember,
|
|
48
|
+
ChatMessage,
|
|
49
|
+
RoomSummary,
|
|
19
50
|
} from "./commands.ts";
|
|
20
51
|
|
|
21
52
|
export { subscribe, DEFAULT_WS_URL } from "./relay.ts";
|
|
22
53
|
export type { RelayEventType } from "./relay.ts";
|
|
23
54
|
|
|
24
55
|
export { repoNameForPath } from "./repos.ts";
|
|
56
|
+
|
|
57
|
+
// ─── Settings (RT-50) ────────────────────────────────────────────────────────
|
|
58
|
+
|
|
59
|
+
export { getSetting, listSettings, explainSetting, expandVariables, SCOPE_ORDER } from "./settings/resolve.ts";
|
|
60
|
+
export type {
|
|
61
|
+
Scope,
|
|
62
|
+
Provenance,
|
|
63
|
+
ResolveOpts,
|
|
64
|
+
Resolved,
|
|
65
|
+
InvalidScope,
|
|
66
|
+
ListedSetting,
|
|
67
|
+
ExplainRow,
|
|
68
|
+
ExpandCtx,
|
|
69
|
+
} from "./settings/resolve.ts";
|
|
70
|
+
|
|
71
|
+
export { setSetting } from "./settings/write.ts";
|
|
72
|
+
export type { SetSettingOpts } from "./settings/write.ts";
|
|
73
|
+
|
|
74
|
+
export { getDef, allDefs, validateValue, isMigrated } from "./settings/registry-machinery.ts";
|
|
75
|
+
export type { SettingDef, SettingScope } from "./settings/registry-machinery.ts";
|
|
76
|
+
export { REGISTRY } from "./settings/registry-defs.ts";
|
|
77
|
+
|
|
78
|
+
export { readStore, listTeams } from "./settings/stores.ts";
|
|
79
|
+
export type { StoreFile } from "./settings/stores.ts";
|
|
80
|
+
|
|
81
|
+
export {
|
|
82
|
+
normalizeRemote, identityFromRemote, deriveRepoIdentity, clearIdentityMemo,
|
|
83
|
+
serializeIdentity, parseIdentity, resolveNameToIdentity,
|
|
84
|
+
type RepoIdentity,
|
|
85
|
+
} from "./settings/identity.ts";
|
package/src/repos.ts
CHANGED
|
@@ -1,28 +1,83 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Repo-name resolution against rt's global index
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* Repo-name resolution against rt's global index. rt itself resolves this
|
|
3
|
+
* through state.db (~/.mattstack/rt/state.db); this module runs OUT OF
|
|
4
|
+
* PROCESS (gitq, mr-board, deck), so it has no handle on that db and reads
|
|
5
|
+
* a derived snapshot instead — state.db's kv `repo-index` namespace when
|
|
6
|
+
* reachable, falling back to the legacy `repos.json` mirror
|
|
7
|
+
* (~/.mattstack/rt/repos.json, a flat `{ "<repoName>": "<absolute path>" }`
|
|
8
|
+
* map) rt keeps in sync for exactly this purpose. See rt's
|
|
9
|
+
* lib/repo-index.ts `repoIndexCompatPath` for the write side.
|
|
6
10
|
*/
|
|
7
11
|
import { existsSync, readFileSync } from "fs";
|
|
8
12
|
import { homedir } from "os";
|
|
9
|
-
import { join } from "path";
|
|
13
|
+
import { dirname, join } from "path";
|
|
10
14
|
|
|
11
15
|
function defaultReposJsonPath(): string {
|
|
12
|
-
|
|
16
|
+
// Duplicates the ~/.mattstack/rt layout: rt-client has no dependency on rt's
|
|
17
|
+
// lib/, so this literal cannot import rtDir(). repo-tools/lib/rt-paths.ts is
|
|
18
|
+
// the authority — change there first, mirror here.
|
|
19
|
+
return join(homedir(), ".mattstack", "rt", "repos.json");
|
|
13
20
|
}
|
|
14
21
|
|
|
22
|
+
interface RepoIndexRow {
|
|
23
|
+
k: string;
|
|
24
|
+
v: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface BunSqliteDatabase {
|
|
28
|
+
query(sql: string): { all(...params: unknown[]): unknown[] };
|
|
29
|
+
close(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type BunSqliteDatabaseCtor = new (path: string, opts?: { readonly?: boolean }) => BunSqliteDatabase;
|
|
33
|
+
|
|
15
34
|
/**
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* whatever it had before (an unqualified path, a prompt, etc).
|
|
35
|
+
* Loads bun:sqlite defensively: only Bun's runtime provides this built-in.
|
|
36
|
+
* A non-Bun consumer throws resolving the specifier, caught here so the
|
|
37
|
+
* caller degrades to the repos.json path instead of throwing.
|
|
20
38
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
39
|
+
function loadBunSqliteDatabase(): BunSqliteDatabaseCtor | null {
|
|
40
|
+
try {
|
|
41
|
+
return (require("bun:sqlite") as { Database: BunSqliteDatabaseCtor }).Database;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Exact-match lookup against state.db's `repo-index` kv namespace (ns=k=v
|
|
49
|
+
* rows written by rt's setKvValue, so `v` is a JSON-encoded string). Returns
|
|
50
|
+
* null (never throws) on any failure — missing db, unreadable, wrong
|
|
51
|
+
* schema, no match — so the caller always has repos.json as a fallback.
|
|
52
|
+
*/
|
|
53
|
+
function repoNameFromStateDb(repoPath: string, dbPath: string): string | null {
|
|
54
|
+
if (!existsSync(dbPath)) return null;
|
|
55
|
+
const DatabaseCtor = loadBunSqliteDatabase();
|
|
56
|
+
if (!DatabaseCtor) return null;
|
|
57
|
+
try {
|
|
58
|
+
const db = new DatabaseCtor(dbPath, { readonly: true });
|
|
59
|
+
try {
|
|
60
|
+
const rows = db.query("SELECT k, v FROM kv WHERE ns = 'repo-index';").all() as RepoIndexRow[];
|
|
61
|
+
for (const row of rows) {
|
|
62
|
+
try {
|
|
63
|
+
if (JSON.parse(row.v) === repoPath) return row.k;
|
|
64
|
+
} catch {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
} finally {
|
|
70
|
+
db.close();
|
|
71
|
+
}
|
|
72
|
+
} catch {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function repoNameFromJson(repoPath: string, reposJsonPath: string): string | null {
|
|
23
78
|
try {
|
|
24
|
-
if (!existsSync(
|
|
25
|
-
const raw = readFileSync(
|
|
79
|
+
if (!existsSync(reposJsonPath)) return null;
|
|
80
|
+
const raw = readFileSync(reposJsonPath, "utf8");
|
|
26
81
|
const index = JSON.parse(raw) as Record<string, unknown>;
|
|
27
82
|
for (const [repoName, value] of Object.entries(index)) {
|
|
28
83
|
if (value === repoPath) return repoName;
|
|
@@ -32,3 +87,23 @@ export function repoNameForPath(repoPath: string, reposJsonPath?: string): strin
|
|
|
32
87
|
return null;
|
|
33
88
|
}
|
|
34
89
|
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Exact-match lookup: returns the repo name whose recorded path equals
|
|
93
|
+
* `repoPath`, or null if no source has a match. Never throws -- a
|
|
94
|
+
* resolution failure just means the caller falls back to whatever it had
|
|
95
|
+
* before (an unqualified path, a prompt, etc).
|
|
96
|
+
*
|
|
97
|
+
* Prefers state.db (authoritative, kept live by every rt process); falls
|
|
98
|
+
* back to the repos.json compat mirror when state.db is unreachable — a
|
|
99
|
+
* pre-upgrade rt install, a non-Bun consumer, or a state.db this process
|
|
100
|
+
* can't open. `reposJsonPath`, when passed, also relocates the state.db
|
|
101
|
+
* lookup: both files live side by side under the same rt data directory.
|
|
102
|
+
*/
|
|
103
|
+
export function repoNameForPath(repoPath: string, reposJsonPath?: string): string | null {
|
|
104
|
+
const jsonPath = reposJsonPath ?? defaultReposJsonPath();
|
|
105
|
+
const dbPath = join(dirname(jsonPath), "state.db");
|
|
106
|
+
const fromDb = repoNameFromStateDb(repoPath, dbPath);
|
|
107
|
+
if (fromDb !== null) return fromDb;
|
|
108
|
+
return repoNameFromJson(repoPath, jsonPath);
|
|
109
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Async subprocess capture, duplicated from repo-tools/lib/subprocess.ts:
|
|
3
|
+
* rt-client has no dependency on rt's lib/, so this can't import runCapture
|
|
4
|
+
* from there. lib/subprocess.ts is the authority — change there first,
|
|
5
|
+
* mirror here.
|
|
6
|
+
*
|
|
7
|
+
* execSync blocks the event loop for the entire child lifetime; identity
|
|
8
|
+
* derivation must stay safe to call from daemon contexts, hence this instead.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface RunResult {
|
|
12
|
+
stdout: string;
|
|
13
|
+
stderr: string;
|
|
14
|
+
exitCode: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Run argv and capture stdout. Never throws: spawn failures and timeouts
|
|
19
|
+
* surface as a non-zero exitCode with whatever stdout was collected.
|
|
20
|
+
*
|
|
21
|
+
* Children inherit the caller's live `process.env` unless `opts.env` overrides it.
|
|
22
|
+
*/
|
|
23
|
+
export async function runCapture(
|
|
24
|
+
argv: [string, ...string[]],
|
|
25
|
+
opts: {
|
|
26
|
+
cwd?: string;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
stderr?: "ignore" | "pipe";
|
|
29
|
+
env?: Record<string, string | undefined>;
|
|
30
|
+
} = {},
|
|
31
|
+
): Promise<RunResult> {
|
|
32
|
+
const captureStderr = opts.stderr === "pipe";
|
|
33
|
+
let proc: ReturnType<typeof Bun.spawn>;
|
|
34
|
+
try {
|
|
35
|
+
proc = Bun.spawn(argv, {
|
|
36
|
+
cwd: opts.cwd,
|
|
37
|
+
// Bun.spawn ignores assignments made to process.env after startup, so an
|
|
38
|
+
// inherited env strands a PATH resolved at boot and leaves
|
|
39
|
+
// `#!/usr/bin/env node` shebangs unresolvable under launchd. execSync,
|
|
40
|
+
// which this replaces, reads process.env per call.
|
|
41
|
+
env: opts.env ?? { ...process.env },
|
|
42
|
+
stdin: "ignore",
|
|
43
|
+
stdout: "pipe",
|
|
44
|
+
stderr: captureStderr ? "pipe" : "ignore",
|
|
45
|
+
});
|
|
46
|
+
} catch {
|
|
47
|
+
return { stdout: "", stderr: "", exitCode: -1 };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const timer = setTimeout(() => {
|
|
51
|
+
try { proc.kill(); } catch { /* already exited */ }
|
|
52
|
+
}, opts.timeoutMs ?? 10_000);
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const stdoutPromise = new Response(proc.stdout as ReadableStream).text();
|
|
56
|
+
const stderrPromise = captureStderr
|
|
57
|
+
? new Response(proc.stderr as ReadableStream).text()
|
|
58
|
+
: Promise.resolve("");
|
|
59
|
+
const [stdout, stderr] = await Promise.all([stdoutPromise, stderrPromise]);
|
|
60
|
+
const exitCode = await proc.exited;
|
|
61
|
+
return { stdout, stderr, exitCode };
|
|
62
|
+
} catch {
|
|
63
|
+
return { stdout: "", stderr: "", exitCode: -1 };
|
|
64
|
+
} finally {
|
|
65
|
+
clearTimeout(timer);
|
|
66
|
+
}
|
|
67
|
+
}
|