@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
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings key TABLE (RT-47/RT-50): rt's rows, plus the suite rows
|
|
3
|
+
* (deck/board/gitq/mattstack/claude) — the machinery in registry-machinery.ts
|
|
4
|
+
* that reads and validates against this table does not change when rows are
|
|
5
|
+
* added.
|
|
6
|
+
*
|
|
7
|
+
* Suite rows omit `migrated` (see registry-machinery.ts's docblock): they
|
|
8
|
+
* carry no rt-legacy file to migrate from, so `isMigrated()` treats the
|
|
9
|
+
* absent flag as resolver-backed from day one.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { SettingDef, SettingScope } from "./registry-machinery.ts";
|
|
13
|
+
|
|
14
|
+
const ALL_SCOPES: SettingScope[] = ["user", "team", "machine"];
|
|
15
|
+
|
|
16
|
+
export const REGISTRY: readonly SettingDef[] = [
|
|
17
|
+
// --- migrated:true (wave 1) ---------------------------------------------
|
|
18
|
+
{
|
|
19
|
+
key: "rt.roles",
|
|
20
|
+
type: "object",
|
|
21
|
+
scopes: ALL_SCOPES,
|
|
22
|
+
merge: "deep",
|
|
23
|
+
repoScoped: true,
|
|
24
|
+
migrated: true,
|
|
25
|
+
pathGuardFields: ["hook"],
|
|
26
|
+
description: "Per-repo dev-role definitions: port pools, env passthrough, and the dev-server hook command.",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "rt.intercepts",
|
|
30
|
+
type: "array",
|
|
31
|
+
scopes: ALL_SCOPES,
|
|
32
|
+
merge: "replace",
|
|
33
|
+
repoScoped: true,
|
|
34
|
+
migrated: true,
|
|
35
|
+
description: "Per-repo endpoint intercept rules consumed by rt intercept install.",
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "rt.worktrees",
|
|
39
|
+
type: "object",
|
|
40
|
+
scopes: ALL_SCOPES,
|
|
41
|
+
default: { onDeck: 0 },
|
|
42
|
+
merge: "deep",
|
|
43
|
+
repoScoped: true,
|
|
44
|
+
migrated: true,
|
|
45
|
+
description: "Per-repo worktree pool config (onDeck size, ready steps, name pool); root/branchFormat/ready computed-or-empty in the reader.",
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
key: "rt.repoIdentityOverrides",
|
|
49
|
+
type: "object",
|
|
50
|
+
scopes: ["machine"],
|
|
51
|
+
merge: "replace",
|
|
52
|
+
migrated: true,
|
|
53
|
+
description: "Map of observed remote URL to pinned repo identity, for forks/multi-remote repos on this machine.",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
key: "rt.repoRoots",
|
|
57
|
+
type: "array",
|
|
58
|
+
scopes: ["machine"],
|
|
59
|
+
default: [],
|
|
60
|
+
merge: "replace",
|
|
61
|
+
migrated: true,
|
|
62
|
+
description:
|
|
63
|
+
'Directories rt scans for git repos (rt cd, run-outside-a-repo pickers). Entries may start with "~/" or use "${home}". One level deep, plus worktree-pool parent folders one level deeper.',
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
key: "rt.notifications",
|
|
67
|
+
type: "object",
|
|
68
|
+
scopes: ["user"],
|
|
69
|
+
merge: "deep",
|
|
70
|
+
migrated: true,
|
|
71
|
+
description: "Desktop notification preferences (which events notify, sound on/off).",
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
key: "rt.cron",
|
|
75
|
+
type: "object",
|
|
76
|
+
scopes: ["machine"],
|
|
77
|
+
merge: "deep",
|
|
78
|
+
migrated: true,
|
|
79
|
+
description: "Scheduled rt job definitions and their cron expressions. Restart the daemon to apply changes.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
key: "rt.repoTracking",
|
|
83
|
+
type: "object",
|
|
84
|
+
scopes: ["machine"],
|
|
85
|
+
merge: "deep",
|
|
86
|
+
migrated: true,
|
|
87
|
+
description: "Which repos rt tracks for background sync and status polling.",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
key: "rt.runsPruneDays",
|
|
91
|
+
type: "number",
|
|
92
|
+
scopes: ["machine"],
|
|
93
|
+
default: 30,
|
|
94
|
+
merge: "replace",
|
|
95
|
+
migrated: true,
|
|
96
|
+
description: "Age floor in days for pruning finished pipeline run directories under ~/.mattstack/runs (default 30).",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
key: "rt.runaway",
|
|
100
|
+
type: "object",
|
|
101
|
+
scopes: ["machine"],
|
|
102
|
+
merge: "deep",
|
|
103
|
+
migrated: true,
|
|
104
|
+
description: "Thresholds for the runaway-process guard that kills stuck dev servers. Restart the daemon to apply changes.",
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
key: "rt.workspacePrefs",
|
|
108
|
+
type: "object",
|
|
109
|
+
scopes: ["machine"],
|
|
110
|
+
merge: "deep",
|
|
111
|
+
migrated: true,
|
|
112
|
+
description: "Per-machine editor/terminal preferences applied when opening a worktree.",
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
key: "rt.homeSnapshot",
|
|
116
|
+
type: "object",
|
|
117
|
+
scopes: ["machine"],
|
|
118
|
+
default: { enabled: true, debounceSec: 20, pushDelaySec: 60, janitorThresholdHours: 6, janitorIntervalMin: 30 },
|
|
119
|
+
merge: "deep",
|
|
120
|
+
migrated: true,
|
|
121
|
+
description: "Home-repo snapshot daemon config: enabled, debounce/push delays, and the janitor threshold/interval for zones left dirty too long.",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
key: "rt.sync",
|
|
125
|
+
type: "object",
|
|
126
|
+
scopes: ALL_SCOPES,
|
|
127
|
+
merge: "deep",
|
|
128
|
+
repoScoped: true,
|
|
129
|
+
migrated: true,
|
|
130
|
+
description: "Branch sync behavior: fast-forward rules and stale-branch handling.",
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "rt.branchNaming",
|
|
134
|
+
type: "object",
|
|
135
|
+
scopes: ALL_SCOPES,
|
|
136
|
+
merge: "deep",
|
|
137
|
+
repoScoped: true,
|
|
138
|
+
migrated: true,
|
|
139
|
+
description: "Branch-naming templates, repoScoped. Read by the VS Code extension (extensions/vscode/rt-context), which lazily imports the legacy repos/<repo>/branch-naming.json into this key on first read; rt itself has no CLI-side reader.",
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
key: "rt.variations",
|
|
143
|
+
type: "object",
|
|
144
|
+
scopes: ALL_SCOPES,
|
|
145
|
+
merge: "deep",
|
|
146
|
+
repoScoped: true,
|
|
147
|
+
migrated: true,
|
|
148
|
+
description: "Named parameter sets rt run can pick between for a command.",
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
key: "rt.presets",
|
|
152
|
+
type: "object",
|
|
153
|
+
scopes: ALL_SCOPES,
|
|
154
|
+
merge: "deep",
|
|
155
|
+
repoScoped: true,
|
|
156
|
+
migrated: true,
|
|
157
|
+
description: "Saved argument presets for frequently repeated rt commands, keyed by name.",
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
key: "rt.dopplerTemplate",
|
|
161
|
+
type: "array",
|
|
162
|
+
scopes: ALL_SCOPES,
|
|
163
|
+
merge: "replace",
|
|
164
|
+
repoScoped: true,
|
|
165
|
+
migrated: true,
|
|
166
|
+
description: "Template used to generate a repo's Doppler secrets config.",
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
// --- wave 2 (ownership-latch ports, RT-53) ------------------------------
|
|
170
|
+
// These rows carry NO `default`: the ownership latch is `getSetting(key)
|
|
171
|
+
// .value === undefined`, and a registry default materializes as a present
|
|
172
|
+
// value — adding one flips the key store-authoritative on every install
|
|
173
|
+
// (same invariant as the board.* block above).
|
|
174
|
+
{
|
|
175
|
+
key: "rt.worktreeApp",
|
|
176
|
+
type: "object",
|
|
177
|
+
scopes: ["machine"],
|
|
178
|
+
merge: "deep",
|
|
179
|
+
migrated: true,
|
|
180
|
+
description: "Machine-local worktree feature toggle (enabled, killProcesses); ownership-latch port of ~/.mattstack/rt/worktrees.json, store wins per field. A distinct key from rt.worktrees (the per-repo pool config above) on purpose — same file family, unrelated shape and scope.",
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
key: "rt.sdmEnrichment",
|
|
184
|
+
type: "object",
|
|
185
|
+
scopes: ["team"],
|
|
186
|
+
merge: "replace",
|
|
187
|
+
migrated: true,
|
|
188
|
+
description: "Team-declared StrongDM resource enrichment (resource name -> label/tier/db/reasonSuggestion); team-ONLY by design, enrichment names employer resources and must never be settable in a user or machine store. Ownership-latch port of ~/.mattstack/rt/sdm/enrichment.jsonc, store wins wholesale (a name-keyed map, not a field-bag).",
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
key: "rt.logRetentionDays",
|
|
192
|
+
type: "number",
|
|
193
|
+
scopes: ["machine", "user"],
|
|
194
|
+
default: 14,
|
|
195
|
+
merge: "replace",
|
|
196
|
+
migrated: true,
|
|
197
|
+
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.",
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
key: "rt.hooks",
|
|
201
|
+
type: "object",
|
|
202
|
+
scopes: ALL_SCOPES,
|
|
203
|
+
merge: "deep",
|
|
204
|
+
repoScoped: true,
|
|
205
|
+
migrated: true,
|
|
206
|
+
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.",
|
|
207
|
+
},
|
|
208
|
+
|
|
209
|
+
// --- mattstack (installer-lane) -----------------------------------------
|
|
210
|
+
{
|
|
211
|
+
key: "mattstack.integrations",
|
|
212
|
+
type: "object",
|
|
213
|
+
scopes: ["team"],
|
|
214
|
+
merge: "deep",
|
|
215
|
+
description: "Team-wide external integration config (forge/slack/linear/switchboard) the installer provisions; client secrets never live here.",
|
|
216
|
+
},
|
|
217
|
+
{
|
|
218
|
+
key: "mattstack.tracking",
|
|
219
|
+
type: "object",
|
|
220
|
+
scopes: ["team"],
|
|
221
|
+
merge: "deep",
|
|
222
|
+
description: "Team-declared repo tracking intent, identity-keyed; the daemon layers it under machine-scoped rt.repoTracking, which wins per repo whenever it names that repo at all — including an explicit local {mode:\"off\"} entry, the way to opt a repo out of team-declared tracking.",
|
|
223
|
+
},
|
|
224
|
+
{
|
|
225
|
+
key: "mattstack.appPath",
|
|
226
|
+
type: "string",
|
|
227
|
+
scopes: ["machine"],
|
|
228
|
+
merge: "replace",
|
|
229
|
+
description: "Absolute path to the installed mattstack.app bundle, written by the app at launch so rt stops hardcoding ~/Applications.",
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
key: "rt.integrations",
|
|
233
|
+
type: "object",
|
|
234
|
+
scopes: ["user"],
|
|
235
|
+
merge: "deep",
|
|
236
|
+
migrated: true,
|
|
237
|
+
description:
|
|
238
|
+
"User-confirmed integration hosts (forgeHost, switchboardUrl), written only by an explicit `rt setup <id> connect --host` after that host validates a real credential. The one trusted source a credential is ever sent to — mattstack.integrations' team-declared host is shown to the user but never auto-used for a fetch.",
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// --- claude (installer-lane) --------------------------------------------
|
|
242
|
+
{
|
|
243
|
+
key: "claude.marketplaces",
|
|
244
|
+
type: "array",
|
|
245
|
+
scopes: ["user", "team"],
|
|
246
|
+
merge: "replace",
|
|
247
|
+
description: "Claude Code plugin marketplaces to replay on restore, in add order.",
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
key: "claude.plugins",
|
|
251
|
+
type: "array",
|
|
252
|
+
scopes: ["user", "team"],
|
|
253
|
+
merge: "replace",
|
|
254
|
+
description: "Claude Code plugins to replay on restore, in install order.",
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
// --- deck (MAT-384 settings half) ---------------------------------------
|
|
258
|
+
{
|
|
259
|
+
key: "deck.apps",
|
|
260
|
+
type: "object",
|
|
261
|
+
scopes: ["user"],
|
|
262
|
+
merge: "deep",
|
|
263
|
+
description: "Per-app deck publish state (published flag, publicFollowsOverride); password hashes and session secrets stay out of this store.",
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
key: "deck.access",
|
|
267
|
+
type: "object",
|
|
268
|
+
scopes: ["user"],
|
|
269
|
+
merge: "deep",
|
|
270
|
+
description: "deck's access-control roster, migrated from access.json.",
|
|
271
|
+
},
|
|
272
|
+
{
|
|
273
|
+
key: "deck.platform",
|
|
274
|
+
type: "object",
|
|
275
|
+
scopes: ["machine"],
|
|
276
|
+
merge: "deep",
|
|
277
|
+
description: "deck's platform-level machine config: public domain and legacy URL prefixes; Cloudflare secrets stay out of this store.",
|
|
278
|
+
},
|
|
279
|
+
|
|
280
|
+
// --- board (team) --------------------------------------------------------
|
|
281
|
+
// board.* rows carry NO `default`: the board's store-ownership latch is
|
|
282
|
+
// `getSetting(key).value === undefined`, and a registry default materializes
|
|
283
|
+
// as a present value — adding one flips that key store-authoritative on
|
|
284
|
+
// every install and blanks the file's value.
|
|
285
|
+
{
|
|
286
|
+
key: "board.gitlabHost",
|
|
287
|
+
type: "string",
|
|
288
|
+
scopes: ["team"],
|
|
289
|
+
merge: "replace",
|
|
290
|
+
description: "GitLab host the board polls for MRs, shared by the whole team.",
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
key: "board.projects",
|
|
294
|
+
type: "array",
|
|
295
|
+
scopes: ["team"],
|
|
296
|
+
merge: "replace",
|
|
297
|
+
description: "GitLab projects the board tracks, shared by the whole team.",
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
key: "board.members",
|
|
301
|
+
type: "array",
|
|
302
|
+
scopes: ["team"],
|
|
303
|
+
merge: "replace",
|
|
304
|
+
description: "The board's full member roster, including hidden-by-default entries.",
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
key: "board.title",
|
|
308
|
+
type: "string",
|
|
309
|
+
scopes: ["team"],
|
|
310
|
+
merge: "replace",
|
|
311
|
+
description: "Display title shown in the board's UI.",
|
|
312
|
+
},
|
|
313
|
+
{
|
|
314
|
+
key: "board.botUsernames",
|
|
315
|
+
type: "array",
|
|
316
|
+
scopes: ["team"],
|
|
317
|
+
merge: "replace",
|
|
318
|
+
description: "GitLab usernames the board treats as bots, excluded from human MR attribution.",
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
key: "board.ticketPrefixes",
|
|
322
|
+
type: "array",
|
|
323
|
+
scopes: ["team"],
|
|
324
|
+
merge: "replace",
|
|
325
|
+
description: "Ticket key prefixes (e.g. RT, MAT) the board links out to Linear from an MR title.",
|
|
326
|
+
},
|
|
327
|
+
{
|
|
328
|
+
key: "board.slack",
|
|
329
|
+
type: "object",
|
|
330
|
+
scopes: ["team"],
|
|
331
|
+
merge: "deep",
|
|
332
|
+
description: "The board's Slack posting config (app id, client id, channel, callback port); client secrets stay out of this store.",
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
key: "board.doctorSkill",
|
|
336
|
+
type: "string",
|
|
337
|
+
scopes: ["team"],
|
|
338
|
+
merge: "replace",
|
|
339
|
+
description: "Default doctor skill for repairing a stuck MR; a repo's skills.jsonc doctor slot overrides it when present.",
|
|
340
|
+
},
|
|
341
|
+
{
|
|
342
|
+
key: "board.triage.doctorSkill",
|
|
343
|
+
type: "string",
|
|
344
|
+
scopes: ["team"],
|
|
345
|
+
merge: "replace",
|
|
346
|
+
description: "Doctor skill the board's own API-tier triage sweep runs on your MRs; deliberately never resolved through a repo's skills.jsonc manifest. A sibling flat key of board.triage, not a field inside it — the board reader assembles the two independently.",
|
|
347
|
+
},
|
|
348
|
+
|
|
349
|
+
// --- board (user) ----------------------------------------------------------
|
|
350
|
+
{
|
|
351
|
+
key: "board.staleAfterDays",
|
|
352
|
+
type: "number",
|
|
353
|
+
scopes: ["user"],
|
|
354
|
+
merge: "replace",
|
|
355
|
+
description: "Days of MR inactivity before the board flags it stale, for this developer.",
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
key: "board.workspaces",
|
|
359
|
+
type: "object",
|
|
360
|
+
scopes: ["user"],
|
|
361
|
+
merge: "deep",
|
|
362
|
+
description: "Herdr workspace names the board's review/respond/doctor panes launch into, per developer.",
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
key: "board.defaultMember",
|
|
366
|
+
type: "string",
|
|
367
|
+
scopes: ["user"],
|
|
368
|
+
merge: "replace",
|
|
369
|
+
description: "Which board member identity this developer's local board runs as by default.",
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
key: "board.hiddenMembers",
|
|
373
|
+
type: "array",
|
|
374
|
+
scopes: ["user"],
|
|
375
|
+
merge: "replace",
|
|
376
|
+
description: "Usernames this developer hides from the team roster's board.members list; overlays the team truth without editing it.",
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
key: "board.triage",
|
|
380
|
+
type: "object",
|
|
381
|
+
scopes: ["user"],
|
|
382
|
+
merge: "deep",
|
|
383
|
+
description: "This developer's triage user-intent flags (which triage sweeps run automatically); a sibling flat key of board.triage.doctorSkill, not its container — the board reader assembles the two independently.",
|
|
384
|
+
},
|
|
385
|
+
|
|
386
|
+
// --- board (machine) ---------------------------------------------------
|
|
387
|
+
{
|
|
388
|
+
key: "board.claudeCommand",
|
|
389
|
+
type: "string",
|
|
390
|
+
scopes: ["machine"],
|
|
391
|
+
merge: "replace",
|
|
392
|
+
description: "Local command used to launch Claude Code for the board's review/respond/doctor panes.",
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
key: "board.cwds",
|
|
396
|
+
type: "object",
|
|
397
|
+
scopes: ["machine"],
|
|
398
|
+
merge: "deep",
|
|
399
|
+
description: "Local working directories the board's review/respond/doctor panes launch from.",
|
|
400
|
+
},
|
|
401
|
+
{
|
|
402
|
+
key: "board.rtRepos",
|
|
403
|
+
type: "array",
|
|
404
|
+
scopes: ["machine"],
|
|
405
|
+
merge: "replace",
|
|
406
|
+
description: "rt-registered repo names the board resolves MRs against on this machine.",
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
key: "board.triageMaxConcurrent",
|
|
410
|
+
type: "number",
|
|
411
|
+
scopes: ["machine"],
|
|
412
|
+
merge: "replace",
|
|
413
|
+
description: "Max concurrent triage panes the board launches on this machine.",
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
key: "board.switchboardUrl",
|
|
417
|
+
type: "string",
|
|
418
|
+
scopes: ["machine"],
|
|
419
|
+
merge: "replace",
|
|
420
|
+
description: "Local switchboard URL the board's POST /peer/join writer targets.",
|
|
421
|
+
},
|
|
422
|
+
|
|
423
|
+
// --- gitq ------------------------------------------------------------------
|
|
424
|
+
{
|
|
425
|
+
key: "gitq.workSlots",
|
|
426
|
+
type: "object",
|
|
427
|
+
scopes: ["machine"],
|
|
428
|
+
merge: "deep",
|
|
429
|
+
description: "gitq's local work-slot config: on-disk location and the max slot count.",
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
key: "gitq.forges",
|
|
433
|
+
type: "object",
|
|
434
|
+
scopes: ["user"],
|
|
435
|
+
merge: "deep",
|
|
436
|
+
description: "gitq's host-keyed forge config, tokenEnv names only — never a live token.",
|
|
437
|
+
},
|
|
438
|
+
{
|
|
439
|
+
key: "gitq.board",
|
|
440
|
+
type: "object",
|
|
441
|
+
scopes: ["machine"],
|
|
442
|
+
merge: "deep",
|
|
443
|
+
description: "gitq checkout-board config: tracked repos, local port, and the herdr workspace it launches into.",
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
// --- chat (RT-48 Task 7) -------------------------------------------------
|
|
447
|
+
{
|
|
448
|
+
key: "chat.handle",
|
|
449
|
+
type: "string",
|
|
450
|
+
scopes: ["user"],
|
|
451
|
+
merge: "replace",
|
|
452
|
+
description: "Explicit rt chat handle for this developer on this machine; overrides the derived <repo>-<dir> handle when set.",
|
|
453
|
+
},
|
|
454
|
+
{
|
|
455
|
+
key: "chat.humanHandle",
|
|
456
|
+
type: "string",
|
|
457
|
+
scopes: ["user"],
|
|
458
|
+
default: "matt",
|
|
459
|
+
merge: "replace",
|
|
460
|
+
description: "The human's own chat handle, so agents can @-mention them by name.",
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
key: "chat.push.provider",
|
|
464
|
+
type: "string",
|
|
465
|
+
scopes: ["user"],
|
|
466
|
+
merge: "replace",
|
|
467
|
+
description: "Push notification provider used to alert the human of chat mentions when away from a terminal.",
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
key: "chat.push.target",
|
|
471
|
+
type: "string",
|
|
472
|
+
scopes: ["user"],
|
|
473
|
+
merge: "replace",
|
|
474
|
+
description: "Destination (topic/URL/token) the configured chat.push.provider sends to.",
|
|
475
|
+
},
|
|
476
|
+
];
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings schema registry machinery (RT-47/RT-50): lookup and
|
|
3
|
+
* validation over a static table describing every known settings key.
|
|
4
|
+
*
|
|
5
|
+
* This module is pure data plumbing — no file IO, no daemon dependency, safe
|
|
6
|
+
* to import anywhere (including the daemon thread). The def TABLE itself
|
|
7
|
+
* lives in registry-defs.ts (rt's rows today; a later task adds the rest of
|
|
8
|
+
* the suite); this file only knows how to look a def up and check a value
|
|
9
|
+
* against it.
|
|
10
|
+
*
|
|
11
|
+
* `migrated: true` means the reader for this key goes through the resolver.
|
|
12
|
+
* `migrated: false` keys still appear in `rt settings list` (so the full
|
|
13
|
+
* settings map is visible even before a key's reader has been ported), but
|
|
14
|
+
* `set` on them refuses — see the spec's "Schema registry" section for why
|
|
15
|
+
* writing a value nothing reads is the dishonesty class this design bans.
|
|
16
|
+
*
|
|
17
|
+
* `migrated` is omitted entirely (not `undefined` written out) for suite keys
|
|
18
|
+
* outside rt's wave-1 legacy-file migration — deck/board/gitq/mattstack/claude
|
|
19
|
+
* defs have no legacy file to migrate FROM, so the flag is meaningless for
|
|
20
|
+
* them. `isMigrated()` is the one place that turns absence into "yes,
|
|
21
|
+
* resolver-backed" — every other module must call it rather than testing
|
|
22
|
+
* `def.migrated` directly, or a suite key's absent flag reads as `false` and
|
|
23
|
+
* `set` refuses it.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { REGISTRY } from "./registry-defs.ts";
|
|
27
|
+
|
|
28
|
+
export type SettingScope = "user" | "team" | "machine";
|
|
29
|
+
|
|
30
|
+
export interface SettingDef {
|
|
31
|
+
key: string;
|
|
32
|
+
type: "string" | "number" | "boolean" | "object" | "array";
|
|
33
|
+
scopes: SettingScope[];
|
|
34
|
+
default?: unknown;
|
|
35
|
+
merge: "replace" | "deep";
|
|
36
|
+
teamLocked?: boolean;
|
|
37
|
+
secret?: boolean;
|
|
38
|
+
repoScoped?: boolean;
|
|
39
|
+
migrated?: boolean;
|
|
40
|
+
legacyFile?: string;
|
|
41
|
+
pathGuardFields?: string[];
|
|
42
|
+
description: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const BY_KEY: Map<string, SettingDef> = new Map(REGISTRY.map((def) => [def.key, def]));
|
|
46
|
+
|
|
47
|
+
/** Looks up a def by its flat namespaced key (e.g. "rt.roles"). */
|
|
48
|
+
export function getDef(key: string): SettingDef | undefined {
|
|
49
|
+
return BY_KEY.get(key);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Every registered def, in registry declaration order. */
|
|
53
|
+
export function allDefs(): SettingDef[] {
|
|
54
|
+
return [...REGISTRY];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** True unless `def.migrated` is explicitly `false` — see the module doc. */
|
|
58
|
+
export function isMigrated(def: SettingDef): boolean {
|
|
59
|
+
return def.migrated !== false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const PATH_LIKE = /^[/~]/;
|
|
63
|
+
|
|
64
|
+
function typeOf(value: unknown): string {
|
|
65
|
+
if (value === null) return "null";
|
|
66
|
+
if (Array.isArray(value)) return "array";
|
|
67
|
+
return typeof value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Checks whether `value` is a legal value for `def`: the JSON-ish type
|
|
72
|
+
* matches def.type, and (when def.pathGuardFields is set) no guarded field
|
|
73
|
+
* anywhere in the value looks like an absolute path or home-relative path
|
|
74
|
+
* literal — those are only legal in the machine store's own file contents,
|
|
75
|
+
* never as a shared-scope value (spec: "No path type exists ... enforced for
|
|
76
|
+
* wave-1 keys on the hook field specifically").
|
|
77
|
+
*/
|
|
78
|
+
export function validateValue(def: SettingDef, value: unknown): { ok: true } | { ok: false; reason: string } {
|
|
79
|
+
const typeCheck = checkType(def.type, value);
|
|
80
|
+
if (!typeCheck.ok) return typeCheck;
|
|
81
|
+
|
|
82
|
+
if (def.pathGuardFields && def.pathGuardFields.length > 0) {
|
|
83
|
+
const violation = findPathGuardViolation(value, def.pathGuardFields);
|
|
84
|
+
if (violation) {
|
|
85
|
+
return {
|
|
86
|
+
ok: false,
|
|
87
|
+
reason: `field "${violation.field}" looks like a path literal ("${violation.value}"); path literals are only legal in the machine store`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { ok: true };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function checkType(type: SettingDef["type"], value: unknown): { ok: true } | { ok: false; reason: string } {
|
|
96
|
+
switch (type) {
|
|
97
|
+
case "string":
|
|
98
|
+
return typeof value === "string" ? { ok: true } : { ok: false, reason: `expected string, got ${typeOf(value)}` };
|
|
99
|
+
case "number":
|
|
100
|
+
return typeof value === "number" ? { ok: true } : { ok: false, reason: `expected number, got ${typeOf(value)}` };
|
|
101
|
+
case "boolean":
|
|
102
|
+
return typeof value === "boolean" ? { ok: true } : { ok: false, reason: `expected boolean, got ${typeOf(value)}` };
|
|
103
|
+
case "array":
|
|
104
|
+
return Array.isArray(value) ? { ok: true } : { ok: false, reason: `expected array, got ${typeOf(value)}` };
|
|
105
|
+
case "object":
|
|
106
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
107
|
+
? { ok: true }
|
|
108
|
+
: { ok: false, reason: `expected object, got ${typeOf(value)}` };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Walks a value looking for any object field named in `guardFields` whose
|
|
114
|
+
* string value looks like a path literal (leading `/` or `~`). Recurses
|
|
115
|
+
* through plain objects and arrays; best-effort (spec: "enforced for wave-1
|
|
116
|
+
* keys on the hook field specifically, best-effort elsewhere").
|
|
117
|
+
*/
|
|
118
|
+
function findPathGuardViolation(
|
|
119
|
+
value: unknown,
|
|
120
|
+
guardFields: string[],
|
|
121
|
+
): { field: string; value: string } | null {
|
|
122
|
+
if (Array.isArray(value)) {
|
|
123
|
+
for (const item of value) {
|
|
124
|
+
const hit = findPathGuardViolation(item, guardFields);
|
|
125
|
+
if (hit) return hit;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (value !== null && typeof value === "object") {
|
|
131
|
+
for (const [field, fieldValue] of Object.entries(value as Record<string, unknown>)) {
|
|
132
|
+
if (guardFields.includes(field) && typeof fieldValue === "string" && PATH_LIKE.test(fieldValue)) {
|
|
133
|
+
return { field, value: fieldValue };
|
|
134
|
+
}
|
|
135
|
+
const hit = findPathGuardViolation(fieldValue, guardFields);
|
|
136
|
+
if (hit) return hit;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return null;
|
|
141
|
+
}
|