@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/dist/index.js CHANGED
@@ -1,3 +1,6 @@
1
+ import { createRequire } from "node:module";
2
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
3
+
1
4
  // src/transport.ts
2
5
  import { homedir } from "os";
3
6
  import { join } from "path";
@@ -39,6 +42,93 @@ function readMrsByBranch(repoName, branches, opts = {}) {
39
42
  function resolveForgeToken(repoName, forge, opts = {}) {
40
43
  return rtCommand("secrets:forge-token", { repoName, forge }, { sockPath: opts.sockPath, timeoutMs: 1e4 });
41
44
  }
45
+ function listRuns(repo, opts = {}) {
46
+ const payload = {};
47
+ if (repo !== undefined)
48
+ payload.repo = repo;
49
+ return rtCommand("runs:list", payload, { sockPath: opts.sockPath, timeoutMs: 1e4 });
50
+ }
51
+ function getRun(runId, repo, opts = {}) {
52
+ const payload = { runId };
53
+ if (repo !== undefined)
54
+ payload.repo = repo;
55
+ return rtCommand("runs:get", payload, { sockPath: opts.sockPath, timeoutMs: 1e4 });
56
+ }
57
+ function abandonRun(runId, repo, reason, opts = {}) {
58
+ const payload = { runId };
59
+ if (repo !== undefined)
60
+ payload.repo = repo;
61
+ if (reason !== undefined)
62
+ payload.reason = reason;
63
+ return rtCommand("runs:abandon", payload, { sockPath: opts.sockPath, timeoutMs: 1e4 });
64
+ }
65
+ function chatJoin(a, o = {}) {
66
+ const payload = { room: a.room, handle: a.handle };
67
+ if (a.wakeOn !== undefined)
68
+ payload.wakeOn = a.wakeOn;
69
+ if (a.cwd !== undefined)
70
+ payload.cwd = a.cwd;
71
+ if (a.pane !== undefined)
72
+ payload.pane = a.pane;
73
+ return rtCommand("chat:join", payload, { sockPath: o.sockPath, timeoutMs: 1e4 });
74
+ }
75
+ function chatLeave(a, o = {}) {
76
+ return rtCommand("chat:leave", { room: a.room, handle: a.handle }, { sockPath: o.sockPath, timeoutMs: 1e4 });
77
+ }
78
+ function chatPost(a, o = {}) {
79
+ return rtCommand("chat:post", { room: a.room, handle: a.handle, body: a.body }, { sockPath: o.sockPath, timeoutMs: 1e4 });
80
+ }
81
+ function chatRead(a, o = {}) {
82
+ const payload = { handle: a.handle };
83
+ if (a.room !== undefined)
84
+ payload.room = a.room;
85
+ if (a.limit !== undefined)
86
+ payload.limit = a.limit;
87
+ if (a.sinceMs !== undefined)
88
+ payload.sinceMs = a.sinceMs;
89
+ return rtCommand("chat:read", payload, { sockPath: o.sockPath, timeoutMs: 1e4 });
90
+ }
91
+ function chatRooms(a, o = {}) {
92
+ return rtCommand("chat:rooms", { handle: a.handle }, { sockPath: o.sockPath, timeoutMs: 1e4 });
93
+ }
94
+ function chatWho(a, o = {}) {
95
+ return rtCommand("chat:who", { room: a.room }, { sockPath: o.sockPath, timeoutMs: 1e4 });
96
+ }
97
+ function chatMark(a, o = {}) {
98
+ const payload = { handle: a.handle };
99
+ if (a.room !== undefined)
100
+ payload.room = a.room;
101
+ return rtCommand("chat:mark", payload, { sockPath: o.sockPath, timeoutMs: 1e4 });
102
+ }
103
+ function chatMessages(a, o = {}) {
104
+ const payload = { room: a.room };
105
+ if (a.before !== undefined)
106
+ payload.before = a.before;
107
+ if (a.limit !== undefined)
108
+ payload.limit = a.limit;
109
+ return rtCommand("chat:messages", payload, { sockPath: o.sockPath, timeoutMs: 1e4 });
110
+ }
111
+ function chatArm(a, o = {}) {
112
+ const payload = { handle: a.handle };
113
+ if (a.room !== undefined)
114
+ payload.room = a.room;
115
+ return rtCommand("chat:arm", payload, { sockPath: o.sockPath, timeoutMs: 1e4 });
116
+ }
117
+ function chatTouch(a, o = {}) {
118
+ return rtCommand("chat:touch", { handle: a.handle }, { sockPath: o.sockPath, timeoutMs: 1e4 });
119
+ }
120
+ function chatDisarm(a, o = {}) {
121
+ return rtCommand("chat:disarm", { handle: a.handle }, { sockPath: o.sockPath, timeoutMs: 1e4 });
122
+ }
123
+ function chatUnreadWaking(a, o = {}) {
124
+ const payload = { handle: a.handle };
125
+ if (a.room !== undefined)
126
+ payload.room = a.room;
127
+ return rtCommand("chat:unread-waking", payload, { sockPath: o.sockPath, timeoutMs: 1e4 });
128
+ }
129
+ function eventsHead(o = {}) {
130
+ return rtCommand("events:head", {}, { sockPath: o.sockPath, timeoutMs: 1e4 });
131
+ }
42
132
  // src/commands.ts
43
133
  var COMMAND_NAMES = [
44
134
  "project-mrs:read",
@@ -49,8 +139,22 @@ var COMMAND_NAMES = [
49
139
  "events:emit",
50
140
  "events:wait",
51
141
  "events:list",
142
+ "events:head",
52
143
  "runs:list",
53
- "runs:get"
144
+ "runs:get",
145
+ "runs:abandon",
146
+ "chat:join",
147
+ "chat:leave",
148
+ "chat:post",
149
+ "chat:read",
150
+ "chat:rooms",
151
+ "chat:who",
152
+ "chat:mark",
153
+ "chat:messages",
154
+ "chat:arm",
155
+ "chat:touch",
156
+ "chat:disarm",
157
+ "chat:unread-waking"
54
158
  ];
55
159
  // src/relay.ts
56
160
  var DEFAULT_WS_URL = "ws://127.0.0.1:9401/ws";
@@ -98,16 +202,48 @@ function subscribe(onEvent, opts = {}) {
98
202
  // src/repos.ts
99
203
  import { existsSync, readFileSync } from "fs";
100
204
  import { homedir as homedir2 } from "os";
101
- import { join as join2 } from "path";
205
+ import { dirname, join as join2 } from "path";
102
206
  function defaultReposJsonPath() {
103
207
  return join2(homedir2(), ".mattstack", "rt", "repos.json");
104
208
  }
105
- function repoNameForPath(repoPath, reposJsonPath) {
106
- const path = reposJsonPath ?? defaultReposJsonPath();
209
+ function loadBunSqliteDatabase() {
210
+ try {
211
+ return __require("bun:sqlite").Database;
212
+ } catch {
213
+ return null;
214
+ }
215
+ }
216
+ function repoNameFromStateDb(repoPath, dbPath) {
217
+ if (!existsSync(dbPath))
218
+ return null;
219
+ const DatabaseCtor = loadBunSqliteDatabase();
220
+ if (!DatabaseCtor)
221
+ return null;
107
222
  try {
108
- if (!existsSync(path))
223
+ const db = new DatabaseCtor(dbPath, { readonly: true });
224
+ try {
225
+ const rows = db.query("SELECT k, v FROM kv WHERE ns = 'repo-index';").all();
226
+ for (const row of rows) {
227
+ try {
228
+ if (JSON.parse(row.v) === repoPath)
229
+ return row.k;
230
+ } catch {
231
+ continue;
232
+ }
233
+ }
109
234
  return null;
110
- const raw = readFileSync(path, "utf8");
235
+ } finally {
236
+ db.close();
237
+ }
238
+ } catch {
239
+ return null;
240
+ }
241
+ }
242
+ function repoNameFromJson(repoPath, reposJsonPath) {
243
+ try {
244
+ if (!existsSync(reposJsonPath))
245
+ return null;
246
+ const raw = readFileSync(reposJsonPath, "utf8");
111
247
  const index = JSON.parse(raw);
112
248
  for (const [repoName, value] of Object.entries(index)) {
113
249
  if (value === repoPath)
@@ -118,6 +254,14 @@ function repoNameForPath(repoPath, reposJsonPath) {
118
254
  return null;
119
255
  }
120
256
  }
257
+ function repoNameForPath(repoPath, reposJsonPath) {
258
+ const jsonPath = reposJsonPath ?? defaultReposJsonPath();
259
+ const dbPath = join2(dirname(jsonPath), "state.db");
260
+ const fromDb = repoNameFromStateDb(repoPath, dbPath);
261
+ if (fromDb !== null)
262
+ return fromDb;
263
+ return repoNameFromJson(repoPath, jsonPath);
264
+ }
121
265
  // src/settings/resolve.ts
122
266
  import { homedir as homedir4 } from "os";
123
267
  import { join as join5 } from "path";
@@ -278,7 +422,7 @@ var REGISTRY = [
278
422
  merge: "deep",
279
423
  repoScoped: true,
280
424
  migrated: true,
281
- description: "Branch-naming templates. rt itself has no readers of this key yet the VS Code extension still reads repos/<repo>/branch-naming.json by repo name, which stays authoritative until the extension ports over. Setting this key stores a value nothing consumes."
425
+ 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."
282
426
  },
283
427
  {
284
428
  key: "rt.variations",
@@ -338,9 +482,8 @@ var REGISTRY = [
338
482
  scopes: ALL_SCOPES,
339
483
  merge: "deep",
340
484
  repoScoped: true,
341
- migrated: false,
342
- legacyFile: "repos/<repo>/hooks.json",
343
- description: "User-defined lifecycle hooks rt runs around commands (pre/post command scripts)."
485
+ migrated: true,
486
+ 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."
344
487
  },
345
488
  {
346
489
  key: "mattstack.integrations",
@@ -363,6 +506,14 @@ var REGISTRY = [
363
506
  merge: "replace",
364
507
  description: "Absolute path to the installed mattstack.app bundle, written by the app at launch so rt stops hardcoding ~/Applications."
365
508
  },
509
+ {
510
+ key: "rt.integrations",
511
+ type: "object",
512
+ scopes: ["user"],
513
+ merge: "deep",
514
+ migrated: true,
515
+ description: "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."
516
+ },
366
517
  {
367
518
  key: "claude.marketplaces",
368
519
  type: "array",
@@ -551,6 +702,35 @@ var REGISTRY = [
551
702
  scopes: ["machine"],
552
703
  merge: "deep",
553
704
  description: "gitq checkout-board config: tracked repos, local port, and the herdr workspace it launches into."
705
+ },
706
+ {
707
+ key: "chat.handle",
708
+ type: "string",
709
+ scopes: ["user"],
710
+ merge: "replace",
711
+ description: "Explicit rt chat handle for this developer on this machine; overrides the derived <repo>-<dir> handle when set."
712
+ },
713
+ {
714
+ key: "chat.humanHandle",
715
+ type: "string",
716
+ scopes: ["user"],
717
+ default: "matt",
718
+ merge: "replace",
719
+ description: "The human's own chat handle, so agents can @-mention them by name."
720
+ },
721
+ {
722
+ key: "chat.push.provider",
723
+ type: "string",
724
+ scopes: ["user"],
725
+ merge: "replace",
726
+ description: "Push notification provider used to alert the human of chat mentions when away from a terminal."
727
+ },
728
+ {
729
+ key: "chat.push.target",
730
+ type: "string",
731
+ scopes: ["user"],
732
+ merge: "replace",
733
+ description: "Destination (topic/URL/token) the configured chat.push.provider sends to."
554
734
  }
555
735
  ];
556
736
 
@@ -992,7 +1172,7 @@ function explainSetting(key, opts = {}) {
992
1172
  import { existsSync as existsSync3, mkdirSync, readFileSync as readFileSync4, renameSync, unlinkSync, writeFileSync } from "fs";
993
1173
  import { applyEdits, modify, parseTree } from "jsonc-parser";
994
1174
  import { randomBytes } from "crypto";
995
- import { dirname } from "path";
1175
+ import { dirname as dirname2 } from "path";
996
1176
  var FORMAT = { tabSize: 2, insertSpaces: true, eol: `
997
1177
  ` };
998
1178
  function refuse(message) {
@@ -1022,6 +1202,30 @@ function setSetting(key, value, scope, opts = {}) {
1022
1202
  writeIntoStore(storePath, jsonPath, value, scope !== "team");
1023
1203
  console.error(`rt: wrote "${key}" to the local ${scope} store (${storePath}) — this is local only until you commit and push it.`);
1024
1204
  }
1205
+ function unsetSetting(key, scope, opts = {}) {
1206
+ const def = getDef(key);
1207
+ if (!def) {
1208
+ refuse(`unknown setting "${key}" — not in the settings registry (see \`rt settings list\`)`);
1209
+ }
1210
+ if (!isMigrated(def)) {
1211
+ refuse(migratedFalseMessage(key, def));
1212
+ }
1213
+ if (!def.scopes.includes(scope)) {
1214
+ refuse(`"${key}" cannot be unset in the ${scope} store (allowed: ${def.scopes.join(", ")})`);
1215
+ }
1216
+ if (opts.repoIdentity !== undefined && def.repoScoped !== true) {
1217
+ refuse(`"${key}" is not repo-scoped — omit the repo identity`);
1218
+ }
1219
+ const storePath = resolveStorePathForUnset(scope, opts);
1220
+ if (storePath === null || !existsSync3(storePath))
1221
+ return false;
1222
+ const jsonPath = opts.repoIdentity !== undefined ? ["repos", opts.repoIdentity, key] : [key];
1223
+ const removed = removeFromStore(storePath, jsonPath);
1224
+ if (removed) {
1225
+ console.error(`rt: removed "${key}" from the local ${scope} store (${storePath}) — this is local only until you commit and push it.`);
1226
+ }
1227
+ return removed;
1228
+ }
1025
1229
  function migratedFalseMessage(key, def) {
1026
1230
  const legacyPart = def.legacyFile ? ` — it is still read from ${def.legacyFile}` : "";
1027
1231
  return `"${key}" is not writable through the settings resolver yet${legacyPart}`;
@@ -1047,6 +1251,23 @@ function resolveStorePath(scope, opts) {
1047
1251
  }
1048
1252
  return teamSettingsPath(teams[0]);
1049
1253
  }
1254
+ function resolveStorePathForUnset(scope, opts) {
1255
+ if (scope === "user")
1256
+ return userSettingsPath();
1257
+ if (scope === "machine")
1258
+ return machineSettingsPath();
1259
+ if (opts.team !== undefined) {
1260
+ const path = teamSettingsPath(opts.team);
1261
+ return existsSync3(path) ? path : null;
1262
+ }
1263
+ const teams = listTeams();
1264
+ if (teams.length === 0)
1265
+ return null;
1266
+ if (teams.length > 1) {
1267
+ refuse(`multiple local team stores found (${teams.join(", ")}) — pass opts.team to choose one`);
1268
+ }
1269
+ return teamSettingsPath(teams[0]);
1270
+ }
1050
1271
  function seedHeader() {
1051
1272
  return `// rt settings — created by \`rt settings set\`. JSONC: comments and trailing commas are fine.
1052
1273
  {}
@@ -1101,7 +1322,7 @@ function writeIntoStore(storePath, jsonPath, value, createIfMissing) {
1101
1322
  if (!createIfMissing) {
1102
1323
  refuse(`store file ${storePath} does not exist`);
1103
1324
  }
1104
- mkdirSync(dirname(storePath), { recursive: true });
1325
+ mkdirSync(dirname2(storePath), { recursive: true });
1105
1326
  content = seedHeader();
1106
1327
  }
1107
1328
  const edits = modify(content, jsonPath, value, { formattingOptions: FORMAT });
@@ -1109,6 +1330,24 @@ function writeIntoStore(storePath, jsonPath, value, createIfMissing) {
1109
1330
  const finalText = next.endsWith(`
1110
1331
  `) ? next : `${next}
1111
1332
  `;
1333
+ writeTempThenRename(storePath, finalText);
1334
+ }
1335
+ function removeFromStore(storePath, jsonPath) {
1336
+ const content = readFileSync4(storePath, "utf8");
1337
+ if (content.trim() === "")
1338
+ return false;
1339
+ assertEditableJsonc(storePath, content);
1340
+ const edits = modify(content, jsonPath, undefined, { formattingOptions: FORMAT });
1341
+ if (edits.length === 0)
1342
+ return false;
1343
+ const next = applyEdits(content, edits);
1344
+ const finalText = next.endsWith(`
1345
+ `) ? next : `${next}
1346
+ `;
1347
+ writeTempThenRename(storePath, finalText);
1348
+ return true;
1349
+ }
1350
+ function writeTempThenRename(storePath, finalText) {
1112
1351
  const tmp = `${storePath}.${process.pid}.${randomBytes(6).toString("hex")}.tmp`;
1113
1352
  try {
1114
1353
  writeFileSync(tmp, finalText);
@@ -1120,6 +1359,9 @@ function writeIntoStore(storePath, jsonPath, value, createIfMissing) {
1120
1359
  throw err;
1121
1360
  }
1122
1361
  }
1362
+ // src/settings/identity.ts
1363
+ import { existsSync as existsSync4, readFileSync as readFileSync5, realpathSync } from "fs";
1364
+
1123
1365
  // src/settings/exec.ts
1124
1366
  async function runCapture(argv, opts = {}) {
1125
1367
  const captureStderr = opts.stderr === "pipe";
@@ -1156,6 +1398,27 @@ async function runCapture(argv, opts = {}) {
1156
1398
  // src/settings/identity.ts
1157
1399
  var URL_RE = /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/(?:[^@/]+@)?([^/]+)\/(.+)$/;
1158
1400
  var SCP_RE = /^(?:[^@/\s]+@)?([^:/\s]+):(.+)$/;
1401
+ function serializeIdentity(id) {
1402
+ return `${id.kind}:${encodeURIComponent(id.id)}`;
1403
+ }
1404
+ function parseIdentity(wire) {
1405
+ const colon = wire.indexOf(":");
1406
+ if (colon === -1)
1407
+ return null;
1408
+ const kind = wire.slice(0, colon);
1409
+ if (kind !== "remote" && kind !== "path")
1410
+ return null;
1411
+ const encoded = wire.slice(colon + 1);
1412
+ let id;
1413
+ try {
1414
+ id = decodeURIComponent(encoded);
1415
+ } catch {
1416
+ return null;
1417
+ }
1418
+ if (encodeURIComponent(id) !== encoded)
1419
+ return null;
1420
+ return { kind, id };
1421
+ }
1159
1422
  function normalizeRemote(remote) {
1160
1423
  const trimmed = remote.trim();
1161
1424
  if (!trimmed)
@@ -1186,9 +1449,10 @@ function identityFromRemote(remote) {
1186
1449
  if (overrides !== null && typeof overrides === "object" && !Array.isArray(overrides)) {
1187
1450
  const hit = overrides[remote];
1188
1451
  if (typeof hit === "string")
1189
- return hit;
1452
+ return { kind: "remote", id: hit };
1190
1453
  }
1191
- return normalizeRemote(remote);
1454
+ const normalized = normalizeRemote(remote);
1455
+ return normalized === null ? null : { kind: "remote", id: normalized };
1192
1456
  }
1193
1457
  var memo = new Map;
1194
1458
  async function deriveRepoIdentity(repoPath) {
@@ -1197,43 +1461,96 @@ async function deriveRepoIdentity(repoPath) {
1197
1461
  return cached;
1198
1462
  const result = await (async () => {
1199
1463
  const spawned = await runCapture(["git", "-C", repoPath, "config", "--get", "remote.origin.url"]);
1200
- if (spawned.exitCode !== 0)
1201
- return null;
1202
- const remote = spawned.stdout.trim();
1203
- if (!remote)
1204
- return null;
1205
- return identityFromRemote(remote);
1464
+ if (spawned.exitCode === 0) {
1465
+ const remote = spawned.stdout.trim();
1466
+ const fromRemote = remote ? identityFromRemote(remote) : null;
1467
+ if (fromRemote)
1468
+ return fromRemote;
1469
+ }
1470
+ const listed = await runCapture(["git", "-C", repoPath, "worktree", "list", "--porcelain"]);
1471
+ const first = listed.exitCode === 0 ? /^worktree (.+)$/m.exec(listed.stdout)?.[1]?.trim() : undefined;
1472
+ let base;
1473
+ if (first) {
1474
+ const top = await runCapture(["git", "-C", first, "rev-parse", "--show-toplevel"]);
1475
+ if (top.exitCode === 0 && top.stdout.trim())
1476
+ base = top.stdout.trim();
1477
+ }
1478
+ if (!base) {
1479
+ const own = await runCapture(["git", "-C", repoPath, "rev-parse", "--show-toplevel"]);
1480
+ base = own.exitCode === 0 && own.stdout.trim() ? own.stdout.trim() : repoPath;
1481
+ }
1482
+ return { kind: "path", id: safeRealpath(base) };
1206
1483
  })();
1207
- if (result !== null)
1484
+ if (result.kind === "remote")
1208
1485
  memo.set(repoPath, Promise.resolve(result));
1209
1486
  return result;
1210
1487
  }
1211
1488
  function clearIdentityMemo() {
1212
1489
  memo.clear();
1213
1490
  }
1491
+ function safeRealpath(p) {
1492
+ try {
1493
+ return realpathSync(p);
1494
+ } catch {
1495
+ return p;
1496
+ }
1497
+ }
1498
+ async function resolveNameToIdentity(name, reposJsonPath) {
1499
+ if (!existsSync4(reposJsonPath))
1500
+ return null;
1501
+ try {
1502
+ const index = JSON.parse(readFileSync5(reposJsonPath, "utf8"));
1503
+ const path = index[name];
1504
+ if (typeof path !== "string")
1505
+ return null;
1506
+ return await deriveRepoIdentity(path);
1507
+ } catch {
1508
+ return null;
1509
+ }
1510
+ }
1214
1511
  export {
1215
1512
  validateValue,
1513
+ unsetSetting,
1216
1514
  subscribe,
1217
1515
  setSetting,
1516
+ serializeIdentity,
1218
1517
  rtCommand,
1518
+ resolveNameToIdentity,
1219
1519
  resolveForgeToken,
1220
1520
  repoNameForPath,
1221
1521
  readStore,
1222
1522
  readProjectMRs,
1223
1523
  readMrsByBranch,
1224
1524
  readDiscussions,
1525
+ parseIdentity,
1225
1526
  normalizeRemote,
1226
1527
  listTeams,
1227
1528
  listSettings,
1529
+ listRuns,
1228
1530
  isMigrated,
1229
1531
  identityFromRemote,
1230
1532
  getSetting,
1533
+ getRun,
1231
1534
  getDef,
1232
1535
  explainSetting,
1233
1536
  expandVariables,
1537
+ eventsHead,
1234
1538
  deriveRepoIdentity,
1235
1539
  clearIdentityMemo,
1540
+ chatWho,
1541
+ chatUnreadWaking,
1542
+ chatTouch,
1543
+ chatRooms,
1544
+ chatRead,
1545
+ chatPost,
1546
+ chatMessages,
1547
+ chatMark,
1548
+ chatLeave,
1549
+ chatJoin,
1550
+ chatDisarm,
1551
+ chatArm,
1236
1552
  allDefs,
1553
+ abandonRun,
1237
1554
  SCOPE_ORDER,
1238
1555
  REGISTRY,
1239
1556
  DEFAULT_WS_URL,
package/dist/repos.d.ts CHANGED
@@ -1,7 +1,13 @@
1
1
  /**
2
2
  * Exact-match lookup: returns the repo name whose recorded path equals
3
- * `repoPath`, or null if the file is missing, corrupt, or has no match.
4
- * Never throws -- a resolution failure just means the caller falls back to
5
- * whatever it had before (an unqualified path, a prompt, etc).
3
+ * `repoPath`, or null if no source has a match. Never throws -- a
4
+ * resolution failure just means the caller falls back to whatever it had
5
+ * before (an unqualified path, a prompt, etc).
6
+ *
7
+ * Prefers state.db (authoritative, kept live by every rt process); falls
8
+ * back to the repos.json compat mirror when state.db is unreachable — a
9
+ * pre-upgrade rt install, a non-Bun consumer, or a state.db this process
10
+ * can't open. `reposJsonPath`, when passed, also relocates the state.db
11
+ * lookup: both files live side by side under the same rt data directory.
6
12
  */
7
13
  export declare function repoNameForPath(repoPath: string, reposJsonPath?: string): string | null;
@@ -1,14 +1,15 @@
1
1
  /**
2
- * Repo identity: the normalized-remote string that keys `repos.<identity>`
3
- * sections in every settings store (RT-47 spec, "Repo identity").
2
+ * Repo identity: the tagged value that keys `repos.<identity>` sections in
3
+ * every settings store.
4
4
  *
5
- * Identity is `host/path` (lowercase host, path case preserved), derived from
6
- * `remote.origin.url` — never a filesystem path, so it is checkout-location
7
- * independent: every worktree of a repo shares the same remote and therefore
8
- * the same identity. A remote that doesn't match a recognized host form
9
- * (bare local paths are the main case repos.json has two) normalizes to
10
- * null, meaning repo-scoped sections are unreachable for it and only global
11
- * scopes apply. That's an honest degrade, not a crash.
5
+ * A `remote`-kind identity is `host/path` (lowercase host, path case
6
+ * preserved), derived from `remote.origin.url` — checkout-location
7
+ * independent, since every worktree of a repo shares the same remote. When
8
+ * no usable remote exists, identity falls back to `path`-kind: the realpath
9
+ * of the *main* worktree, which is still shared across that repo's linked
10
+ * worktrees (see `deriveRepoIdentity`) even though it is filesystem-bound.
11
+ * `deriveRepoIdentity` therefore never returns null every repo has at
12
+ * least a path-kind identity.
12
13
  *
13
14
  * Three entry points:
14
15
  * - `normalizeRemote` is the pure string transform, no I/O.
@@ -24,6 +25,20 @@
24
25
  * through `identityFromRemote`, memoized per path so repeated callers in
25
26
  * one process don't re-spawn git.
26
27
  */
28
+ export type RepoIdentity = {
29
+ kind: "remote";
30
+ id: string;
31
+ } | {
32
+ kind: "path";
33
+ id: string;
34
+ };
35
+ /**
36
+ * The wire form crosses the daemon socket, sits in board config, and lands in
37
+ * console's `/runs/:repo/...` URL — all of which need one slash-free segment.
38
+ * `encodeURIComponent` guarantees that and is exactly reversible.
39
+ */
40
+ export declare function serializeIdentity(id: RepoIdentity): string;
41
+ export declare function parseIdentity(wire: string): RepoIdentity | null;
27
42
  /**
28
43
  * Pure normalization: `remote` → `host/path` (lowercase host, `.git` and
29
44
  * embedded credentials stripped) or null when the remote doesn't match a
@@ -36,21 +51,30 @@ export declare function normalizeRemote(remote: string): string | null;
36
51
  * Reads the machine store fresh each call (files are small; store reads are
37
52
  * not memoized anywhere in the resolver design).
38
53
  */
39
- export declare function identityFromRemote(remote: string): string | null;
54
+ export declare function identityFromRemote(remote: string): RepoIdentity | null;
40
55
  /**
41
56
  * Async derivation from a repo path: `git -C <repoPath> config --get
42
57
  * remote.origin.url`, then identityFromRemote (so overrides apply to
43
58
  * derivation too). Never a sync spawn — safe to call from daemon contexts.
59
+ * Never returns null: no usable remote falls back to a path-kind identity
60
+ * (the main worktree's realpath, via `git worktree list`, so every linked
61
+ * worktree of one repo still shares the same identity).
44
62
  *
45
- * Only a SUCCESSFUL derivation (non-null identity) is memoized, for the life
46
- * of the process; a remote change after that first success is NOT picked up
47
- * until clearIdentityMemo() — documented behavior, not a bug (see spec:
48
- * derivation is a one-time capture per process, not a live poll). A FAILED
49
- * derivation (no remote yet, git not initialized yet, etc.) is never cached
50
- * and is retried on every subsequent call — a caller racing repo
51
- * provisioning (mid-clone, daemon-startup) must not permanently lose
52
- * identity for a path just because it asked too early.
63
+ * Only a `remote`-kind result is memoized, for the life of the process; a
64
+ * remote change after that first success is NOT picked up until
65
+ * clearIdentityMemo() — documented behavior, not a bug (see spec: derivation
66
+ * is a one-time capture per process, not a live poll). A `path`-kind result
67
+ * is never cached and is retried on every subsequent call cheap to
68
+ * recompute, and a caller racing repo provisioning (mid-clone,
69
+ * daemon-startup, a remote added after the fact) must not permanently lose
70
+ * the chance to pick up a real remote just because it asked too early.
53
71
  */
54
- export declare function deriveRepoIdentity(repoPath: string): Promise<string | null>;
72
+ export declare function deriveRepoIdentity(repoPath: string): Promise<RepoIdentity>;
55
73
  /** Test-only: clear the derivation memo so a test can force re-derivation. */
56
74
  export declare function clearIdentityMemo(): void;
75
+ /**
76
+ * One-shot helper for rewriting board's existing name-valued config to
77
+ * host/path identities. NOT a runtime path — the daemon never calls it.
78
+ * Resolves a repo name to the identity of the path it points at in repos.json.
79
+ */
80
+ export declare function resolveNameToIdentity(name: string, reposJsonPath: string): Promise<RepoIdentity | null>;
@@ -108,3 +108,15 @@ export interface SetSettingOpts {
108
108
  * doc for the full refusal list and the team-selection rule.
109
109
  */
110
110
  export declare function setSetting(key: string, value: unknown, scope: SettingScope, opts?: SetSettingOpts): void;
111
+ /**
112
+ * Removes `key` from the given scope's store, comment-preserving. The refusal
113
+ * ladder is `setSetting`'s minus the value check (there is no value): unknown
114
+ * key, unmigrated, scope not in `def.scopes`, repoIdentity on a non-repoScoped
115
+ * key, and the team-selection rule when ambiguous. Divergences from set, both
116
+ * because removal has nothing to act on: a store FILE that does not exist is a
117
+ * clean no-op rather than a refusal (an explicit `opts.team` naming a team
118
+ * with no local store included — nothing to remove is success, not an error),
119
+ * and a key not present in the store is a no-op. Returns whether anything was
120
+ * actually removed; the local-only reminder prints only on a real removal.
121
+ */
122
+ export declare function unsetSetting(key: string, scope: SettingScope, opts?: SetSettingOpts): boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mattstack/rt-client",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {