@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
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
|
|
106
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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:
|
|
342
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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
|
|
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
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
|
3
|
-
*
|
|
2
|
+
* Repo identity: the tagged value that keys `repos.<identity>` sections in
|
|
3
|
+
* every settings store.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* `remote.origin.url` —
|
|
7
|
-
* independent
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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):
|
|
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
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* and
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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<
|
|
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>;
|
package/dist/settings/write.d.ts
CHANGED
|
@@ -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;
|