@mattstack/rt-client 0.3.0 → 0.4.1
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 +48 -0
- package/dist/client.d.ts +88 -1
- package/dist/commands.d.ts +180 -0
- package/dist/index.d.ts +4 -4
- package/dist/index.js +338 -21
- package/dist/repos.d.ts +9 -3
- package/dist/settings/identity.d.ts +43 -19
- package/dist/settings/write.d.ts +12 -0
- package/package.json +1 -1
- package/src/client.ts +141 -1
- package/src/commands.ts +91 -1
- package/src/index.ts +38 -3
- package/src/repos.ts +85 -14
- package/src/settings/identity.ts +121 -28
- package/src/settings/registry-defs.ts +43 -6
- package/src/settings/write.ts +91 -0
|
@@ -136,7 +136,7 @@ export const REGISTRY: readonly SettingDef[] = [
|
|
|
136
136
|
merge: "deep",
|
|
137
137
|
repoScoped: true,
|
|
138
138
|
migrated: true,
|
|
139
|
-
description: "Branch-naming templates.
|
|
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
140
|
},
|
|
141
141
|
{
|
|
142
142
|
key: "rt.variations",
|
|
@@ -196,17 +196,14 @@ export const REGISTRY: readonly SettingDef[] = [
|
|
|
196
196
|
migrated: true,
|
|
197
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
198
|
},
|
|
199
|
-
|
|
200
|
-
// --- migrated:false (deferred by ruling) --------------------------------
|
|
201
199
|
{
|
|
202
200
|
key: "rt.hooks",
|
|
203
201
|
type: "object",
|
|
204
202
|
scopes: ALL_SCOPES,
|
|
205
203
|
merge: "deep",
|
|
206
204
|
repoScoped: true,
|
|
207
|
-
migrated:
|
|
208
|
-
|
|
209
|
-
description: "User-defined lifecycle hooks rt runs around commands (pre/post command scripts).",
|
|
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.",
|
|
210
207
|
},
|
|
211
208
|
|
|
212
209
|
// --- mattstack (installer-lane) -----------------------------------------
|
|
@@ -231,6 +228,15 @@ export const REGISTRY: readonly SettingDef[] = [
|
|
|
231
228
|
merge: "replace",
|
|
232
229
|
description: "Absolute path to the installed mattstack.app bundle, written by the app at launch so rt stops hardcoding ~/Applications.",
|
|
233
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
|
+
},
|
|
234
240
|
|
|
235
241
|
// --- claude (installer-lane) --------------------------------------------
|
|
236
242
|
{
|
|
@@ -436,4 +442,35 @@ export const REGISTRY: readonly SettingDef[] = [
|
|
|
436
442
|
merge: "deep",
|
|
437
443
|
description: "gitq checkout-board config: tracked repos, local port, and the herdr workspace it launches into.",
|
|
438
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
|
+
},
|
|
439
476
|
];
|
package/src/settings/write.ts
CHANGED
|
@@ -160,6 +160,49 @@ export function setSetting(key: string, value: unknown, scope: SettingScope, opt
|
|
|
160
160
|
);
|
|
161
161
|
}
|
|
162
162
|
|
|
163
|
+
/**
|
|
164
|
+
* Removes `key` from the given scope's store, comment-preserving. The refusal
|
|
165
|
+
* ladder is `setSetting`'s minus the value check (there is no value): unknown
|
|
166
|
+
* key, unmigrated, scope not in `def.scopes`, repoIdentity on a non-repoScoped
|
|
167
|
+
* key, and the team-selection rule when ambiguous. Divergences from set, both
|
|
168
|
+
* because removal has nothing to act on: a store FILE that does not exist is a
|
|
169
|
+
* clean no-op rather than a refusal (an explicit `opts.team` naming a team
|
|
170
|
+
* with no local store included — nothing to remove is success, not an error),
|
|
171
|
+
* and a key not present in the store is a no-op. Returns whether anything was
|
|
172
|
+
* actually removed; the local-only reminder prints only on a real removal.
|
|
173
|
+
*/
|
|
174
|
+
export function unsetSetting(key: string, scope: SettingScope, opts: SetSettingOpts = {}): boolean {
|
|
175
|
+
const def = getDef(key);
|
|
176
|
+
if (!def) {
|
|
177
|
+
refuse(`unknown setting "${key}" — not in the settings registry (see \`rt settings list\`)`);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!isMigrated(def)) {
|
|
181
|
+
refuse(migratedFalseMessage(key, def));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (!def.scopes.includes(scope)) {
|
|
185
|
+
refuse(`"${key}" cannot be unset in the ${scope} store (allowed: ${def.scopes.join(", ")})`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (opts.repoIdentity !== undefined && def.repoScoped !== true) {
|
|
189
|
+
refuse(`"${key}" is not repo-scoped — omit the repo identity`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const storePath = resolveStorePathForUnset(scope, opts);
|
|
193
|
+
if (storePath === null || !existsSync(storePath)) return false;
|
|
194
|
+
|
|
195
|
+
const jsonPath: JSONPath = opts.repoIdentity !== undefined ? ["repos", opts.repoIdentity, key] : [key];
|
|
196
|
+
const removed = removeFromStore(storePath, jsonPath);
|
|
197
|
+
|
|
198
|
+
if (removed) {
|
|
199
|
+
console.error(
|
|
200
|
+
`rt: removed "${key}" from the local ${scope} store (${storePath}) — this is local only until you commit and push it.`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
return removed;
|
|
204
|
+
}
|
|
205
|
+
|
|
163
206
|
function migratedFalseMessage(key: string, def: SettingDef): string {
|
|
164
207
|
const legacyPart = def.legacyFile ? ` — it is still read from ${def.legacyFile}` : "";
|
|
165
208
|
return `"${key}" is not writable through the settings resolver yet${legacyPart}`;
|
|
@@ -188,6 +231,29 @@ function resolveStorePath(scope: SettingScope, opts: SetSettingOpts): string {
|
|
|
188
231
|
return teamSettingsPath(teams[0] as string);
|
|
189
232
|
}
|
|
190
233
|
|
|
234
|
+
/**
|
|
235
|
+
* `resolveStorePath` for removal: same selection rule, but "no store to
|
|
236
|
+
* target" answers null (nothing to remove) instead of refusing — EXCEPT the
|
|
237
|
+
* multiple-teams case, which still refuses: guessing which team's store to
|
|
238
|
+
* edit is banned on the unset side for the same reason as the set side.
|
|
239
|
+
*/
|
|
240
|
+
function resolveStorePathForUnset(scope: SettingScope, opts: SetSettingOpts): string | null {
|
|
241
|
+
if (scope === "user") return userSettingsPath();
|
|
242
|
+
if (scope === "machine") return machineSettingsPath();
|
|
243
|
+
|
|
244
|
+
if (opts.team !== undefined) {
|
|
245
|
+
const path = teamSettingsPath(opts.team);
|
|
246
|
+
return existsSync(path) ? path : null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const teams = listTeams();
|
|
250
|
+
if (teams.length === 0) return null;
|
|
251
|
+
if (teams.length > 1) {
|
|
252
|
+
refuse(`multiple local team stores found (${teams.join(", ")}) — pass opts.team to choose one`);
|
|
253
|
+
}
|
|
254
|
+
return teamSettingsPath(teams[0] as string);
|
|
255
|
+
}
|
|
256
|
+
|
|
191
257
|
/** `// header comment\n{}\n` — see module doc for why the object must be seeded before the first `modify`. */
|
|
192
258
|
function seedHeader(): string {
|
|
193
259
|
return `// rt settings — created by \`rt settings set\`. JSONC: comments and trailing commas are fine.\n{}\n`;
|
|
@@ -279,6 +345,31 @@ function writeIntoStore(storePath: string, jsonPath: JSONPath, value: unknown, c
|
|
|
279
345
|
// a torn write would sit as a corrupt uncommitted file until a human
|
|
280
346
|
// noticed. The edited TEXT is written as-is, never round-tripped through
|
|
281
347
|
// JSON.stringify, so comments and formatting survive.
|
|
348
|
+
writeTempThenRename(storePath, finalText);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Removes `jsonPath` from an existing store file. A key that isn't present
|
|
353
|
+
* yields zero edits from `modify` and the file is left untouched (no write,
|
|
354
|
+
* no mtime churn). Malformed stores refuse exactly as on the set side —
|
|
355
|
+
* `modify`-by-offset against a duplicate-key document is as wrong for
|
|
356
|
+
* removal as it is for writes.
|
|
357
|
+
*/
|
|
358
|
+
function removeFromStore(storePath: string, jsonPath: JSONPath): boolean {
|
|
359
|
+
const content = readFileSync(storePath, "utf8");
|
|
360
|
+
if (content.trim() === "") return false;
|
|
361
|
+
assertEditableJsonc(storePath, content);
|
|
362
|
+
|
|
363
|
+
const edits = modify(content, jsonPath, undefined, { formattingOptions: FORMAT });
|
|
364
|
+
if (edits.length === 0) return false;
|
|
365
|
+
|
|
366
|
+
const next = applyEdits(content, edits);
|
|
367
|
+
const finalText = next.endsWith("\n") ? next : `${next}\n`;
|
|
368
|
+
writeTempThenRename(storePath, finalText);
|
|
369
|
+
return true;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function writeTempThenRename(storePath: string, finalText: string): void {
|
|
282
373
|
const tmp = `${storePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
|
|
283
374
|
try {
|
|
284
375
|
writeFileSync(tmp, finalText);
|