@rness/cli 0.12.0 → 0.14.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 +189 -32
- package/dist/bin/rness.js +1 -1
- package/dist/{chunk-AEW7ZDFS.js → chunk-AXTFDXN2.js} +1790 -757
- package/dist/chunk-AXTFDXN2.js.map +1 -0
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunk-AEW7ZDFS.js.map +0 -1
|
@@ -139,6 +139,36 @@ async function changedPaths(dir, paths) {
|
|
|
139
139
|
}
|
|
140
140
|
return changed;
|
|
141
141
|
}
|
|
142
|
+
async function seenPaths(dir, paths) {
|
|
143
|
+
const seen = /* @__PURE__ */ new Set();
|
|
144
|
+
for (const path of new Set(paths)) {
|
|
145
|
+
try {
|
|
146
|
+
const commit = await git(
|
|
147
|
+
[
|
|
148
|
+
"log",
|
|
149
|
+
"-1",
|
|
150
|
+
"--full-history",
|
|
151
|
+
"--format=%H",
|
|
152
|
+
"HEAD",
|
|
153
|
+
"--",
|
|
154
|
+
`:(literal)${path}`
|
|
155
|
+
],
|
|
156
|
+
dir
|
|
157
|
+
);
|
|
158
|
+
if (commit.trim() !== "") seen.add(path);
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return seen;
|
|
163
|
+
}
|
|
164
|
+
async function gitPath(dir, name) {
|
|
165
|
+
try {
|
|
166
|
+
const path = (await git(["rev-parse", "--git-path", name], dir)).trim();
|
|
167
|
+
return path === "" ? null : resolve(dir, path);
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
142
172
|
async function pullFastForward(dir, credentials) {
|
|
143
173
|
await git(["pull", "--ff-only", "--quiet"], dir, credentials);
|
|
144
174
|
}
|
|
@@ -360,9 +390,9 @@ function readScopes(value) {
|
|
|
360
390
|
scopes[name] = { path, extends: ext };
|
|
361
391
|
}
|
|
362
392
|
for (const [name, entry] of Object.entries(scopes)) {
|
|
363
|
-
for (const
|
|
364
|
-
if (!Object.hasOwn(scopes,
|
|
365
|
-
fail(`scope "${name}" extends unknown scope "${
|
|
393
|
+
for (const target2 of entry.extends) {
|
|
394
|
+
if (!Object.hasOwn(scopes, target2))
|
|
395
|
+
fail(`scope "${name}" extends unknown scope "${target2}"`);
|
|
366
396
|
}
|
|
367
397
|
}
|
|
368
398
|
return scopes;
|
|
@@ -456,7 +486,7 @@ function parseRepoSpec(spec, org, host) {
|
|
|
456
486
|
}
|
|
457
487
|
|
|
458
488
|
// src/version.ts
|
|
459
|
-
var VERSION = "0.
|
|
489
|
+
var VERSION = "0.14.0";
|
|
460
490
|
|
|
461
491
|
// src/core/pm.ts
|
|
462
492
|
import { execFile as execFile2 } from "child_process";
|
|
@@ -493,6 +523,12 @@ function rnessRunner(pm, version) {
|
|
|
493
523
|
return "yarn dlx @rness/cli";
|
|
494
524
|
return "npx @rness/cli";
|
|
495
525
|
}
|
|
526
|
+
function localRunner(pm) {
|
|
527
|
+
if (pm === "pnpm") return "pnpm";
|
|
528
|
+
if (pm === "yarn") return "yarn";
|
|
529
|
+
if (pm === "bun") return "bunx";
|
|
530
|
+
return "npx";
|
|
531
|
+
}
|
|
496
532
|
function rnessCommand(env = process.env) {
|
|
497
533
|
const agent = env["npm_config_user_agent"] ?? "";
|
|
498
534
|
if (agent === "") return "rness";
|
|
@@ -599,8 +635,8 @@ async function collectMarkdown(dir) {
|
|
|
599
635
|
}
|
|
600
636
|
|
|
601
637
|
// src/core/scope.ts
|
|
602
|
-
function isPrefix(prefix,
|
|
603
|
-
return prefix ===
|
|
638
|
+
function isPrefix(prefix, target2) {
|
|
639
|
+
return prefix === target2 || target2.startsWith(`${prefix}/`);
|
|
604
640
|
}
|
|
605
641
|
function resolveScope(manifest, relDir) {
|
|
606
642
|
let best = null;
|
|
@@ -624,7 +660,7 @@ function scopeChain(manifest, scope) {
|
|
|
624
660
|
chain.push(name);
|
|
625
661
|
stack.add(name);
|
|
626
662
|
const entry = Object.hasOwn(manifest.scopes, name) ? manifest.scopes[name] : void 0;
|
|
627
|
-
for (const
|
|
663
|
+
for (const target2 of entry?.extends ?? []) visit(target2, stack);
|
|
628
664
|
stack.delete(name);
|
|
629
665
|
};
|
|
630
666
|
visit(scope, /* @__PURE__ */ new Set());
|
|
@@ -1116,6 +1152,158 @@ function statusMarkdown(workspace, tabs) {
|
|
|
1116
1152
|
return out.join("\n");
|
|
1117
1153
|
}
|
|
1118
1154
|
|
|
1155
|
+
// src/pulse/body.ts
|
|
1156
|
+
import { createHash } from "crypto";
|
|
1157
|
+
import { posix as posix4 } from "path";
|
|
1158
|
+
var BODY_LIMIT = 65536;
|
|
1159
|
+
var blobUrl = (org, path) => `https://github.com/${org}/.rness/blob/main/${encodeURI(path)}`;
|
|
1160
|
+
var issueUrl = (org, number) => `https://github.com/${org}/.rness/issues/${number}`;
|
|
1161
|
+
function headerOf(path, org) {
|
|
1162
|
+
return `\`${path}\` \xB7 [on GitHub](${blobUrl(org, path)})`;
|
|
1163
|
+
}
|
|
1164
|
+
var digest = (text) => createHash("sha256").update(text, "utf8").digest("hex").slice(0, 12);
|
|
1165
|
+
var comment = (d) => `<!-- rness ${d} -->`;
|
|
1166
|
+
var TAIL = `
|
|
1167
|
+
|
|
1168
|
+
${comment("0".repeat(12))}`.length;
|
|
1169
|
+
var LAST_LINE = /\n\n<!-- rness ([0-9a-f]{12}) -->\s*$/;
|
|
1170
|
+
function digestOf(body) {
|
|
1171
|
+
const text = body.replace(/\r\n?/g, "\n");
|
|
1172
|
+
const m = LAST_LINE.exec(text);
|
|
1173
|
+
const d = m?.[1];
|
|
1174
|
+
if (m === null || d === void 0) return null;
|
|
1175
|
+
return digest(text.slice(0, m.index)) === d ? d : null;
|
|
1176
|
+
}
|
|
1177
|
+
var OPENER = /^[ \t]*(`{3,}|~{3,})(.*)$/;
|
|
1178
|
+
var CLOSER = /^[ \t]*(`{3,}|~{3,})[ \t]*$/;
|
|
1179
|
+
function fenced(lines) {
|
|
1180
|
+
const code = [];
|
|
1181
|
+
let run3 = null;
|
|
1182
|
+
for (const line3 of lines) {
|
|
1183
|
+
if (run3 === null) {
|
|
1184
|
+
const m = OPENER.exec(line3);
|
|
1185
|
+
const fence2 = m?.[1];
|
|
1186
|
+
if (fence2 !== void 0 && !(fence2.startsWith("`") && (m?.[2] ?? "").includes("`")))
|
|
1187
|
+
run3 = fence2;
|
|
1188
|
+
code.push(run3 !== null);
|
|
1189
|
+
continue;
|
|
1190
|
+
}
|
|
1191
|
+
code.push(true);
|
|
1192
|
+
const fence = CLOSER.exec(line3)?.[1];
|
|
1193
|
+
if (fence !== void 0 && fence[0] === run3[0] && fence.length >= run3.length)
|
|
1194
|
+
run3 = null;
|
|
1195
|
+
}
|
|
1196
|
+
return code;
|
|
1197
|
+
}
|
|
1198
|
+
var TITLE = /^ {0,3}#(?:[ \t]|$)/;
|
|
1199
|
+
function withoutTitle(lines) {
|
|
1200
|
+
const code = fenced(lines);
|
|
1201
|
+
const at = lines.findIndex((line3, i) => code[i] !== true && TITLE.test(line3));
|
|
1202
|
+
if (at < 0) return [...lines];
|
|
1203
|
+
let end = at + 1;
|
|
1204
|
+
while (end < lines.length && (lines[end] ?? "").trim() === "") end++;
|
|
1205
|
+
return [...lines.slice(0, at), ...lines.slice(end)];
|
|
1206
|
+
}
|
|
1207
|
+
var SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
1208
|
+
var IMAGE = /\.(?:png|jpe?g|gif|svg|webp|avif)$/i;
|
|
1209
|
+
var decoded = (path) => {
|
|
1210
|
+
try {
|
|
1211
|
+
return decodeURI(path);
|
|
1212
|
+
} catch {
|
|
1213
|
+
return path;
|
|
1214
|
+
}
|
|
1215
|
+
};
|
|
1216
|
+
function target(dest, s) {
|
|
1217
|
+
const raw = dest.startsWith("<") ? dest.slice(1, -1) : dest;
|
|
1218
|
+
if (raw === "" || SCHEME.test(raw) || raw.startsWith("//") || raw.startsWith("#"))
|
|
1219
|
+
return dest;
|
|
1220
|
+
const hash = raw.indexOf("#");
|
|
1221
|
+
const pathPart = hash < 0 ? raw : raw.slice(0, hash);
|
|
1222
|
+
const anchor = hash < 0 ? "" : raw.slice(hash);
|
|
1223
|
+
const file = decoded(
|
|
1224
|
+
posix4.normalize(
|
|
1225
|
+
pathPart.startsWith("/") ? pathPart.slice(1) : posix4.join(posix4.dirname(s.path), pathPart)
|
|
1226
|
+
)
|
|
1227
|
+
);
|
|
1228
|
+
const number = s.numbers.get(file);
|
|
1229
|
+
if (number !== void 0) return issueUrl(s.org, number);
|
|
1230
|
+
if (!s.files.has(file)) return dest;
|
|
1231
|
+
return `${blobUrl(s.org, file)}${IMAGE.test(file) ? "?raw=true" : ""}${anchor}`;
|
|
1232
|
+
}
|
|
1233
|
+
var PROTECTED = new RegExp(
|
|
1234
|
+
[
|
|
1235
|
+
/(?<!`)(`+)(?!`)[\s\S]*?(?<!`)\1(?!`)/.source,
|
|
1236
|
+
/<!--[\s\S]*?-->/.source,
|
|
1237
|
+
/<[a-zA-Z][a-zA-Z0-9+.-]*:[^\s<>]*>/.source,
|
|
1238
|
+
/^ {0,3}\[(?:[^\]\\\n]|\\.)+\]:[ \t]*(?<def><[^>\n]*>|\S+)/.source,
|
|
1239
|
+
/\]\([ \t]*(?<dest><[^>\n]*>|[^\s()]+)/.source,
|
|
1240
|
+
/\b(?:https?:\/\/|www\.)[^\s<>]*[^\s<>.,:;"')\]]/.source
|
|
1241
|
+
].join("|"),
|
|
1242
|
+
"gm"
|
|
1243
|
+
);
|
|
1244
|
+
var MENTION = /(^|[^A-Za-z0-9`\\])@([A-Za-z0-9][A-Za-z0-9-]*)/g;
|
|
1245
|
+
var REFERENCE = /(^|[^A-Za-z0-9&#/])#(\d+)(?![A-Za-z0-9])/g;
|
|
1246
|
+
var ZWSP = "\u200B";
|
|
1247
|
+
var neutral = (text) => text.replace(MENTION, "$1\\@$2").replace(REFERENCE, `$1#${ZWSP}$2`);
|
|
1248
|
+
function inline(text, s) {
|
|
1249
|
+
let out = "";
|
|
1250
|
+
let from = 0;
|
|
1251
|
+
for (const m of text.matchAll(PROTECTED)) {
|
|
1252
|
+
const at = m.index ?? 0;
|
|
1253
|
+
out += neutral(text.slice(from, at));
|
|
1254
|
+
const dest = m.groups?.["dest"] ?? m.groups?.["def"];
|
|
1255
|
+
out += dest === void 0 ? m[0] : `${m[0].slice(0, m[0].length - dest.length)}${target(dest, s)}`;
|
|
1256
|
+
from = at + m[0].length;
|
|
1257
|
+
}
|
|
1258
|
+
return out + neutral(text.slice(from));
|
|
1259
|
+
}
|
|
1260
|
+
function rewrite(lines, s) {
|
|
1261
|
+
const code = fenced(lines);
|
|
1262
|
+
const out = [];
|
|
1263
|
+
let paragraph = [];
|
|
1264
|
+
const flush = () => {
|
|
1265
|
+
if (paragraph.length > 0) out.push(inline(paragraph.join("\n"), s));
|
|
1266
|
+
paragraph = [];
|
|
1267
|
+
};
|
|
1268
|
+
lines.forEach((line3, i) => {
|
|
1269
|
+
if (code[i] === true || line3.trim() === "") {
|
|
1270
|
+
flush();
|
|
1271
|
+
out.push(line3);
|
|
1272
|
+
} else paragraph.push(line3);
|
|
1273
|
+
});
|
|
1274
|
+
flush();
|
|
1275
|
+
return out;
|
|
1276
|
+
}
|
|
1277
|
+
function cut(text, rest) {
|
|
1278
|
+
const room = BODY_LIMIT - TAIL - 2 - rest.length;
|
|
1279
|
+
const lines = text.split("\n");
|
|
1280
|
+
const code = fenced(lines);
|
|
1281
|
+
let length = -1;
|
|
1282
|
+
let at = 1;
|
|
1283
|
+
for (let k = 1; k < lines.length; k++) {
|
|
1284
|
+
length += 1 + (lines[k - 1] ?? "").length;
|
|
1285
|
+
if (length > room) break;
|
|
1286
|
+
if (code[k] !== true && (lines[k] ?? "").trim() === "") at = k;
|
|
1287
|
+
}
|
|
1288
|
+
return `${lines.slice(0, at).join("\n").trimEnd()}
|
|
1289
|
+
|
|
1290
|
+
${rest}`;
|
|
1291
|
+
}
|
|
1292
|
+
function issueBody(s) {
|
|
1293
|
+
const lines = withoutTitle(
|
|
1294
|
+
stripFrontMatter(s.text.replace(/\r\n?/g, "\n")).split("\n")
|
|
1295
|
+
);
|
|
1296
|
+
const content = rewrite(lines, s).join("\n").replace(/^(?:[ \t]*\n)+/, "").trimEnd();
|
|
1297
|
+
const header2 = headerOf(s.path, s.org);
|
|
1298
|
+
const whole = content === "" ? header2 : `${header2}
|
|
1299
|
+
|
|
1300
|
+
${content}`;
|
|
1301
|
+
const text = whole.length + TAIL <= BODY_LIMIT ? whole : cut(whole, `The rest: ${blobUrl(s.org, s.path)}`);
|
|
1302
|
+
return `${text}
|
|
1303
|
+
|
|
1304
|
+
${comment(digest(text))}`;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1119
1307
|
// src/pulse/layout.ts
|
|
1120
1308
|
var statusOf2 = (status2) => status2 === null || status2 === "?" ? null : status2;
|
|
1121
1309
|
var LIFECYCLE = [
|
|
@@ -1142,8 +1330,8 @@ var STATUS_COLORS = {
|
|
|
1142
1330
|
Accepted: "green",
|
|
1143
1331
|
Implemented: "green",
|
|
1144
1332
|
Completed: "green",
|
|
1145
|
-
Rejected: "
|
|
1146
|
-
Superseded: "
|
|
1333
|
+
Rejected: "red",
|
|
1334
|
+
Superseded: "yellow",
|
|
1147
1335
|
Abandoned: "gray"
|
|
1148
1336
|
};
|
|
1149
1337
|
var COLLECTION_COLORS = {
|
|
@@ -1208,9 +1396,7 @@ function desiredOf(tabs, org) {
|
|
|
1208
1396
|
(tab) => tab.rows.map((row) => ({
|
|
1209
1397
|
path: row.path,
|
|
1210
1398
|
title: `${row.id} \u2014 ${row.title}`,
|
|
1211
|
-
body:
|
|
1212
|
-
|
|
1213
|
-
https://github.com/${org}/.rness/blob/main/${row.path}`,
|
|
1399
|
+
body: headerOf(row.path, org),
|
|
1214
1400
|
status: statusOf2(row.status),
|
|
1215
1401
|
type: tab.label,
|
|
1216
1402
|
statusField: fields2.get(tab.label)?.name ?? null
|
|
@@ -1219,6 +1405,7 @@ https://github.com/${org}/.rness/blob/main/${row.path}`,
|
|
|
1219
1405
|
}
|
|
1220
1406
|
|
|
1221
1407
|
// src/core/github.ts
|
|
1408
|
+
import { setTimeout as delay } from "timers/promises";
|
|
1222
1409
|
var DEFAULT_GITHUB_API = "https://api.github.com";
|
|
1223
1410
|
var MAX_PAGES = 50;
|
|
1224
1411
|
var PER_PAGE = 100;
|
|
@@ -1353,6 +1540,39 @@ async function listRepositories(owner, options) {
|
|
|
1353
1540
|
truncated: true
|
|
1354
1541
|
};
|
|
1355
1542
|
}
|
|
1543
|
+
var WAIT_BUDGET_MS = 6e5;
|
|
1544
|
+
var DEFAULT_WAIT_MS = 6e4;
|
|
1545
|
+
var MIN_WAIT_MS = 1e3;
|
|
1546
|
+
var RATE_LIMIT_OUTLASTED = "GitHub's rate limit outlasted the 10 minutes rness waits";
|
|
1547
|
+
function rateWait(sleep = (ms) => delay(ms)) {
|
|
1548
|
+
return { left: WAIT_BUDGET_MS, now: () => Date.now(), sleep };
|
|
1549
|
+
}
|
|
1550
|
+
var PROCESS_WAIT = rateWait();
|
|
1551
|
+
var RateLimitError = class extends Error {
|
|
1552
|
+
};
|
|
1553
|
+
var messageOf = (body) => {
|
|
1554
|
+
const m = body !== null && typeof body === "object" ? body["message"] : void 0;
|
|
1555
|
+
return typeof m === "string" ? m : "";
|
|
1556
|
+
};
|
|
1557
|
+
function rateDelay(page, now, limited = false) {
|
|
1558
|
+
const h = page.headers;
|
|
1559
|
+
const spent = h.get("x-ratelimit-remaining") === "0";
|
|
1560
|
+
const hit = limited || page.status === 429 || page.status === 403 && (h.has("retry-after") || spent || /rate limit/i.test(messageOf(page.body)));
|
|
1561
|
+
if (!hit) return null;
|
|
1562
|
+
const after = (h.get("retry-after") ?? "").trim();
|
|
1563
|
+
if (/^\d+$/.test(after)) return Number(after) * 1e3;
|
|
1564
|
+
const reset = (h.get("x-ratelimit-reset") ?? "").trim();
|
|
1565
|
+
if (spent && /^\d+$/.test(reset))
|
|
1566
|
+
return Math.max(0, Number(reset) * 1e3 - now);
|
|
1567
|
+
return DEFAULT_WAIT_MS;
|
|
1568
|
+
}
|
|
1569
|
+
async function waitOut(options, ms) {
|
|
1570
|
+
const wait = options.wait ?? PROCESS_WAIT;
|
|
1571
|
+
const spent = Math.max(ms, MIN_WAIT_MS);
|
|
1572
|
+
if (spent > wait.left) throw new RateLimitError(RATE_LIMIT_OUTLASTED);
|
|
1573
|
+
wait.left -= spent;
|
|
1574
|
+
await wait.sleep(spent);
|
|
1575
|
+
}
|
|
1356
1576
|
function apiHeaders(token) {
|
|
1357
1577
|
return {
|
|
1358
1578
|
Accept: "application/vnd.github+json",
|
|
@@ -1375,23 +1595,34 @@ async function getJson(path, options) {
|
|
|
1375
1595
|
const body = await res.json().catch(() => void 0);
|
|
1376
1596
|
return { status: res.status, body };
|
|
1377
1597
|
}
|
|
1378
|
-
async function postJson(path, payload, options, explain = false) {
|
|
1598
|
+
async function postJson(path, payload, options, explain = false, limited = () => false) {
|
|
1379
1599
|
const base = (options.apiBase ?? DEFAULT_GITHUB_API).replace(/\/+$/, "");
|
|
1380
|
-
const
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1600
|
+
const clock = options.wait ?? PROCESS_WAIT;
|
|
1601
|
+
for (; ; ) {
|
|
1602
|
+
const page = await getPage(
|
|
1603
|
+
`${base}${path}`,
|
|
1604
|
+
{ ...apiHeaders(options.token), "Content-Type": "application/json" },
|
|
1605
|
+
options.timeoutMs ?? 15e3,
|
|
1606
|
+
// A refusal's body says whether it is a rate limit. A timeout throws
|
|
1607
|
+
// above and is never retried: the write may have been made.
|
|
1608
|
+
{ method: "POST", body: JSON.stringify(payload), errorBody: true }
|
|
1609
|
+
);
|
|
1610
|
+
const ok = page.status >= 200 && page.status < 300;
|
|
1611
|
+
const wait = rateDelay(page, clock.now(), ok && limited(page.body));
|
|
1612
|
+
if (wait !== null) {
|
|
1613
|
+
await waitOut(options, wait);
|
|
1614
|
+
continue;
|
|
1615
|
+
}
|
|
1616
|
+
if (!ok) {
|
|
1617
|
+
const message2 = explain && page.status !== 401 && page.status !== 429 && page.body !== null && typeof page.body === "object" ? page.body.message : void 0;
|
|
1618
|
+
if (typeof message2 === "string" && message2 !== "")
|
|
1619
|
+
throw new GitHubMessageError(message2, {
|
|
1620
|
+
cause: { status: page.status, path }
|
|
1621
|
+
});
|
|
1622
|
+
throw httpError(page, path);
|
|
1623
|
+
}
|
|
1624
|
+
return page.body;
|
|
1393
1625
|
}
|
|
1394
|
-
return page.body;
|
|
1395
1626
|
}
|
|
1396
1627
|
async function getScopes(options) {
|
|
1397
1628
|
const base = (options.apiBase ?? DEFAULT_GITHUB_API).replace(/\/+$/, "");
|
|
@@ -1489,8 +1720,17 @@ async function createRepository(owner, name, options) {
|
|
|
1489
1720
|
function isRecord2(v) {
|
|
1490
1721
|
return v !== null && typeof v === "object";
|
|
1491
1722
|
}
|
|
1723
|
+
var rateLimited = (body) => isRecord2(body) && Array.isArray(body["errors"]) && body["errors"].some(
|
|
1724
|
+
(e) => isRecord2(e) && e["type"] === "RATE_LIMITED"
|
|
1725
|
+
);
|
|
1492
1726
|
async function graphqlRaw(query, variables, options) {
|
|
1493
|
-
const body = await postJson(
|
|
1727
|
+
const body = await postJson(
|
|
1728
|
+
"/graphql",
|
|
1729
|
+
{ query, variables },
|
|
1730
|
+
options,
|
|
1731
|
+
true,
|
|
1732
|
+
rateLimited
|
|
1733
|
+
);
|
|
1494
1734
|
return isRecord2(body) ? body : {};
|
|
1495
1735
|
}
|
|
1496
1736
|
function firstError(answer) {
|
|
@@ -1505,6 +1745,15 @@ async function graphql(query, variables, options) {
|
|
|
1505
1745
|
throw new Error("GitHub GraphQL answered without data");
|
|
1506
1746
|
return answer.data;
|
|
1507
1747
|
}
|
|
1748
|
+
async function graphqlFound(query, variables, options) {
|
|
1749
|
+
const answer = await graphqlRaw(query, variables, options);
|
|
1750
|
+
const errors = answer.errors ?? [];
|
|
1751
|
+
if (errors.length > 0 && errors.every((e) => e.type === "NOT_FOUND"))
|
|
1752
|
+
return null;
|
|
1753
|
+
const error = firstError(answer);
|
|
1754
|
+
if (error !== null) throw error;
|
|
1755
|
+
return answer.data ?? null;
|
|
1756
|
+
}
|
|
1508
1757
|
async function orgId(org, o) {
|
|
1509
1758
|
const d = await graphql(
|
|
1510
1759
|
"query($login: String!) { organization(login: $login) { id } }",
|
|
@@ -1541,7 +1790,9 @@ async function findProject(org, number, o) {
|
|
|
1541
1790
|
{ login: org, number },
|
|
1542
1791
|
o
|
|
1543
1792
|
);
|
|
1544
|
-
|
|
1793
|
+
const errors = answer.errors ?? [];
|
|
1794
|
+
if (errors.length > 0 && errors.every((e) => e.type === "NOT_FOUND"))
|
|
1795
|
+
return null;
|
|
1545
1796
|
const error = firstError(answer);
|
|
1546
1797
|
if (error !== null) throw error;
|
|
1547
1798
|
return answer.data?.organization?.projectV2 ?? null;
|
|
@@ -1621,7 +1872,7 @@ async function setOptions(field, options, o) {
|
|
|
1621
1872
|
);
|
|
1622
1873
|
return toField(d.updateProjectV2Field.projectV2Field);
|
|
1623
1874
|
}
|
|
1624
|
-
async function listItems(projectId, o) {
|
|
1875
|
+
async function listItems(projectId, o, { bodies = true } = {}) {
|
|
1625
1876
|
const items = [];
|
|
1626
1877
|
let cursor = null;
|
|
1627
1878
|
for (; ; ) {
|
|
@@ -1639,8 +1890,23 @@ async function listItems(projectId, o) {
|
|
|
1639
1890
|
id
|
|
1640
1891
|
title
|
|
1641
1892
|
}
|
|
1893
|
+
... on Issue {
|
|
1894
|
+
id
|
|
1895
|
+
number
|
|
1896
|
+
title
|
|
1897
|
+
state
|
|
1898
|
+
${bodies ? "body" : ""}
|
|
1899
|
+
repository {
|
|
1900
|
+
nameWithOwner
|
|
1901
|
+
}
|
|
1902
|
+
labels(first: 20) {
|
|
1903
|
+
nodes {
|
|
1904
|
+
name
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1642
1908
|
}
|
|
1643
|
-
fieldValues(first:
|
|
1909
|
+
fieldValues(first: 50) {
|
|
1644
1910
|
nodes {
|
|
1645
1911
|
... on ProjectV2ItemFieldTextValue {
|
|
1646
1912
|
text
|
|
@@ -1680,10 +1946,20 @@ async function listItems(projectId, o) {
|
|
|
1680
1946
|
const value = v.text ?? v.name;
|
|
1681
1947
|
if (field !== void 0 && value !== void 0) values2[field] = value;
|
|
1682
1948
|
}
|
|
1949
|
+
const c = n.content;
|
|
1950
|
+
const issue = c?.id !== void 0 && c.number !== void 0 ? {
|
|
1951
|
+
id: c.id,
|
|
1952
|
+
number: c.number,
|
|
1953
|
+
open: c.state === "OPEN",
|
|
1954
|
+
repository: c.repository?.nameWithOwner ?? "",
|
|
1955
|
+
body: bodies ? c.body ?? "" : null,
|
|
1956
|
+
labels: (c.labels?.nodes ?? []).map((l) => l.name ?? "").filter((l) => l !== "")
|
|
1957
|
+
} : null;
|
|
1683
1958
|
items.push({
|
|
1684
1959
|
id: n.id,
|
|
1685
|
-
draftId:
|
|
1686
|
-
|
|
1960
|
+
draftId: issue === null ? c?.id ?? null : null,
|
|
1961
|
+
issue,
|
|
1962
|
+
title: c?.title ?? "",
|
|
1687
1963
|
archived: n.isArchived === true,
|
|
1688
1964
|
values: values2
|
|
1689
1965
|
});
|
|
@@ -1693,41 +1969,6 @@ async function listItems(projectId, o) {
|
|
|
1693
1969
|
cursor = endCursor;
|
|
1694
1970
|
}
|
|
1695
1971
|
}
|
|
1696
|
-
async function addDraft(projectId, title, body, o) {
|
|
1697
|
-
const d = await graphql(
|
|
1698
|
-
`
|
|
1699
|
-
mutation ($projectId: ID!, $title: String!, $body: String) {
|
|
1700
|
-
addProjectV2DraftIssue(
|
|
1701
|
-
input: { projectId: $projectId, title: $title, body: $body }
|
|
1702
|
-
) {
|
|
1703
|
-
projectItem {
|
|
1704
|
-
id
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
}
|
|
1708
|
-
`,
|
|
1709
|
-
{ projectId, title, body },
|
|
1710
|
-
o
|
|
1711
|
-
);
|
|
1712
|
-
return d.addProjectV2DraftIssue.projectItem.id;
|
|
1713
|
-
}
|
|
1714
|
-
async function editDraft(draftId, title, body, o) {
|
|
1715
|
-
await graphql(
|
|
1716
|
-
`
|
|
1717
|
-
mutation ($draftIssueId: ID!, $title: String!, $body: String) {
|
|
1718
|
-
updateProjectV2DraftIssue(
|
|
1719
|
-
input: { draftIssueId: $draftIssueId, title: $title, body: $body }
|
|
1720
|
-
) {
|
|
1721
|
-
draftIssue {
|
|
1722
|
-
id
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
}
|
|
1726
|
-
`,
|
|
1727
|
-
{ draftIssueId: draftId, title, body },
|
|
1728
|
-
o
|
|
1729
|
-
);
|
|
1730
|
-
}
|
|
1731
1972
|
async function setValue(projectId, itemId, fieldId, value, o) {
|
|
1732
1973
|
if (value === null) {
|
|
1733
1974
|
await graphql(
|
|
@@ -1795,6 +2036,90 @@ async function archive(projectId, itemId, o) {
|
|
|
1795
2036
|
o
|
|
1796
2037
|
);
|
|
1797
2038
|
}
|
|
2039
|
+
async function addItem(projectId, contentId, o) {
|
|
2040
|
+
const d = await graphql(
|
|
2041
|
+
`
|
|
2042
|
+
mutation ($projectId: ID!, $contentId: ID!) {
|
|
2043
|
+
addProjectV2ItemById(
|
|
2044
|
+
input: { projectId: $projectId, contentId: $contentId }
|
|
2045
|
+
) {
|
|
2046
|
+
item {
|
|
2047
|
+
id
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2051
|
+
`,
|
|
2052
|
+
{ projectId, contentId },
|
|
2053
|
+
o
|
|
2054
|
+
);
|
|
2055
|
+
return d.addProjectV2ItemById.item.id;
|
|
2056
|
+
}
|
|
2057
|
+
async function convertDraft(itemId, repositoryId, o) {
|
|
2058
|
+
const d = await graphql(
|
|
2059
|
+
`
|
|
2060
|
+
mutation ($itemId: ID!, $repositoryId: ID!) {
|
|
2061
|
+
convertProjectV2DraftIssueItemToIssue(
|
|
2062
|
+
input: { itemId: $itemId, repositoryId: $repositoryId }
|
|
2063
|
+
) {
|
|
2064
|
+
item {
|
|
2065
|
+
id
|
|
2066
|
+
content {
|
|
2067
|
+
... on Issue {
|
|
2068
|
+
id
|
|
2069
|
+
number
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
`,
|
|
2076
|
+
{ itemId, repositoryId },
|
|
2077
|
+
o
|
|
2078
|
+
);
|
|
2079
|
+
const content = d.convertProjectV2DraftIssueItemToIssue.item.content;
|
|
2080
|
+
const id = content?.id;
|
|
2081
|
+
const number = content?.number;
|
|
2082
|
+
if (id === void 0 || number === void 0)
|
|
2083
|
+
throw new Error(`GitHub converted the item ${itemId} without an issue`);
|
|
2084
|
+
return { id, number };
|
|
2085
|
+
}
|
|
2086
|
+
async function linkedRepositories(projectId, o) {
|
|
2087
|
+
const d = await graphql(
|
|
2088
|
+
`
|
|
2089
|
+
query ($projectId: ID!) {
|
|
2090
|
+
node(id: $projectId) {
|
|
2091
|
+
... on ProjectV2 {
|
|
2092
|
+
repositories(first: 100) {
|
|
2093
|
+
nodes {
|
|
2094
|
+
nameWithOwner
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
}
|
|
2100
|
+
`,
|
|
2101
|
+
{ projectId },
|
|
2102
|
+
o
|
|
2103
|
+
);
|
|
2104
|
+
return (d.node?.repositories.nodes ?? []).map((r) => r.nameWithOwner);
|
|
2105
|
+
}
|
|
2106
|
+
async function linkRepository(projectId, repositoryId, o) {
|
|
2107
|
+
await graphql(
|
|
2108
|
+
`
|
|
2109
|
+
mutation ($projectId: ID!, $repositoryId: ID!) {
|
|
2110
|
+
linkProjectV2ToRepository(
|
|
2111
|
+
input: { projectId: $projectId, repositoryId: $repositoryId }
|
|
2112
|
+
) {
|
|
2113
|
+
repository {
|
|
2114
|
+
id
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
}
|
|
2118
|
+
`,
|
|
2119
|
+
{ projectId, repositoryId },
|
|
2120
|
+
o
|
|
2121
|
+
);
|
|
2122
|
+
}
|
|
1798
2123
|
async function createView(org, number, view, o) {
|
|
1799
2124
|
const body = {
|
|
1800
2125
|
name: view.name,
|
|
@@ -1889,10 +2214,127 @@ async function renameView(viewId, name, visibleFieldIds, o) {
|
|
|
1889
2214
|
);
|
|
1890
2215
|
}
|
|
1891
2216
|
|
|
2217
|
+
// src/core/github-issues.ts
|
|
2218
|
+
async function repository(owner, name, label, o) {
|
|
2219
|
+
const d = await graphqlFound(
|
|
2220
|
+
`query($owner: String!, $name: String!, $label: String!) {
|
|
2221
|
+
repository(owner: $owner, name: $name) { id hasIssuesEnabled label(name: $label) { id } }
|
|
2222
|
+
}`,
|
|
2223
|
+
{ owner, name, label },
|
|
2224
|
+
o
|
|
2225
|
+
);
|
|
2226
|
+
const r = d?.repository ?? null;
|
|
2227
|
+
return r === null ? null : { id: r.id, issues: r.hasIssuesEnabled, labelId: r.label?.id ?? null };
|
|
2228
|
+
}
|
|
2229
|
+
async function createLabel(owner, name, label, o) {
|
|
2230
|
+
const body = await postJson(
|
|
2231
|
+
`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}/labels`,
|
|
2232
|
+
label,
|
|
2233
|
+
o,
|
|
2234
|
+
true
|
|
2235
|
+
);
|
|
2236
|
+
const id = body !== null && typeof body === "object" ? body["node_id"] : void 0;
|
|
2237
|
+
if (typeof id !== "string")
|
|
2238
|
+
throw new Error(`GitHub created the label ${label.name} without a node id`);
|
|
2239
|
+
return id;
|
|
2240
|
+
}
|
|
2241
|
+
async function createIssue(repositoryId, issue, o) {
|
|
2242
|
+
const d = await graphql(
|
|
2243
|
+
`
|
|
2244
|
+
mutation (
|
|
2245
|
+
$repositoryId: ID!
|
|
2246
|
+
$title: String!
|
|
2247
|
+
$body: String
|
|
2248
|
+
$labelIds: [ID!]
|
|
2249
|
+
) {
|
|
2250
|
+
createIssue(
|
|
2251
|
+
input: {
|
|
2252
|
+
repositoryId: $repositoryId
|
|
2253
|
+
title: $title
|
|
2254
|
+
body: $body
|
|
2255
|
+
labelIds: $labelIds
|
|
2256
|
+
}
|
|
2257
|
+
) {
|
|
2258
|
+
issue {
|
|
2259
|
+
id
|
|
2260
|
+
number
|
|
2261
|
+
}
|
|
2262
|
+
}
|
|
2263
|
+
}
|
|
2264
|
+
`,
|
|
2265
|
+
{ repositoryId, ...issue },
|
|
2266
|
+
o
|
|
2267
|
+
);
|
|
2268
|
+
return d.createIssue.issue;
|
|
2269
|
+
}
|
|
2270
|
+
async function updateIssue(id, change, o) {
|
|
2271
|
+
await graphql(
|
|
2272
|
+
`
|
|
2273
|
+
mutation ($id: ID!, $title: String, $body: String) {
|
|
2274
|
+
updateIssue(input: { id: $id, title: $title, body: $body }) {
|
|
2275
|
+
issue {
|
|
2276
|
+
id
|
|
2277
|
+
}
|
|
2278
|
+
}
|
|
2279
|
+
}
|
|
2280
|
+
`,
|
|
2281
|
+
{ id, ...change },
|
|
2282
|
+
o
|
|
2283
|
+
);
|
|
2284
|
+
}
|
|
2285
|
+
async function closeIssue(id, o) {
|
|
2286
|
+
await graphql(
|
|
2287
|
+
`
|
|
2288
|
+
mutation ($id: ID!) {
|
|
2289
|
+
closeIssue(input: { issueId: $id, stateReason: NOT_PLANNED }) {
|
|
2290
|
+
issue {
|
|
2291
|
+
id
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
`,
|
|
2296
|
+
{ id },
|
|
2297
|
+
o
|
|
2298
|
+
);
|
|
2299
|
+
}
|
|
2300
|
+
async function reopenIssue(id, o) {
|
|
2301
|
+
await graphql(
|
|
2302
|
+
`
|
|
2303
|
+
mutation ($id: ID!) {
|
|
2304
|
+
reopenIssue(input: { issueId: $id }) {
|
|
2305
|
+
issue {
|
|
2306
|
+
id
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
`,
|
|
2311
|
+
{ id },
|
|
2312
|
+
o
|
|
2313
|
+
);
|
|
2314
|
+
}
|
|
2315
|
+
async function addLabels(id, labelIds, o) {
|
|
2316
|
+
await graphql(
|
|
2317
|
+
`
|
|
2318
|
+
mutation ($id: ID!, $labelIds: [ID!]!) {
|
|
2319
|
+
addLabelsToLabelable(input: { labelableId: $id, labelIds: $labelIds }) {
|
|
2320
|
+
clientMutationId
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
`,
|
|
2324
|
+
{ id, labelIds },
|
|
2325
|
+
o
|
|
2326
|
+
);
|
|
2327
|
+
}
|
|
2328
|
+
|
|
1892
2329
|
// src/core/github-board.ts
|
|
1893
2330
|
var PROJECT_TITLE = "Agent Pulse";
|
|
1894
2331
|
var WORKING = "working";
|
|
1895
2332
|
var WORKING_VIEW = "Working";
|
|
2333
|
+
var MEMORY = ".rness";
|
|
2334
|
+
var LABEL = "rness";
|
|
2335
|
+
var LABEL_COLOR = "5319e7";
|
|
2336
|
+
var LABEL_DESCRIPTION = "A document of .rness, on Agent Pulse";
|
|
2337
|
+
var labelled = (labels) => labels.some((l) => l.toLowerCase() === LABEL.toLowerCase());
|
|
1896
2338
|
var STATUS_FIELD = "Status";
|
|
1897
2339
|
var ALL_VIEW = "All";
|
|
1898
2340
|
var DEFAULT_VIEW = "View 1";
|
|
@@ -1921,6 +2363,8 @@ var orderedFirst = (wanted, all) => {
|
|
|
1921
2363
|
var GitHubBoards = class {
|
|
1922
2364
|
#o;
|
|
1923
2365
|
#cache = /* @__PURE__ */ new Map();
|
|
2366
|
+
/** Each organization's `.rness`, as first read: its id, its Issues, its label. */
|
|
2367
|
+
#memories = /* @__PURE__ */ new Map();
|
|
1924
2368
|
constructor(options) {
|
|
1925
2369
|
this.#o = options;
|
|
1926
2370
|
}
|
|
@@ -1943,6 +2387,23 @@ var GitHubBoards = class {
|
|
|
1943
2387
|
cache.fields ??= await fields(cache.projectId, this.#o);
|
|
1944
2388
|
return cache.fields;
|
|
1945
2389
|
}
|
|
2390
|
+
async #memory(org) {
|
|
2391
|
+
const known = this.#memories.get(org);
|
|
2392
|
+
if (known !== void 0) return known;
|
|
2393
|
+
const found = await repository(org, MEMORY, LABEL, this.#o);
|
|
2394
|
+
if (found === null)
|
|
2395
|
+
throw new Error(
|
|
2396
|
+
`the pulse needs ${org}/${MEMORY} on GitHub: it is not there, or this login cannot see it`
|
|
2397
|
+
);
|
|
2398
|
+
this.#memories.set(org, found);
|
|
2399
|
+
return found;
|
|
2400
|
+
}
|
|
2401
|
+
async checkIssues(org) {
|
|
2402
|
+
if (!(await this.#memory(org)).issues)
|
|
2403
|
+
throw new Error(
|
|
2404
|
+
`the pulse needs Issues on ${org}/${MEMORY}: turn them on in its Settings`
|
|
2405
|
+
);
|
|
2406
|
+
}
|
|
1946
2407
|
/** The project only: the caller declares it before `ensureLayout` builds it. */
|
|
1947
2408
|
async createBoard(org) {
|
|
1948
2409
|
const project = await createProject(
|
|
@@ -2079,89 +2540,186 @@ var GitHubBoards = class {
|
|
|
2079
2540
|
this.#o
|
|
2080
2541
|
);
|
|
2081
2542
|
}
|
|
2543
|
+
const memory = await this.#memory(board.org);
|
|
2544
|
+
if (memory.labelId === null) {
|
|
2545
|
+
memory.labelId = await createLabel(
|
|
2546
|
+
board.org,
|
|
2547
|
+
MEMORY,
|
|
2548
|
+
{ name: LABEL, color: LABEL_COLOR, description: LABEL_DESCRIPTION },
|
|
2549
|
+
this.#o
|
|
2550
|
+
);
|
|
2551
|
+
added.push(`label ${LABEL}`);
|
|
2552
|
+
}
|
|
2553
|
+
const repository2 = `${board.org}/${MEMORY}`;
|
|
2554
|
+
const linked = await linkedRepositories(cache.projectId, this.#o);
|
|
2555
|
+
if (!linked.some((r) => r.toLowerCase() === repository2.toLowerCase())) {
|
|
2556
|
+
await linkRepository(cache.projectId, memory.id, this.#o);
|
|
2557
|
+
added.push(`link ${repository2}`);
|
|
2558
|
+
}
|
|
2082
2559
|
cache.fields = [status2, collection, ...own.values(), agent, session, path];
|
|
2083
2560
|
return added;
|
|
2084
2561
|
}
|
|
2085
|
-
async items(board) {
|
|
2562
|
+
async items(board, options = {}) {
|
|
2086
2563
|
const cache = await this.#cacheOf(board);
|
|
2087
|
-
const raw = await listItems(cache.projectId, this.#o);
|
|
2564
|
+
const raw = await listItems(cache.projectId, this.#o, options);
|
|
2088
2565
|
cache.known = new Map(raw.map((r) => [r.id, r]));
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2566
|
+
const memory = `${board.org}/${MEMORY}`.toLowerCase();
|
|
2567
|
+
return raw.filter((r) => !r.archived).map((r) => {
|
|
2568
|
+
const issue = r.issue !== null && r.issue.repository.toLowerCase() === memory ? r.issue : null;
|
|
2569
|
+
const ours = r.draftId !== null || issue !== null;
|
|
2570
|
+
return {
|
|
2571
|
+
id: r.id,
|
|
2572
|
+
path: ours ? r.values["Path"] ?? null : null,
|
|
2573
|
+
issue: issue === null ? null : {
|
|
2574
|
+
number: issue.number,
|
|
2575
|
+
open: issue.open,
|
|
2576
|
+
labelled: labelled(issue.labels),
|
|
2577
|
+
body: issue.body
|
|
2578
|
+
},
|
|
2579
|
+
title: r.title,
|
|
2580
|
+
status: r.values[STATUS_FIELD] ?? null,
|
|
2581
|
+
collectionStatus: r.values[statusFieldName(r.values[COLLECTION_FIELD] ?? "")] ?? null,
|
|
2582
|
+
type: r.values[COLLECTION_FIELD] ?? null,
|
|
2583
|
+
agent: r.values["Agent"] ?? null,
|
|
2584
|
+
session: r.values["Session"] ?? null
|
|
2585
|
+
};
|
|
2586
|
+
});
|
|
2101
2587
|
}
|
|
2102
2588
|
async #set(board, itemId, name, value) {
|
|
2103
2589
|
const cache = await this.#cacheOf(board);
|
|
2104
2590
|
const field = (await this.#fields(board)).find((f) => f.name === name);
|
|
2105
2591
|
if (field === void 0) throw new Error(`the board has no field ${name}`);
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
itemId,
|
|
2114
|
-
field.id,
|
|
2115
|
-
{ text: value },
|
|
2116
|
-
this.#o
|
|
2117
|
-
);
|
|
2118
|
-
return;
|
|
2592
|
+
let sent = null;
|
|
2593
|
+
if (value !== null && field.options === null) sent = { text: value };
|
|
2594
|
+
else if (value !== null) {
|
|
2595
|
+
const option = field.options?.find((opt) => opt.name === value);
|
|
2596
|
+
if (option === void 0)
|
|
2597
|
+
throw new Error(`the field ${name} has no option ${value}`);
|
|
2598
|
+
sent = { optionId: option.id };
|
|
2119
2599
|
}
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
itemId,
|
|
2126
|
-
field.id,
|
|
2127
|
-
{ optionId: option.id },
|
|
2128
|
-
this.#o
|
|
2129
|
-
);
|
|
2600
|
+
await setValue(cache.projectId, itemId, field.id, sent, this.#o);
|
|
2601
|
+
const known = cache.known.get(itemId);
|
|
2602
|
+
if (known === void 0) return;
|
|
2603
|
+
if (value === null) delete known.values[name];
|
|
2604
|
+
else known.values[name] = value;
|
|
2130
2605
|
}
|
|
2131
2606
|
async apply(board, step2) {
|
|
2132
|
-
if (step2.kind === "unchanged") return;
|
|
2607
|
+
if (step2.kind === "unchanged") return null;
|
|
2133
2608
|
const cache = await this.#cacheOf(board);
|
|
2134
|
-
if (step2.kind === "
|
|
2609
|
+
if (step2.kind === "create") return this.#create(board, cache, step2.want);
|
|
2610
|
+
const known = await this.#known(board, step2.id);
|
|
2611
|
+
if (step2.kind === "archive" || step2.kind === "close") {
|
|
2612
|
+
if (step2.kind === "close" && known.issue?.open === true)
|
|
2613
|
+
await closeIssue(known.issue.id, this.#o);
|
|
2135
2614
|
await archive(cache.projectId, step2.id, this.#o);
|
|
2136
2615
|
cache.known.delete(step2.id);
|
|
2137
|
-
return;
|
|
2616
|
+
return null;
|
|
2617
|
+
}
|
|
2618
|
+
if (step2.kind === "body") {
|
|
2619
|
+
const issue2 = this.#issueOf(known);
|
|
2620
|
+
await updateIssue(issue2.id, { body: step2.body }, this.#o);
|
|
2621
|
+
issue2.body = step2.body;
|
|
2622
|
+
return null;
|
|
2623
|
+
}
|
|
2624
|
+
const labelId = await this.#labelId(board);
|
|
2625
|
+
if (step2.kind === "convert") {
|
|
2626
|
+
const made = await convertDraft(
|
|
2627
|
+
step2.id,
|
|
2628
|
+
(await this.#memory(board.org)).id,
|
|
2629
|
+
this.#o
|
|
2630
|
+
);
|
|
2631
|
+
known.draftId = null;
|
|
2632
|
+
known.issue = {
|
|
2633
|
+
...made,
|
|
2634
|
+
open: true,
|
|
2635
|
+
repository: `${board.org}/${MEMORY}`,
|
|
2636
|
+
body: "",
|
|
2637
|
+
labels: []
|
|
2638
|
+
};
|
|
2639
|
+
await this.#issue(known, step2.want, labelId);
|
|
2640
|
+
await this.#values(board, step2.id, step2.want);
|
|
2641
|
+
return { id: step2.id, number: made.number };
|
|
2642
|
+
}
|
|
2643
|
+
const issue = this.#issueOf(known);
|
|
2644
|
+
if (step2.reopen) {
|
|
2645
|
+
await reopenIssue(issue.id, this.#o);
|
|
2646
|
+
issue.open = true;
|
|
2647
|
+
}
|
|
2648
|
+
await this.#issue(known, step2.want, labelId);
|
|
2649
|
+
await this.#values(board, step2.id, step2.want);
|
|
2650
|
+
return null;
|
|
2651
|
+
}
|
|
2652
|
+
/** A new document's issue: labelled, the first line as body, added to the board, Path first. */
|
|
2653
|
+
async #create(board, cache, want) {
|
|
2654
|
+
const memory = await this.#memory(board.org);
|
|
2655
|
+
const labelId = await this.#labelId(board);
|
|
2656
|
+
const made = await createIssue(
|
|
2657
|
+
memory.id,
|
|
2658
|
+
{ title: want.title, body: want.body, labelIds: [labelId] },
|
|
2659
|
+
this.#o
|
|
2660
|
+
);
|
|
2661
|
+
const id = await addItem(cache.projectId, made.id, this.#o);
|
|
2662
|
+
cache.known.set(id, {
|
|
2663
|
+
id,
|
|
2664
|
+
draftId: null,
|
|
2665
|
+
issue: {
|
|
2666
|
+
...made,
|
|
2667
|
+
open: true,
|
|
2668
|
+
repository: `${board.org}/${MEMORY}`,
|
|
2669
|
+
body: want.body,
|
|
2670
|
+
labels: [LABEL]
|
|
2671
|
+
},
|
|
2672
|
+
title: want.title,
|
|
2673
|
+
archived: false,
|
|
2674
|
+
values: {}
|
|
2675
|
+
});
|
|
2676
|
+
await this.#values(board, id, want);
|
|
2677
|
+
return { id, number: made.number };
|
|
2678
|
+
}
|
|
2679
|
+
/** The label and the title an issue of the board carries. */
|
|
2680
|
+
async #issue(known, want, labelId) {
|
|
2681
|
+
const issue = this.#issueOf(known);
|
|
2682
|
+
if (!labelled(issue.labels)) {
|
|
2683
|
+
await addLabels(issue.id, [labelId], this.#o);
|
|
2684
|
+
issue.labels.push(LABEL);
|
|
2685
|
+
}
|
|
2686
|
+
if (known.title !== want.title) {
|
|
2687
|
+
await updateIssue(issue.id, { title: want.title }, this.#o);
|
|
2688
|
+
known.title = want.title;
|
|
2138
2689
|
}
|
|
2139
|
-
|
|
2140
|
-
|
|
2690
|
+
}
|
|
2691
|
+
/** Path first — an issue on the board without it is the team's — then the fields, each only when it differs. */
|
|
2692
|
+
async #values(board, id, want) {
|
|
2693
|
+
const values2 = (await this.#known(board, id)).values;
|
|
2694
|
+
const wanted = [
|
|
2141
2695
|
["Path", want.path],
|
|
2142
2696
|
[COLLECTION_FIELD, want.type],
|
|
2143
2697
|
[STATUS_FIELD, want.status],
|
|
2144
2698
|
...want.statusField === null ? [] : [[want.statusField, want.status]]
|
|
2145
2699
|
];
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2700
|
+
for (const [name, value] of wanted)
|
|
2701
|
+
if ((values2[name] ?? null) !== value)
|
|
2702
|
+
await this.#set(board, id, name, value);
|
|
2703
|
+
}
|
|
2704
|
+
async #known(board, id) {
|
|
2705
|
+
const cache = await this.#cacheOf(board);
|
|
2706
|
+
if (!cache.known.has(id)) await this.items(board);
|
|
2707
|
+
const known = cache.known.get(id);
|
|
2708
|
+
if (known === void 0) throw new Error(`the board has no item ${id}`);
|
|
2709
|
+
return known;
|
|
2710
|
+
}
|
|
2711
|
+
#issueOf(known) {
|
|
2712
|
+
if (known.issue === null)
|
|
2713
|
+
throw new Error(`the board's item ${known.id} is no issue`);
|
|
2714
|
+
return known.issue;
|
|
2715
|
+
}
|
|
2716
|
+
async #labelId(board) {
|
|
2717
|
+
const id = (await this.#memory(board.org)).labelId;
|
|
2718
|
+
if (id === null)
|
|
2719
|
+
throw new Error(
|
|
2720
|
+
`${board.org}/${MEMORY} has no label ${LABEL}: run rness pulse sync`
|
|
2152
2721
|
);
|
|
2153
|
-
|
|
2154
|
-
if (value !== null) await this.#set(board, id, name, value);
|
|
2155
|
-
return;
|
|
2156
|
-
}
|
|
2157
|
-
if (!cache.known.has(step2.id)) await this.items(board);
|
|
2158
|
-
const known = cache.known.get(step2.id);
|
|
2159
|
-
if (known?.draftId == null)
|
|
2160
|
-
throw new Error(`the board has no draft item ${step2.id}`);
|
|
2161
|
-
await editDraft(known.draftId, want.title, want.body, this.#o);
|
|
2162
|
-
for (const [name, value] of values2)
|
|
2163
|
-
if ((known.values[name] ?? null) !== value)
|
|
2164
|
-
await this.#set(board, step2.id, name, value);
|
|
2722
|
+
return id;
|
|
2165
2723
|
}
|
|
2166
2724
|
async mark(board, itemIds, session) {
|
|
2167
2725
|
for (const id of itemIds) {
|
|
@@ -2176,6 +2734,7 @@ var GITHUB_HTTPS = "https://github.com/";
|
|
|
2176
2734
|
var GitHubOAuthProvider = class {
|
|
2177
2735
|
#token;
|
|
2178
2736
|
#apiBase;
|
|
2737
|
+
#wait;
|
|
2179
2738
|
#login;
|
|
2180
2739
|
#boardClient;
|
|
2181
2740
|
get authenticated() {
|
|
@@ -2184,11 +2743,16 @@ var GitHubOAuthProvider = class {
|
|
|
2184
2743
|
constructor(options) {
|
|
2185
2744
|
this.#token = options.token;
|
|
2186
2745
|
this.#apiBase = options.apiBase;
|
|
2746
|
+
this.#wait = options.wait;
|
|
2187
2747
|
}
|
|
2188
2748
|
#api(token) {
|
|
2189
|
-
return
|
|
2190
|
-
|
|
2191
|
-
|
|
2749
|
+
return {
|
|
2750
|
+
token,
|
|
2751
|
+
...this.#apiBase === void 0 ? {} : { apiBase: this.#apiBase },
|
|
2752
|
+
...this.#wait === void 0 ? {} : { wait: this.#wait }
|
|
2753
|
+
};
|
|
2754
|
+
}
|
|
2755
|
+
identity() {
|
|
2192
2756
|
this.#login ??= (async () => {
|
|
2193
2757
|
const t = this.#token;
|
|
2194
2758
|
if (t === null) return null;
|
|
@@ -2240,6 +2804,9 @@ var GitHubOAuthProvider = class {
|
|
|
2240
2804
|
return this.#boardClient;
|
|
2241
2805
|
}
|
|
2242
2806
|
// The board methods are async so that a missing login rejects, never throws.
|
|
2807
|
+
async checkIssues(org) {
|
|
2808
|
+
return this.#boards().checkIssues(org);
|
|
2809
|
+
}
|
|
2243
2810
|
async createBoard(org) {
|
|
2244
2811
|
return this.#boards().createBoard(org);
|
|
2245
2812
|
}
|
|
@@ -2249,8 +2816,8 @@ var GitHubOAuthProvider = class {
|
|
|
2249
2816
|
async ensureLayout(board, layout) {
|
|
2250
2817
|
return this.#boards().ensureLayout(board, layout);
|
|
2251
2818
|
}
|
|
2252
|
-
async items(board) {
|
|
2253
|
-
return this.#boards().items(board);
|
|
2819
|
+
async items(board, options) {
|
|
2820
|
+
return this.#boards().items(board, options);
|
|
2254
2821
|
}
|
|
2255
2822
|
async apply(board, step2) {
|
|
2256
2823
|
return this.#boards().apply(board, step2);
|
|
@@ -2263,16 +2830,17 @@ var GitHubOAuthProvider = class {
|
|
|
2263
2830
|
return { username: "x-access-token", password: this.#token.token };
|
|
2264
2831
|
}
|
|
2265
2832
|
};
|
|
2266
|
-
async function githubProvider(apiBase) {
|
|
2833
|
+
async function githubProvider(apiBase, wait) {
|
|
2267
2834
|
return new GitHubOAuthProvider({
|
|
2268
2835
|
token: await resolveToken(),
|
|
2269
|
-
...apiBase === void 0 ? {} : { apiBase }
|
|
2836
|
+
...apiBase === void 0 ? {} : { apiBase },
|
|
2837
|
+
...wait === void 0 ? {} : { wait }
|
|
2270
2838
|
});
|
|
2271
2839
|
}
|
|
2272
2840
|
|
|
2273
2841
|
// src/core/repos.ts
|
|
2274
2842
|
import { glob as glob2, readFile as readFile5, rm as rm3 } from "fs/promises";
|
|
2275
|
-
import { join as join8, posix as
|
|
2843
|
+
import { join as join8, posix as posix5, sep as sep2 } from "path";
|
|
2276
2844
|
async function addRepository(input) {
|
|
2277
2845
|
const { name, url } = parseRepoSpec(input.spec, input.org, input.host);
|
|
2278
2846
|
const dir = join8(input.root, "org", name);
|
|
@@ -2352,8 +2920,8 @@ async function workspaceDirs(repoDir) {
|
|
|
2352
2920
|
const dirs = /* @__PURE__ */ new Set();
|
|
2353
2921
|
for (const pattern of patterns) {
|
|
2354
2922
|
for await (const match of glob2(pattern, { cwd: repoDir })) {
|
|
2355
|
-
const rel = match.split(sep2).join(
|
|
2356
|
-
if (await exists(join8(repoDir, ...rel.split(
|
|
2923
|
+
const rel = match.split(sep2).join(posix5.sep);
|
|
2924
|
+
if (await exists(join8(repoDir, ...rel.split(posix5.sep), "package.json")))
|
|
2357
2925
|
dirs.add(rel);
|
|
2358
2926
|
}
|
|
2359
2927
|
}
|
|
@@ -2649,8 +3217,56 @@ function reportError(e) {
|
|
|
2649
3217
|
return 1;
|
|
2650
3218
|
}
|
|
2651
3219
|
|
|
3220
|
+
// src/core/delegate.ts
|
|
3221
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
3222
|
+
import { join as join10 } from "path";
|
|
3223
|
+
var DEFAULT_BIN = "dist/bin/rness.js";
|
|
3224
|
+
var PINNED_BIN = `node_modules/@rness/cli/${DEFAULT_BIN}`;
|
|
3225
|
+
async function readPinnedCli(rnessDir) {
|
|
3226
|
+
const dir = join10(rnessDir, "node_modules", "@rness", "cli");
|
|
3227
|
+
let pkg;
|
|
3228
|
+
try {
|
|
3229
|
+
pkg = JSON.parse(await readFile6(join10(dir, "package.json"), "utf8"));
|
|
3230
|
+
} catch {
|
|
3231
|
+
return null;
|
|
3232
|
+
}
|
|
3233
|
+
const declared2 = pkg.bin;
|
|
3234
|
+
let bin = DEFAULT_BIN;
|
|
3235
|
+
if (typeof declared2 === "string") bin = declared2;
|
|
3236
|
+
else if (declared2 !== null && typeof declared2 === "object") {
|
|
3237
|
+
const named = declared2["rness"];
|
|
3238
|
+
if (typeof named === "string") bin = named;
|
|
3239
|
+
}
|
|
3240
|
+
return {
|
|
3241
|
+
dir,
|
|
3242
|
+
bin,
|
|
3243
|
+
version: typeof pkg.version === "string" ? pkg.version : null
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
async function pinnedBinPath(rnessDir) {
|
|
3247
|
+
const pinned = await readPinnedCli(rnessDir);
|
|
3248
|
+
if (pinned === null) return null;
|
|
3249
|
+
return join10(pinned.dir, ...pinned.bin.split("/"));
|
|
3250
|
+
}
|
|
3251
|
+
async function findDelegate(cwd, ownVersion) {
|
|
3252
|
+
let root;
|
|
3253
|
+
let rnessDir;
|
|
3254
|
+
try {
|
|
3255
|
+
const ws = await findWorkspace(cwd);
|
|
3256
|
+
root = ws.root;
|
|
3257
|
+
rnessDir = ws.rnessDir;
|
|
3258
|
+
} catch {
|
|
3259
|
+
return null;
|
|
3260
|
+
}
|
|
3261
|
+
const pinned = await readPinnedCli(rnessDir);
|
|
3262
|
+
if (pinned === null) return null;
|
|
3263
|
+
const { version } = pinned;
|
|
3264
|
+
if (version === null || version === ownVersion) return null;
|
|
3265
|
+
return { entry: join10(pinned.dir, "dist", "index.js"), version, root };
|
|
3266
|
+
}
|
|
3267
|
+
|
|
2652
3268
|
// src/core/block.ts
|
|
2653
|
-
import { createHash } from "crypto";
|
|
3269
|
+
import { createHash as createHash2 } from "crypto";
|
|
2654
3270
|
var BEGIN = "<!-- BEGIN rness -->";
|
|
2655
3271
|
var END = "<!-- END rness -->";
|
|
2656
3272
|
var BLOCK_SIZE_WARNING = 32 * 1024;
|
|
@@ -2659,7 +3275,7 @@ function toLf(s) {
|
|
|
2659
3275
|
return s.replace(/\r\n?/g, "\n");
|
|
2660
3276
|
}
|
|
2661
3277
|
function blockHash(body) {
|
|
2662
|
-
return
|
|
3278
|
+
return createHash2("sha256").update(toLf(body), "utf8").digest("hex").slice(0, 12);
|
|
2663
3279
|
}
|
|
2664
3280
|
function intro(scope, org, depth) {
|
|
2665
3281
|
if (scope === null) {
|
|
@@ -2713,7 +3329,7 @@ function isCurrentBlock(blockLines, freshHash) {
|
|
|
2713
3329
|
}
|
|
2714
3330
|
|
|
2715
3331
|
// src/core/merge.ts
|
|
2716
|
-
import { join as
|
|
3332
|
+
import { join as join11 } from "path";
|
|
2717
3333
|
function findBlock(lines) {
|
|
2718
3334
|
const begins = [];
|
|
2719
3335
|
const ends = [];
|
|
@@ -2772,7 +3388,7 @@ function claudeMdAtRoot(existing, block) {
|
|
|
2772
3388
|
return merged.ok ? { ...merged, changed: merged.text !== existing } : merged;
|
|
2773
3389
|
}
|
|
2774
3390
|
async function ensureClaudeMd(dir) {
|
|
2775
|
-
const file =
|
|
3391
|
+
const file = join11(dir, "CLAUDE.md");
|
|
2776
3392
|
if (await isSymlink(file)) return "unchanged";
|
|
2777
3393
|
const existing = await readOrNull(file);
|
|
2778
3394
|
if (existing === null) {
|
|
@@ -2787,14 +3403,222 @@ async function ensureClaudeMd(dir) {
|
|
|
2787
3403
|
}
|
|
2788
3404
|
|
|
2789
3405
|
// src/commands/sync.ts
|
|
2790
|
-
import { join as
|
|
3406
|
+
import { join as join14 } from "path";
|
|
2791
3407
|
|
|
2792
3408
|
// src/core/agent-targets.ts
|
|
2793
3409
|
import { mkdir as mkdir2 } from "fs/promises";
|
|
2794
|
-
import { dirname as dirname4, join as
|
|
3410
|
+
import { dirname as dirname4, join as join13 } from "path";
|
|
3411
|
+
|
|
3412
|
+
// src/core/claude-skills.ts
|
|
3413
|
+
var PLUGIN = ".claude/skills/rness";
|
|
3414
|
+
var PLUGIN_JSON = `${JSON.stringify(
|
|
3415
|
+
{
|
|
3416
|
+
name: "rness",
|
|
3417
|
+
description: "The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan, /rness:done."
|
|
3418
|
+
},
|
|
3419
|
+
null,
|
|
3420
|
+
2
|
|
3421
|
+
)}
|
|
3422
|
+
`;
|
|
3423
|
+
function statusSkill(rness, pm) {
|
|
3424
|
+
const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status --cwd "\${CLAUDE_PROJECT_DIR}"`;
|
|
3425
|
+
return `---
|
|
3426
|
+
name: status
|
|
3427
|
+
description: The status of every decision, specification, plan and other tracked document of the rness workspace, one table per directory. Read-only.
|
|
3428
|
+
argument-hint: '[tab]'
|
|
3429
|
+
disable-model-invocation: true
|
|
3430
|
+
allowed-tools: Bash(${command})
|
|
3431
|
+
---
|
|
3432
|
+
|
|
3433
|
+
!\`${command}\`
|
|
3434
|
+
|
|
3435
|
+
Show the output above to the user as it is: the heading and the tables,
|
|
3436
|
+
nothing added, nothing summarised, no other tool. Arguments: \`$ARGUMENTS\`.
|
|
3437
|
+
When they name a tab, show only that tab's section. If the output says the
|
|
3438
|
+
module cannot be found, say instead that ${rness} is not installed next to
|
|
3439
|
+
this repository.
|
|
3440
|
+
|
|
3441
|
+
End with this line: _For the view with tabs and scrolling: here, Ctrl+Z,
|
|
3442
|
+
then \`npx @rness/cli status\` (q to close), then \`fg\`; or in another
|
|
3443
|
+
terminal, from the workspace's \`.rness/\`: \`${localRunner(pm)} rness status\`._
|
|
3444
|
+
`;
|
|
3445
|
+
}
|
|
3446
|
+
var LIFECYCLE2 = [
|
|
3447
|
+
{
|
|
3448
|
+
name: "adr",
|
|
3449
|
+
description: "Record a decision of the rness workspace as an ADR in .rness/adr/ \u2014 a choice expensive to reverse or that creates a lasting constraint. Use when the developer asks for an ADR, or when the conversation reaches such a decision.",
|
|
3450
|
+
hint: "[subject | NNNN]",
|
|
3451
|
+
tab: "adr",
|
|
3452
|
+
body: (rness) => `You record a decision of this workspace as an ADR in \`${rness}/adr/\`. The table
|
|
3453
|
+
above lists the existing ADRs, newest first. Arguments: \`$ARGUMENTS\`.
|
|
3454
|
+
|
|
3455
|
+
Before writing:
|
|
3456
|
+
|
|
3457
|
+
1. If the developer did not ask for an ADR in so many words, propose one in
|
|
3458
|
+
a single line \u2014 its title, and why the choice is hard to reverse \u2014 and
|
|
3459
|
+
wait for a yes.
|
|
3460
|
+
2. Read \`${rness}/CONVENTIONS.md\` and \`${rness}/adr/0000-template.md\`: they hold the
|
|
3461
|
+
rules and the sections; this skill is only the procedure.
|
|
3462
|
+
3. Arguments naming an ADR (\`NNNN\`): reopen that file. An \`Accepted\` ADR is
|
|
3463
|
+
never edited: offer a new ADR that supersedes it.
|
|
3464
|
+
4. Otherwise the subject is the arguments, else the decision just
|
|
3465
|
+
discussed. Context, alternatives or consequences missing from the
|
|
3466
|
+
conversation: ask for them, one question at a time. Never invent them.
|
|
3467
|
+
|
|
3468
|
+
Writing:
|
|
3469
|
+
|
|
3470
|
+
- \`${rness}/adr/NNNN-<slug>.md\`, NNNN the highest number above plus one.
|
|
3471
|
+
- The template's front matter and sections, filled in, its guidance lines
|
|
3472
|
+
removed: \`date\` today, \`status: Proposed\`, \`repo\` the repositories under
|
|
3473
|
+
\`org/\` it affects.
|
|
3474
|
+
- An ADR it replaces is named in Context. That one becomes \`Superseded\`,
|
|
3475
|
+
with \`superseded_by:\`, only once the new ADR is \`Accepted\`.
|
|
3476
|
+
- The rness hook checks every edit under \`${rness}/\`: fix what it reports.
|
|
3477
|
+
|
|
3478
|
+
After writing: give the path and the Decision section, and say the ADR
|
|
3479
|
+
stays \`Proposed\` until the developer accepts it. Change a status only when
|
|
3480
|
+
the developer says so. Do not commit.
|
|
3481
|
+
`
|
|
3482
|
+
},
|
|
3483
|
+
{
|
|
3484
|
+
name: "spec",
|
|
3485
|
+
description: "Write a specification of the rness workspace in .rness/specs/ \u2014 the outcome wanted and its scope, before any plan. Use when the developer asks for a spec, or when agreed work is too large to start without one.",
|
|
3486
|
+
hint: "[subject | NNNN]",
|
|
3487
|
+
tab: "specs",
|
|
3488
|
+
body: (rness) => `You write a specification of this workspace in \`${rness}/specs/\`. The table above
|
|
3489
|
+
lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
|
|
3490
|
+
|
|
3491
|
+
Before writing:
|
|
3492
|
+
|
|
3493
|
+
1. If the developer did not ask for a specification in so many words,
|
|
3494
|
+
propose one in a single line and wait for a yes.
|
|
3495
|
+
2. Read \`${rness}/CONVENTIONS.md\`, then the most recent specification above: match
|
|
3496
|
+
its front matter, sections and tone.
|
|
3497
|
+
3. Arguments naming a specification (\`NNNN\`): reopen it. One that is
|
|
3498
|
+
\`Implemented\`, \`Superseded\` or \`Rejected\` is not edited: offer a new one.
|
|
3499
|
+
4. Otherwise the subject is the arguments, else the work just discussed.
|
|
3500
|
+
The problem, what is in and out of scope, the ADRs it rests on: ask what
|
|
3501
|
+
the conversation does not say, one question at a time.
|
|
3502
|
+
|
|
3503
|
+
Writing:
|
|
3504
|
+
|
|
3505
|
+
- \`${rness}/specs/NNNN-<slug>.md\`, NNNN the highest number above plus one.
|
|
3506
|
+
- \`date\` and \`updated\` today, \`status: Draft\`, \`repo\`, and \`adr:\` linking
|
|
3507
|
+
the decisions it rests on \u2014 or the rationale in the body when none does.
|
|
3508
|
+
- No specification yet to follow: Summary, Scope, Out of scope,
|
|
3509
|
+
Alternatives considered, To verify, Tests.
|
|
3510
|
+
- What was checked in this session (a document read, a command run) is
|
|
3511
|
+
written as verified, with the date; everything else as a proposal or an
|
|
3512
|
+
open question.
|
|
3513
|
+
- The rness hook checks every edit under \`${rness}/\`: fix what it reports.
|
|
3514
|
+
|
|
3515
|
+
After writing: give the path and the summary. It stays \`Draft\` until the
|
|
3516
|
+
developer moves it (\`Proposed\` for review, \`Approved\` to plan it). No plan
|
|
3517
|
+
here: \`/rness:plan NNNN\` once approved. Do not commit.
|
|
3518
|
+
`
|
|
3519
|
+
},
|
|
3520
|
+
{
|
|
3521
|
+
name: "plan",
|
|
3522
|
+
description: "Turn an approved specification of the rness workspace into a plan in .rness/plans/ \u2014 ordered tasks, each with the command that proves it done. Use when the developer asks for a plan, or when a specification has just been approved.",
|
|
3523
|
+
hint: "<spec NNNN>",
|
|
3524
|
+
tab: "plans",
|
|
3525
|
+
body: (rness) => `You turn a specification into a plan in \`${rness}/plans/\`. The table above lists
|
|
3526
|
+
the existing plans, newest first. Arguments, the specification:
|
|
3527
|
+
\`$ARGUMENTS\`.
|
|
3528
|
+
|
|
3529
|
+
Before writing:
|
|
3530
|
+
|
|
3531
|
+
1. If the developer did not ask for a plan in so many words, propose one in
|
|
3532
|
+
a single line and wait for a yes.
|
|
3533
|
+
2. No specification named: ask which. Read it, \`${rness}/CONVENTIONS.md\`, and the
|
|
3534
|
+
most recent plan above: match its front matter and sections.
|
|
3535
|
+
3. A specification that is not \`Approved\`: say so, and plan it only if the
|
|
3536
|
+
developer confirms. A plan of it that is neither \`Completed\` nor
|
|
3537
|
+
\`Abandoned\`: reopen it rather than write a second.
|
|
3538
|
+
4. Read the code and the repositories the specification touches: every
|
|
3539
|
+
task names real files and real commands.
|
|
3540
|
+
|
|
3541
|
+
Writing:
|
|
3542
|
+
|
|
3543
|
+
- \`${rness}/plans/NNNN-<slug>.md\`, NNNN the highest plan number above plus one \u2014
|
|
3544
|
+
not the specification's.
|
|
3545
|
+
- \`date\` and \`updated\` today, \`status: Draft\`, \`repo\`, \`spec:\` linking the
|
|
3546
|
+
specification.
|
|
3547
|
+
- Tasks in order, as checkboxes. Each gives the files, what changes, and
|
|
3548
|
+
the command that proves it (a test, a build, a run). The last tasks:
|
|
3549
|
+
the documentation the work makes inaccurate, then the release when the
|
|
3550
|
+
repository has one.
|
|
3551
|
+
- Where the plan decides something the specification leaves open, a
|
|
3552
|
+
section says so.
|
|
3553
|
+
- The rness hook checks every edit under \`${rness}/\`: fix what it reports.
|
|
3554
|
+
|
|
3555
|
+
After writing: give the path and the list of tasks. It becomes \`Ready\` when
|
|
3556
|
+
the developer agrees. Do not commit.
|
|
3557
|
+
`
|
|
3558
|
+
},
|
|
3559
|
+
{
|
|
3560
|
+
name: "done",
|
|
3561
|
+
description: "Close a piece of work of the rness workspace \u2014 check each task of its plan on evidence, then move the plan and its specification to their final status and correct the documents the work made inaccurate. Use when the developer says the work is done, or when the last task of a plan passes.",
|
|
3562
|
+
hint: "[plan NNNN]",
|
|
3563
|
+
tab: "plans",
|
|
3564
|
+
body: (rness) => `You close a piece of work: its plan in \`${rness}/plans/\`, its specification, and
|
|
3565
|
+
what they made inaccurate. The table above lists the plans, newest first.
|
|
3566
|
+
Arguments: \`$ARGUMENTS\`.
|
|
3567
|
+
|
|
3568
|
+
1. The plan: the one named, else the one \`In progress\`. Several or none:
|
|
3569
|
+
ask.
|
|
3570
|
+
2. If the developer did not ask to close it in so many words, propose it in
|
|
3571
|
+
a single line and wait for a yes.
|
|
3572
|
+
3. Read the plan, its specification and \`${rness}/CONVENTIONS.md\`.
|
|
3573
|
+
4. Evidence first. For each task, what shows it done: a test, a build, a
|
|
3574
|
+
command's output in this session. A task without evidence: run its
|
|
3575
|
+
check now, or report it. Never mark a task done on belief.
|
|
3576
|
+
5. All tasks proven:
|
|
3577
|
+
- the plan: \`status: Completed\`, \`updated\` today, the checkboxes of the
|
|
3578
|
+
proven tasks ticked, and a section \`Verification (YYYY-MM-DD)\` \u2014 what
|
|
3579
|
+
ran, what it showed, what could not run and why;
|
|
3580
|
+
- the specification: \`Implemented\` once every plan of it is
|
|
3581
|
+
\`Completed\`; \`updated\` today;
|
|
3582
|
+
- the smallest set of documents the work makes inaccurate: \`${rness}/docs/\`,
|
|
3583
|
+
the READMEs of the repositories it changed.
|
|
3584
|
+
6. A task not proven: the plan stays \`In progress\` \u2014 or \`Blocked\`, the
|
|
3585
|
+
blocker written in it. Say what remains.
|
|
3586
|
+
7. The rness hook checks every edit under \`${rness}/\`: fix what it reports.
|
|
3587
|
+
|
|
3588
|
+
After: list the files changed and the statuses moved. Do not commit.
|
|
3589
|
+
`
|
|
3590
|
+
}
|
|
3591
|
+
];
|
|
3592
|
+
function lifecycleSkill(rness, s) {
|
|
3593
|
+
const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status ${s.tab} --cwd "\${CLAUDE_PROJECT_DIR}"`;
|
|
3594
|
+
return `---
|
|
3595
|
+
name: ${s.name}
|
|
3596
|
+
description: ${s.description}
|
|
3597
|
+
argument-hint: '${s.hint}'
|
|
3598
|
+
allowed-tools: Bash(${command})
|
|
3599
|
+
---
|
|
3600
|
+
|
|
3601
|
+
!\`${command}\`
|
|
3602
|
+
|
|
3603
|
+
${s.body(rness)}`;
|
|
3604
|
+
}
|
|
3605
|
+
function pluginFiles(rness, at, ctx) {
|
|
3606
|
+
return [
|
|
3607
|
+
{ file: `${PLUGIN}/.claude-plugin/plugin.json`, at, content: PLUGIN_JSON },
|
|
3608
|
+
{
|
|
3609
|
+
file: `${PLUGIN}/skills/status/SKILL.md`,
|
|
3610
|
+
at,
|
|
3611
|
+
content: statusSkill(rness, ctx.packageManager)
|
|
3612
|
+
},
|
|
3613
|
+
...LIFECYCLE2.map((s) => ({
|
|
3614
|
+
file: `${PLUGIN}/skills/${s.name}/SKILL.md`,
|
|
3615
|
+
at,
|
|
3616
|
+
content: lifecycleSkill(rness, s)
|
|
3617
|
+
}))
|
|
3618
|
+
];
|
|
3619
|
+
}
|
|
2795
3620
|
|
|
2796
3621
|
// src/core/agents.ts
|
|
2797
|
-
var PINNED_BIN = "node_modules/@rness/cli/dist/bin/rness.js";
|
|
2798
3622
|
function hookLine(rness, event) {
|
|
2799
3623
|
const run3 = `f="$CLAUDE_PROJECT_DIR/${rness}/${PINNED_BIN}"; if [ -f "$f" ]; then node "$f" hook ${event};`;
|
|
2800
3624
|
if (event !== "session-start") return `${run3} fi`;
|
|
@@ -2847,53 +3671,11 @@ function hooks(rness) {
|
|
|
2847
3671
|
}
|
|
2848
3672
|
];
|
|
2849
3673
|
}
|
|
2850
|
-
var PLUGIN = ".claude/skills/rness";
|
|
2851
|
-
var PLUGIN_JSON = `${JSON.stringify(
|
|
2852
|
-
{
|
|
2853
|
-
name: "rness",
|
|
2854
|
-
description: "The rness workspace in Claude Code: /rness:status."
|
|
2855
|
-
},
|
|
2856
|
-
null,
|
|
2857
|
-
2
|
|
2858
|
-
)}
|
|
2859
|
-
`;
|
|
2860
|
-
function statusSkill(rness) {
|
|
2861
|
-
const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status --cwd "\${CLAUDE_PROJECT_DIR}"`;
|
|
2862
|
-
return `---
|
|
2863
|
-
name: status
|
|
2864
|
-
description: The status of every decision, specification, plan and other tracked document of the rness workspace, one table per directory. Read-only.
|
|
2865
|
-
argument-hint: '[tab]'
|
|
2866
|
-
disable-model-invocation: true
|
|
2867
|
-
allowed-tools: Bash(${command})
|
|
2868
|
-
---
|
|
2869
|
-
|
|
2870
|
-
!\`${command}\`
|
|
2871
|
-
|
|
2872
|
-
Show the output above to the user as it is: the heading and the tables,
|
|
2873
|
-
nothing added, nothing summarised, no other tool. Arguments: \`$ARGUMENTS\`.
|
|
2874
|
-
When they name a tab, show only that tab's section. If the output says the
|
|
2875
|
-
module cannot be found, say instead that ${rness} is not installed next to
|
|
2876
|
-
this repository.
|
|
2877
|
-
|
|
2878
|
-
End with this line: _For the view with tabs and scrolling: Ctrl+Z, then
|
|
2879
|
-
\`npx @rness/cli status\` (q to close), then \`fg\`._
|
|
2880
|
-
`;
|
|
2881
|
-
}
|
|
2882
|
-
function plugin(rness, at) {
|
|
2883
|
-
return [
|
|
2884
|
-
{ file: `${PLUGIN}/.claude-plugin/plugin.json`, at, content: PLUGIN_JSON },
|
|
2885
|
-
{
|
|
2886
|
-
file: `${PLUGIN}/skills/status/SKILL.md`,
|
|
2887
|
-
at,
|
|
2888
|
-
content: statusSkill(rness)
|
|
2889
|
-
}
|
|
2890
|
-
];
|
|
2891
|
-
}
|
|
2892
3674
|
var TARGETS = {
|
|
2893
3675
|
claude: {
|
|
2894
3676
|
name: "claude",
|
|
2895
3677
|
label: "Claude Code",
|
|
2896
|
-
files: [
|
|
3678
|
+
files: (ctx) => [
|
|
2897
3679
|
{
|
|
2898
3680
|
file: ".claude/settings.json",
|
|
2899
3681
|
at: "clones",
|
|
@@ -2929,7 +3711,7 @@ var TARGETS = {
|
|
|
2929
3711
|
}
|
|
2930
3712
|
]
|
|
2931
3713
|
},
|
|
2932
|
-
...
|
|
3714
|
+
...pluginFiles("../../.rness", "clones", ctx),
|
|
2933
3715
|
// A session opened at the workspace root reads this file, one opened
|
|
2934
3716
|
// in a clone does not (spec 0011 §1): the hooks only, `.rness` inside.
|
|
2935
3717
|
{
|
|
@@ -2937,7 +3719,7 @@ var TARGETS = {
|
|
|
2937
3719
|
at: "root",
|
|
2938
3720
|
guarantees: hooks(".rness")
|
|
2939
3721
|
},
|
|
2940
|
-
...
|
|
3722
|
+
...pluginFiles(".rness", "root", ctx)
|
|
2941
3723
|
]
|
|
2942
3724
|
}
|
|
2943
3725
|
};
|
|
@@ -3051,52 +3833,310 @@ function describeGuarantee(g) {
|
|
|
3051
3833
|
return `${g.path.join(".")} lacks ${what}`;
|
|
3052
3834
|
}
|
|
3053
3835
|
|
|
3054
|
-
// src/core/
|
|
3055
|
-
|
|
3056
|
-
|
|
3057
|
-
|
|
3836
|
+
// src/core/pinned.ts
|
|
3837
|
+
import { execFile as execFile3 } from "child_process";
|
|
3838
|
+
import { readFile as readFile7 } from "fs/promises";
|
|
3839
|
+
import { basename as basename3, join as join12 } from "path";
|
|
3840
|
+
import { promisify as promisify3 } from "util";
|
|
3841
|
+
var execFileP3 = promisify3(execFile3);
|
|
3842
|
+
var PACKAGE = "@rness/cli";
|
|
3843
|
+
var EXACT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
3844
|
+
function compareVersions(a, b) {
|
|
3845
|
+
const split = (v) => {
|
|
3846
|
+
const dash = v.indexOf("-");
|
|
3847
|
+
const core = dash === -1 ? v : v.slice(0, dash);
|
|
3848
|
+
return [core.split(".").map(Number), dash === -1 ? null : v.slice(dash + 1)];
|
|
3849
|
+
};
|
|
3850
|
+
const [coreA, preA] = split(a);
|
|
3851
|
+
const [coreB, preB] = split(b);
|
|
3852
|
+
for (let i = 0; i < 3; i += 1) {
|
|
3853
|
+
const diff = (coreA[i] ?? 0) - (coreB[i] ?? 0);
|
|
3854
|
+
if (diff !== 0) return diff;
|
|
3855
|
+
}
|
|
3856
|
+
if (preA === preB) return 0;
|
|
3857
|
+
if (preA === null) return 1;
|
|
3858
|
+
if (preB === null) return -1;
|
|
3859
|
+
return preA.localeCompare(preB, "en", { numeric: true });
|
|
3058
3860
|
}
|
|
3059
|
-
async function
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
|
|
3067
|
-
file: join11(root, ...owned.file.split("/")),
|
|
3068
|
-
label: owned.file
|
|
3069
|
-
});
|
|
3070
|
-
for (const repo of Object.keys(manifest.repos)) {
|
|
3071
|
-
if (!await exists(join11(root, "org", repo))) continue;
|
|
3072
|
-
for (const target of targets)
|
|
3073
|
-
for (const owned of target.files)
|
|
3074
|
-
if (owned.at === "clones")
|
|
3075
|
-
files.push({
|
|
3076
|
-
...owned,
|
|
3077
|
-
file: join11(root, "org", repo, ...owned.file.split("/")),
|
|
3078
|
-
label: `org/${repo}/${owned.file}`
|
|
3079
|
-
});
|
|
3861
|
+
async function readPackageJson(rnessDir) {
|
|
3862
|
+
try {
|
|
3863
|
+
const parsed = JSON.parse(
|
|
3864
|
+
await readFile7(join12(rnessDir, "package.json"), "utf8")
|
|
3865
|
+
);
|
|
3866
|
+
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
3867
|
+
} catch {
|
|
3868
|
+
return null;
|
|
3080
3869
|
}
|
|
3081
|
-
return files;
|
|
3082
3870
|
}
|
|
3083
|
-
function
|
|
3084
|
-
|
|
3085
|
-
|
|
3871
|
+
async function readPin(rnessDir) {
|
|
3872
|
+
const pkg = await readPackageJson(rnessDir);
|
|
3873
|
+
if (pkg === null) return null;
|
|
3874
|
+
for (const field of ["devDependencies", "dependencies"]) {
|
|
3875
|
+
const deps = pkg[field];
|
|
3876
|
+
if (deps === null || typeof deps !== "object") continue;
|
|
3877
|
+
const spec = deps[PACKAGE];
|
|
3878
|
+
if (typeof spec === "string") return { spec, field };
|
|
3879
|
+
}
|
|
3880
|
+
return null;
|
|
3881
|
+
}
|
|
3882
|
+
async function writePin(rnessDir, from, to) {
|
|
3883
|
+
const file = join12(rnessDir, "package.json");
|
|
3884
|
+
const text = await readFile7(file, "utf8");
|
|
3885
|
+
const entry = new RegExp(
|
|
3886
|
+
`("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
|
|
3086
3887
|
);
|
|
3888
|
+
if (!entry.test(text))
|
|
3889
|
+
throw new Error(`"${PACKAGE}": "${from}" not found in package.json`);
|
|
3890
|
+
await writeFileAtomic(file, text.replace(entry, `$1${to}$2`));
|
|
3087
3891
|
}
|
|
3088
|
-
var
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3892
|
+
var LOCKFILES = [
|
|
3893
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
3894
|
+
["yarn.lock", "yarn"],
|
|
3895
|
+
["bun.lock", "bun"],
|
|
3896
|
+
["bun.lockb", "bun"],
|
|
3897
|
+
["package-lock.json", "npm"]
|
|
3898
|
+
];
|
|
3899
|
+
async function workspacePackageManager(rnessDir) {
|
|
3900
|
+
const declared2 = (await readPackageJson(rnessDir))?.["packageManager"];
|
|
3901
|
+
if (typeof declared2 === "string") {
|
|
3902
|
+
const name = declared2.split("@")[0] ?? "";
|
|
3903
|
+
if (isPackageManager(name)) return name;
|
|
3904
|
+
}
|
|
3905
|
+
for (const [lockfile, pm] of LOCKFILES)
|
|
3906
|
+
if (await exists(join12(rnessDir, lockfile))) return pm;
|
|
3907
|
+
return "npm";
|
|
3096
3908
|
}
|
|
3097
|
-
async function
|
|
3098
|
-
const
|
|
3099
|
-
|
|
3909
|
+
async function hasLockfile(rnessDir, pm) {
|
|
3910
|
+
for (const [lockfile, owner] of LOCKFILES)
|
|
3911
|
+
if (owner === pm && await exists(join12(rnessDir, lockfile))) return true;
|
|
3912
|
+
return false;
|
|
3913
|
+
}
|
|
3914
|
+
async function pinDrift(rnessDir) {
|
|
3915
|
+
const pin = await readPin(rnessDir);
|
|
3916
|
+
if (pin === null || !EXACT_VERSION.test(pin.spec)) return null;
|
|
3917
|
+
const installed = (await readPinnedCli(rnessDir))?.version ?? null;
|
|
3918
|
+
return installed === pin.spec ? null : { pin: pin.spec, installed };
|
|
3919
|
+
}
|
|
3920
|
+
async function driftWarning(cwd) {
|
|
3921
|
+
let root;
|
|
3922
|
+
let rnessDir;
|
|
3923
|
+
try {
|
|
3924
|
+
const ws = await findWorkspace(cwd);
|
|
3925
|
+
root = ws.root;
|
|
3926
|
+
rnessDir = ws.rnessDir;
|
|
3927
|
+
} catch {
|
|
3928
|
+
return null;
|
|
3929
|
+
}
|
|
3930
|
+
const drift = await pinDrift(rnessDir);
|
|
3931
|
+
if (drift === null) return null;
|
|
3932
|
+
const pm = await workspacePackageManager(rnessDir);
|
|
3933
|
+
return warn(
|
|
3934
|
+
`${basename3(root)}/.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${pm} install in .rness`
|
|
3935
|
+
);
|
|
3936
|
+
}
|
|
3937
|
+
var CHILD_DEADLINE_MS = 12e4;
|
|
3938
|
+
function childEnv(env = process.env) {
|
|
3939
|
+
return {
|
|
3940
|
+
...env,
|
|
3941
|
+
GIT_SSH_COMMAND: `${env["GIT_SSH_COMMAND"] ?? "ssh"} -o BatchMode=yes`,
|
|
3942
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3945
|
+
async function ensureInstalled(rnessDir, opts = {}) {
|
|
3946
|
+
const drift = await pinDrift(rnessDir);
|
|
3947
|
+
if (drift === null) return null;
|
|
3948
|
+
const env = opts.env ?? process.env;
|
|
3949
|
+
if (env["RNESS_NO_INSTALL"] === "1")
|
|
3950
|
+
return { installed: false, pin: drift.pin, reason: "RNESS_NO_INSTALL=1" };
|
|
3951
|
+
const pm = await workspacePackageManager(rnessDir);
|
|
3952
|
+
const frozen = await hasLockfile(rnessDir, pm);
|
|
3953
|
+
const install = opts.install ?? installDependencies;
|
|
3954
|
+
opts.announce?.(drift.pin);
|
|
3955
|
+
try {
|
|
3956
|
+
await install(pm, rnessDir, { frozen });
|
|
3957
|
+
return { installed: true, pin: drift.pin, reason: null };
|
|
3958
|
+
} catch (e) {
|
|
3959
|
+
return {
|
|
3960
|
+
installed: false,
|
|
3961
|
+
pin: drift.pin,
|
|
3962
|
+
reason: e instanceof Error ? e.message : String(e)
|
|
3963
|
+
};
|
|
3964
|
+
}
|
|
3965
|
+
}
|
|
3966
|
+
async function runPinnedSync(root) {
|
|
3967
|
+
const installed = await readPinnedCli(join12(root, ".rness"));
|
|
3968
|
+
if (installed === null) return null;
|
|
3969
|
+
const pinned = join12(installed.dir, ...installed.bin.split("/"));
|
|
3970
|
+
if (!await exists(pinned))
|
|
3971
|
+
return {
|
|
3972
|
+
code: 1,
|
|
3973
|
+
stdout: "",
|
|
3974
|
+
stderr: `@rness/cli in .rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
|
|
3975
|
+
`
|
|
3976
|
+
};
|
|
3977
|
+
try {
|
|
3978
|
+
const { stdout, stderr } = await execFileP3(
|
|
3979
|
+
process.execPath,
|
|
3980
|
+
[pinned, "sync", "--yes"],
|
|
3981
|
+
{
|
|
3982
|
+
cwd: root,
|
|
3983
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
3984
|
+
env: childEnv(),
|
|
3985
|
+
timeout: CHILD_DEADLINE_MS,
|
|
3986
|
+
killSignal: "SIGKILL"
|
|
3987
|
+
}
|
|
3988
|
+
);
|
|
3989
|
+
return { code: 0, stdout, stderr };
|
|
3990
|
+
} catch (e) {
|
|
3991
|
+
const err = e;
|
|
3992
|
+
const deadline = err.killed === true ? `rness sync in .rness did not finish within ${CHILD_DEADLINE_MS / 1e3}s and was stopped
|
|
3993
|
+
` : "";
|
|
3994
|
+
return {
|
|
3995
|
+
code: typeof err.code === "number" ? err.code : 1,
|
|
3996
|
+
stdout: err.stdout ?? "",
|
|
3997
|
+
stderr: `${err.stderr ?? ""}${deadline}`
|
|
3998
|
+
};
|
|
3999
|
+
}
|
|
4000
|
+
}
|
|
4001
|
+
var SYNC_VERBS = ["updated", "unchanged", "stale"];
|
|
4002
|
+
function counted(noun, counts) {
|
|
4003
|
+
const total = [...counts.values()].reduce((a, b) => a + b, 0);
|
|
4004
|
+
const detail = SYNC_VERBS.filter((verb) => counts.has(verb)).map((verb) => `${counts.get(verb) ?? 0} ${verb}`).join(", ");
|
|
4005
|
+
return `${total} ${noun}${total === 1 ? "" : "s"}${detail === "" ? "" : `: ${detail}`}`;
|
|
4006
|
+
}
|
|
4007
|
+
function summariseSync(stdout) {
|
|
4008
|
+
const blocks = /* @__PURE__ */ new Map();
|
|
4009
|
+
const agentFiles = /* @__PURE__ */ new Map();
|
|
4010
|
+
const others = [];
|
|
4011
|
+
for (const line3 of stdout.split("\n")) {
|
|
4012
|
+
if (line3.trim() === "") continue;
|
|
4013
|
+
const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line3);
|
|
4014
|
+
if (m === null) {
|
|
4015
|
+
others.push(line3);
|
|
4016
|
+
continue;
|
|
4017
|
+
}
|
|
4018
|
+
const [, verb = "", path = ""] = m;
|
|
4019
|
+
const counts = /(^|\/)(AGENTS|CLAUDE)\.md$/.test(path) ? blocks : agentFiles;
|
|
4020
|
+
counts.set(verb, (counts.get(verb) ?? 0) + 1);
|
|
4021
|
+
}
|
|
4022
|
+
return {
|
|
4023
|
+
summary: [
|
|
4024
|
+
counted("block", blocks),
|
|
4025
|
+
...agentFiles.size > 0 ? [counted("agent file", agentFiles)] : []
|
|
4026
|
+
].join("; "),
|
|
4027
|
+
others
|
|
4028
|
+
};
|
|
4029
|
+
}
|
|
4030
|
+
function syncOutcome(result, opts = {}) {
|
|
4031
|
+
const { others } = summariseSync(result.stdout);
|
|
4032
|
+
const wrote = /^(updated|unchanged|stale)\s/m.test(result.stdout);
|
|
4033
|
+
const stderr = result.stderr.split("\n").map((l) => l.trimEnd()).filter((l) => l.trim() !== "");
|
|
4034
|
+
const hints = wrote ? [...others, ...stderr] : others;
|
|
4035
|
+
if (opts.sshWorks === true && /Permission denied \(publickey\)/.test(result.stderr))
|
|
4036
|
+
hints.push(...agentHintLines());
|
|
4037
|
+
return {
|
|
4038
|
+
code: wrote ? 0 : result.code,
|
|
4039
|
+
hints,
|
|
4040
|
+
errors: wrote ? [] : stderr
|
|
4041
|
+
};
|
|
4042
|
+
}
|
|
4043
|
+
async function syncPinned(root, shown, fallback) {
|
|
4044
|
+
const rnessDir = join12(root, ".rness");
|
|
4045
|
+
const installed = await readPinnedCli(rnessDir);
|
|
4046
|
+
if (installed === null) {
|
|
4047
|
+
if (fallback !== void 0) return fallback();
|
|
4048
|
+
process.stderr.write(
|
|
4049
|
+
`@rness/cli is not installed in ${shown}/.rness; install in .rness/, then ${rnessCommand()} sync
|
|
4050
|
+
`
|
|
4051
|
+
);
|
|
4052
|
+
return 1;
|
|
4053
|
+
}
|
|
4054
|
+
const pinned = join12(installed.dir, ...installed.bin.split("/"));
|
|
4055
|
+
if (!await exists(pinned)) {
|
|
4056
|
+
process.stderr.write(
|
|
4057
|
+
`@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
|
|
4058
|
+
`
|
|
4059
|
+
);
|
|
4060
|
+
return 1;
|
|
4061
|
+
}
|
|
4062
|
+
try {
|
|
4063
|
+
const { stdout, stderr } = await execFileP3(
|
|
4064
|
+
process.execPath,
|
|
4065
|
+
[pinned, "sync", "--yes"],
|
|
4066
|
+
{
|
|
4067
|
+
cwd: root,
|
|
4068
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
4069
|
+
env: childEnv(),
|
|
4070
|
+
timeout: CHILD_DEADLINE_MS,
|
|
4071
|
+
killSignal: "SIGKILL"
|
|
4072
|
+
}
|
|
4073
|
+
);
|
|
4074
|
+
process.stdout.write(stdout);
|
|
4075
|
+
if (stderr !== "") process.stderr.write(stderr);
|
|
4076
|
+
return 0;
|
|
4077
|
+
} catch (e) {
|
|
4078
|
+
const err = e;
|
|
4079
|
+
if (err.stdout !== void 0 && err.stdout !== "")
|
|
4080
|
+
process.stdout.write(err.stdout);
|
|
4081
|
+
if (err.stderr !== void 0 && err.stderr !== "")
|
|
4082
|
+
process.stderr.write(err.stderr);
|
|
4083
|
+
return typeof err.code === "number" ? err.code : 1;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
// src/core/agent-targets.ts
|
|
4088
|
+
function wholeDiff(existing, content) {
|
|
4089
|
+
if (existing === content) return null;
|
|
4090
|
+
return existing === null ? "missing" : "differs from what rness writes";
|
|
4091
|
+
}
|
|
4092
|
+
async function contextOf(root) {
|
|
4093
|
+
return {
|
|
4094
|
+
packageManager: await workspacePackageManager(join13(root, ".rness"))
|
|
4095
|
+
};
|
|
4096
|
+
}
|
|
4097
|
+
var PATHS_ONLY = { packageManager: "npm" };
|
|
4098
|
+
async function filesOf(root, manifest, targets) {
|
|
4099
|
+
const files = [];
|
|
4100
|
+
if (!await exists(join13(root, "org"))) return files;
|
|
4101
|
+
const ctx = await contextOf(root);
|
|
4102
|
+
for (const target2 of targets)
|
|
4103
|
+
for (const owned of target2.files(ctx))
|
|
4104
|
+
if (owned.at === "root")
|
|
4105
|
+
files.push({
|
|
4106
|
+
...owned,
|
|
4107
|
+
file: join13(root, ...owned.file.split("/")),
|
|
4108
|
+
label: owned.file
|
|
4109
|
+
});
|
|
4110
|
+
for (const repo of Object.keys(manifest.repos)) {
|
|
4111
|
+
if (!await exists(join13(root, "org", repo))) continue;
|
|
4112
|
+
for (const target2 of targets)
|
|
4113
|
+
for (const owned of target2.files(ctx))
|
|
4114
|
+
if (owned.at === "clones")
|
|
4115
|
+
files.push({
|
|
4116
|
+
...owned,
|
|
4117
|
+
file: join13(root, "org", repo, ...owned.file.split("/")),
|
|
4118
|
+
label: `org/${repo}/${owned.file}`
|
|
4119
|
+
});
|
|
4120
|
+
}
|
|
4121
|
+
return files;
|
|
4122
|
+
}
|
|
4123
|
+
function declared(manifest) {
|
|
4124
|
+
return (manifest.agents ?? []).flatMap(
|
|
4125
|
+
(name) => Object.hasOwn(TARGETS, name) ? [TARGETS[name]] : []
|
|
4126
|
+
);
|
|
4127
|
+
}
|
|
4128
|
+
var BLOCK_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
4129
|
+
function writtenFiles(manifest) {
|
|
4130
|
+
return [
|
|
4131
|
+
...BLOCK_FILES,
|
|
4132
|
+
...declared(manifest).flatMap(
|
|
4133
|
+
(t) => t.files(PATHS_ONLY).filter((f) => f.at === "clones").map((f) => f.file)
|
|
4134
|
+
)
|
|
4135
|
+
];
|
|
4136
|
+
}
|
|
4137
|
+
async function agentTargets(root, manifest, opts) {
|
|
4138
|
+
const outcomes = [];
|
|
4139
|
+
for (const owned of await filesOf(root, manifest, declared(manifest))) {
|
|
3100
4140
|
const { file, label } = owned;
|
|
3101
4141
|
const existing = await readOrNull(file);
|
|
3102
4142
|
if ("content" in owned) {
|
|
@@ -3183,7 +4223,7 @@ var PROVIDERS = {
|
|
|
3183
4223
|
name: "github",
|
|
3184
4224
|
label: "GitHub",
|
|
3185
4225
|
available: true,
|
|
3186
|
-
open: ({ apiBase }) => githubProvider(apiBase)
|
|
4226
|
+
open: ({ apiBase, wait }) => githubProvider(apiBase, wait)
|
|
3187
4227
|
},
|
|
3188
4228
|
gitlab: { name: "gitlab", label: "GitLab", available: false },
|
|
3189
4229
|
atlassian: {
|
|
@@ -3398,7 +4438,7 @@ function targetsOf(manifest, root, only) {
|
|
|
3398
4438
|
const segments = entry.path.split("/");
|
|
3399
4439
|
targets.push({
|
|
3400
4440
|
scope: name,
|
|
3401
|
-
dir:
|
|
4441
|
+
dir: join14(root, ...segments),
|
|
3402
4442
|
depth: segments.length,
|
|
3403
4443
|
label: `${entry.path}/AGENTS.md`
|
|
3404
4444
|
});
|
|
@@ -3557,7 +4597,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
3557
4597
|
try {
|
|
3558
4598
|
const notCloned = /* @__PURE__ */ new Set();
|
|
3559
4599
|
for (const [name, repo] of Object.entries(manifest.repos)) {
|
|
3560
|
-
const dir =
|
|
4600
|
+
const dir = join14(ws.root, "org", name);
|
|
3561
4601
|
const label = `org/${name}`;
|
|
3562
4602
|
if (!await exists(dir)) {
|
|
3563
4603
|
if (!toClone.has(name)) {
|
|
@@ -3609,7 +4649,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
3609
4649
|
lines.push(
|
|
3610
4650
|
`not in rness.json: ${undeclared.join(", ")} (${rnessCommand()} add <name> declares it)`
|
|
3611
4651
|
);
|
|
3612
|
-
if (!await exists(
|
|
4652
|
+
if (!await exists(join14(ws.root, "org"))) {
|
|
3613
4653
|
lines.push(["skipped", "blocks (no org/ directory here)"]);
|
|
3614
4654
|
} else {
|
|
3615
4655
|
let differences = 0;
|
|
@@ -3621,7 +4661,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
3621
4661
|
continue;
|
|
3622
4662
|
}
|
|
3623
4663
|
const [agentsLinked, claudeLinked] = await Promise.all(
|
|
3624
|
-
BLOCK_FILES.map((f) => isSymlink(
|
|
4664
|
+
BLOCK_FILES.map((f) => isSymlink(join14(t.dir, f)))
|
|
3625
4665
|
);
|
|
3626
4666
|
const atRoot = t.scope === null;
|
|
3627
4667
|
if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
|
|
@@ -3644,7 +4684,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
3644
4684
|
`${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)`
|
|
3645
4685
|
);
|
|
3646
4686
|
}
|
|
3647
|
-
const agents = agentsLinked === true ? "skipped" : await syncBlockFile(
|
|
4687
|
+
const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join14(t.dir, "AGENTS.md"), block, check);
|
|
3648
4688
|
if (typeof agents === "object") {
|
|
3649
4689
|
problems.push(`${t.label}: ${agents.error}`);
|
|
3650
4690
|
continue;
|
|
@@ -3659,7 +4699,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
3659
4699
|
}
|
|
3660
4700
|
if (claudeLinked === true) continue;
|
|
3661
4701
|
const claude = await syncBlockFile(
|
|
3662
|
-
|
|
4702
|
+
join14(t.dir, "CLAUDE.md"),
|
|
3663
4703
|
block,
|
|
3664
4704
|
check,
|
|
3665
4705
|
true
|
|
@@ -3716,7 +4756,7 @@ async function syncCommand(opts, deps = {}) {
|
|
|
3716
4756
|
}
|
|
3717
4757
|
|
|
3718
4758
|
// src/commands/add.ts
|
|
3719
|
-
import { basename as
|
|
4759
|
+
import { basename as basename4, join as join15 } from "path";
|
|
3720
4760
|
function printDeclared(ui, result, already) {
|
|
3721
4761
|
for (const name of result.declared) {
|
|
3722
4762
|
if (already.has(name)) continue;
|
|
@@ -3751,7 +4791,7 @@ async function addCommand(spec, opts, deps = {}) {
|
|
|
3751
4791
|
}
|
|
3752
4792
|
if (parsed.name === CONTEXT_REPO) {
|
|
3753
4793
|
process.stderr.write(
|
|
3754
|
-
`${CONTEXT_REPO} is the workspace context, not a member repository \u2014 it is already cloned at ${
|
|
4794
|
+
`${CONTEXT_REPO} is the workspace context, not a member repository \u2014 it is already cloned at ${basename4(ws.root)}/${CONTEXT_REPO}
|
|
3755
4795
|
`
|
|
3756
4796
|
);
|
|
3757
4797
|
return 2;
|
|
@@ -3809,7 +4849,7 @@ async function addCommand(spec, opts, deps = {}) {
|
|
|
3809
4849
|
}
|
|
3810
4850
|
parsed = parseRepoSpec(spec, org, host);
|
|
3811
4851
|
const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
|
|
3812
|
-
const dir =
|
|
4852
|
+
const dir = join15(ws.root, "org", parsed.name);
|
|
3813
4853
|
const present = await exists(dir);
|
|
3814
4854
|
if (interactive) {
|
|
3815
4855
|
const { confirm, isCancel } = await terminal.prompts();
|
|
@@ -3885,7 +4925,7 @@ async function addCommand(spec, opts, deps = {}) {
|
|
|
3885
4925
|
}
|
|
3886
4926
|
|
|
3887
4927
|
// src/commands/context.ts
|
|
3888
|
-
import { posix as
|
|
4928
|
+
import { posix as posix6, relative, sep as sep3 } from "path";
|
|
3889
4929
|
function renderMarkdown(ctx) {
|
|
3890
4930
|
const parts = [];
|
|
3891
4931
|
for (const collection of ctx.collections) {
|
|
@@ -3917,7 +4957,7 @@ async function contextCommand(opts) {
|
|
|
3917
4957
|
}
|
|
3918
4958
|
scope = opts.scope;
|
|
3919
4959
|
} else {
|
|
3920
|
-
const rel = relative(ws.root, cwd).split(sep3).join(
|
|
4960
|
+
const rel = relative(ws.root, cwd).split(sep3).join(posix6.sep);
|
|
3921
4961
|
scope = resolveScope(manifest, rel);
|
|
3922
4962
|
}
|
|
3923
4963
|
const ctx = await assembleContext({
|
|
@@ -3935,56 +4975,9 @@ async function contextCommand(opts) {
|
|
|
3935
4975
|
}
|
|
3936
4976
|
}
|
|
3937
4977
|
|
|
3938
|
-
// src/core/delegate.ts
|
|
3939
|
-
import { readFile as readFile6 } from "fs/promises";
|
|
3940
|
-
import { join as join14 } from "path";
|
|
3941
|
-
var DEFAULT_BIN = "dist/bin/rness.js";
|
|
3942
|
-
async function readPinnedCli(rnessDir) {
|
|
3943
|
-
const dir = join14(rnessDir, "node_modules", "@rness", "cli");
|
|
3944
|
-
let pkg;
|
|
3945
|
-
try {
|
|
3946
|
-
pkg = JSON.parse(await readFile6(join14(dir, "package.json"), "utf8"));
|
|
3947
|
-
} catch {
|
|
3948
|
-
return null;
|
|
3949
|
-
}
|
|
3950
|
-
const declared2 = pkg.bin;
|
|
3951
|
-
let bin = DEFAULT_BIN;
|
|
3952
|
-
if (typeof declared2 === "string") bin = declared2;
|
|
3953
|
-
else if (declared2 !== null && typeof declared2 === "object") {
|
|
3954
|
-
const named = declared2["rness"];
|
|
3955
|
-
if (typeof named === "string") bin = named;
|
|
3956
|
-
}
|
|
3957
|
-
return {
|
|
3958
|
-
dir,
|
|
3959
|
-
bin,
|
|
3960
|
-
version: typeof pkg.version === "string" ? pkg.version : null
|
|
3961
|
-
};
|
|
3962
|
-
}
|
|
3963
|
-
async function pinnedBinPath(rnessDir) {
|
|
3964
|
-
const pinned = await readPinnedCli(rnessDir);
|
|
3965
|
-
if (pinned === null) return null;
|
|
3966
|
-
return join14(pinned.dir, ...pinned.bin.split("/"));
|
|
3967
|
-
}
|
|
3968
|
-
async function findDelegate(cwd, ownVersion) {
|
|
3969
|
-
let root;
|
|
3970
|
-
let rnessDir;
|
|
3971
|
-
try {
|
|
3972
|
-
const ws = await findWorkspace(cwd);
|
|
3973
|
-
root = ws.root;
|
|
3974
|
-
rnessDir = ws.rnessDir;
|
|
3975
|
-
} catch {
|
|
3976
|
-
return null;
|
|
3977
|
-
}
|
|
3978
|
-
const pinned = await readPinnedCli(rnessDir);
|
|
3979
|
-
if (pinned === null) return null;
|
|
3980
|
-
const { version } = pinned;
|
|
3981
|
-
if (version === null || version === ownVersion) return null;
|
|
3982
|
-
return { entry: join14(pinned.dir, "dist", "index.js"), version, root };
|
|
3983
|
-
}
|
|
3984
|
-
|
|
3985
4978
|
// src/core/scaffold.ts
|
|
3986
4979
|
import { statSync } from "fs";
|
|
3987
|
-
import { dirname as dirname5, join as
|
|
4980
|
+
import { dirname as dirname5, join as join16 } from "path";
|
|
3988
4981
|
import { fileURLToPath } from "url";
|
|
3989
4982
|
var SCAFFOLD_FILES = [
|
|
3990
4983
|
"AGENTS.md",
|
|
@@ -4008,9 +5001,9 @@ var SCAFFOLD_FILES = [
|
|
|
4008
5001
|
function scaffoldDir() {
|
|
4009
5002
|
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
4010
5003
|
for (let i = 0; i < 4; i += 1) {
|
|
4011
|
-
const candidate =
|
|
5004
|
+
const candidate = join16(dir, "scaffold");
|
|
4012
5005
|
try {
|
|
4013
|
-
if (statSync(
|
|
5006
|
+
if (statSync(join16(candidate, "rness.json")).isFile()) return candidate;
|
|
4014
5007
|
} catch {
|
|
4015
5008
|
}
|
|
4016
5009
|
dir = dirname5(dir);
|
|
@@ -4019,15 +5012,15 @@ function scaffoldDir() {
|
|
|
4019
5012
|
}
|
|
4020
5013
|
|
|
4021
5014
|
// src/core/scaffold-copy.ts
|
|
4022
|
-
import { chmod as chmod2, mkdir as mkdir3, readFile as
|
|
4023
|
-
import { dirname as dirname6, join as
|
|
5015
|
+
import { chmod as chmod2, mkdir as mkdir3, readFile as readFile8, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
5016
|
+
import { dirname as dirname6, join as join17 } from "path";
|
|
4024
5017
|
var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
|
|
4025
5018
|
async function renderScaffold(source, tokens) {
|
|
4026
5019
|
const files = [];
|
|
4027
5020
|
for (const rel of SCAFFOLD_FILES) {
|
|
4028
5021
|
if (rel === "rness.json") continue;
|
|
4029
|
-
const from =
|
|
4030
|
-
let content = await
|
|
5022
|
+
const from = join17(source, ...rel.split("/"));
|
|
5023
|
+
let content = await readFile8(from, "utf8");
|
|
4031
5024
|
if (TOKENISED.has(rel)) {
|
|
4032
5025
|
content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
|
|
4033
5026
|
}
|
|
@@ -4041,7 +5034,7 @@ async function renderScaffold(source, tokens) {
|
|
|
4041
5034
|
}
|
|
4042
5035
|
async function copyScaffold(dest, tokens) {
|
|
4043
5036
|
for (const file of await renderScaffold(scaffoldDir(), tokens)) {
|
|
4044
|
-
const to =
|
|
5037
|
+
const to = join17(dest, ...file.path.split("/"));
|
|
4045
5038
|
await mkdir3(dirname6(to), { recursive: true });
|
|
4046
5039
|
await writeFile2(to, file.content, "utf8");
|
|
4047
5040
|
if (file.executable) await chmod2(to, 493);
|
|
@@ -4053,16 +5046,16 @@ import { spawn as spawn3 } from "child_process";
|
|
|
4053
5046
|
import { realpath } from "fs/promises";
|
|
4054
5047
|
|
|
4055
5048
|
// src/core/setup-git.ts
|
|
4056
|
-
import { execFile as
|
|
4057
|
-
import { promisify as
|
|
4058
|
-
var
|
|
5049
|
+
import { execFile as execFile4 } from "child_process";
|
|
5050
|
+
import { promisify as promisify4 } from "util";
|
|
5051
|
+
var execFileP4 = promisify4(execFile4);
|
|
4059
5052
|
var KEY = "credential.https://github.com.helper";
|
|
4060
5053
|
function helperValue(node, bin) {
|
|
4061
5054
|
return `!'${node}' '${bin}' git-credential`;
|
|
4062
5055
|
}
|
|
4063
5056
|
var isOurs = (value) => /^!'[^']+' '[^']*rness[^']*' git-credential$/.test(value);
|
|
4064
5057
|
async function config(args) {
|
|
4065
|
-
const { stdout } = await
|
|
5058
|
+
const { stdout } = await execFileP4("git", ["config", "--global", ...args]);
|
|
4066
5059
|
return stdout;
|
|
4067
5060
|
}
|
|
4068
5061
|
async function values() {
|
|
@@ -4096,400 +5089,149 @@ async function removeGitSetup() {
|
|
|
4096
5089
|
}
|
|
4097
5090
|
|
|
4098
5091
|
// src/commands/login.ts
|
|
4099
|
-
function openInBrowser(url) {
|
|
4100
|
-
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
4101
|
-
try {
|
|
4102
|
-
const child = spawn3(command, args, { stdio: "ignore", detached: true });
|
|
4103
|
-
child.on("error", () => void 0);
|
|
4104
|
-
child.unref();
|
|
4105
|
-
} catch {
|
|
4106
|
-
}
|
|
4107
|
-
}
|
|
4108
|
-
async function offerSetupGit(opts, terminal, bin, ui) {
|
|
4109
|
-
if (opts.setupGit === false) return;
|
|
4110
|
-
if (!isStableInstall(bin)) {
|
|
4111
|
-
ui.hint(
|
|
4112
|
-
"install rness globally to let git use your login: npm i -g @rness/cli"
|
|
4113
|
-
);
|
|
4114
|
-
return;
|
|
4115
|
-
}
|
|
4116
|
-
if (opts.setupGit !== true) {
|
|
4117
|
-
if (!terminal.isTty()) return;
|
|
4118
|
-
const p = await terminal.prompts();
|
|
4119
|
-
const ok = await p.confirm({
|
|
4120
|
-
message: "Use rness to authenticate git over HTTPS for github.com?",
|
|
4121
|
-
initialValue: true
|
|
4122
|
-
});
|
|
4123
|
-
if (p.isCancel(ok) || ok !== true) return;
|
|
4124
|
-
}
|
|
4125
|
-
const changed = await setupGit(process.execPath, bin);
|
|
4126
|
-
ui.line(
|
|
4127
|
-
changed ? "updated" : "unchanged",
|
|
4128
|
-
`git: rness answers for https://github.com (${rnessCommand()} logout undoes it)`,
|
|
4129
|
-
`git now asks rness for your github.com login over HTTPS (${rnessCommand()} logout undoes it)`
|
|
4130
|
-
);
|
|
4131
|
-
}
|
|
4132
|
-
async function manifestAround(cwd) {
|
|
4133
|
-
try {
|
|
4134
|
-
return await loadManifest((await findWorkspace(cwd)).rnessDir);
|
|
4135
|
-
} catch {
|
|
4136
|
-
return null;
|
|
4137
|
-
}
|
|
4138
|
-
}
|
|
4139
|
-
function splitScopes(scopes) {
|
|
4140
|
-
return scopes.split(/[\s,]+/).filter((s) => s !== "");
|
|
4141
|
-
}
|
|
4142
|
-
async function loginCommand(opts, deps = {}) {
|
|
4143
|
-
const terminal = deps.terminal ?? defaultTerminal;
|
|
4144
|
-
const own = deps.ui === void 0;
|
|
4145
|
-
const ui = deps.ui ?? await makeUi(terminal);
|
|
4146
|
-
if (own) ui.intro("Log in to GitHub");
|
|
4147
|
-
try {
|
|
4148
|
-
const manifest = await manifestAround(deps.cwd ?? process.cwd());
|
|
4149
|
-
if (manifest?.provider != null && !PROVIDERS[manifest.provider].available) {
|
|
4150
|
-
ui.error(unsupportedProviderMessage(manifest.provider));
|
|
4151
|
-
return 1;
|
|
4152
|
-
}
|
|
4153
|
-
const needsProject = opts.project === true || manifest?.pulse != null;
|
|
4154
|
-
const bin = deps.bin ?? await realpath(process.argv[1] ?? "").catch(() => "");
|
|
4155
|
-
const env = envToken();
|
|
4156
|
-
if (env !== null)
|
|
4157
|
-
ui.hint(`${env.source} is set and wins over a stored login`);
|
|
4158
|
-
const stored = await readAuth();
|
|
4159
|
-
const resolved = stored === null ? null : await resolveToken();
|
|
4160
|
-
const lacksProject = needsProject && resolved?.source === "login" && !splitScopes(stored?.tokens.scopes ?? "").includes(PULSE_SCOPE);
|
|
4161
|
-
if (stored !== null && resolved !== null && !lacksProject) {
|
|
4162
|
-
ui.line(
|
|
4163
|
-
"logged in",
|
|
4164
|
-
`as ${stored.login} (github.com); ${rnessCommand()} logout to switch`
|
|
4165
|
-
);
|
|
4166
|
-
await offerSetupGit(opts, terminal, bin, ui);
|
|
4167
|
-
if (own) ui.outro("Nothing to do", "idle");
|
|
4168
|
-
return 0;
|
|
4169
|
-
}
|
|
4170
|
-
if (lacksProject) ui.line("login", "logging in again for the project scope");
|
|
4171
|
-
const code = await requestDeviceCode(
|
|
4172
|
-
needsProject ? `${SCOPES} ${PULSE_SCOPE}` : SCOPES
|
|
4173
|
-
);
|
|
4174
|
-
if (ui.session)
|
|
4175
|
-
ui.line(
|
|
4176
|
-
"open",
|
|
4177
|
-
code.verificationUri,
|
|
4178
|
-
`Open ${code.verificationUri} and enter the code ${code.userCode}`
|
|
4179
|
-
);
|
|
4180
|
-
else {
|
|
4181
|
-
ui.line("open", code.verificationUri);
|
|
4182
|
-
ui.line("code", code.userCode);
|
|
4183
|
-
}
|
|
4184
|
-
if (terminal.isTty() && process.env["SSH_CONNECTION"] === void 0)
|
|
4185
|
-
(deps.open ?? openInBrowser)(code.verificationUri);
|
|
4186
|
-
if (!ui.session)
|
|
4187
|
-
ui.line("waiting", "for you to approve rness on github.com\u2026");
|
|
4188
|
-
const login = await ui.step(
|
|
4189
|
-
{
|
|
4190
|
-
doing: "waiting for you to approve rness on github.com",
|
|
4191
|
-
sentence: "Waiting for your approval on github.com",
|
|
4192
|
-
quiet: true
|
|
4193
|
-
},
|
|
4194
|
-
async () => {
|
|
4195
|
-
const tokens = await pollForToken(code, deps);
|
|
4196
|
-
const user = await getUser({
|
|
4197
|
-
token: tokens.accessToken,
|
|
4198
|
-
apiBase: opts.githubApi ?? DEFAULT_GITHUB_API
|
|
4199
|
-
});
|
|
4200
|
-
await writeAuth({ login: user.login, tokens });
|
|
4201
|
-
return user.login;
|
|
4202
|
-
},
|
|
4203
|
-
(name) => ["logged in", `as ${name} (github.com)`]
|
|
4204
|
-
);
|
|
4205
|
-
if (!ui.session) ui.line("logged in", `as ${login} (github.com)`);
|
|
4206
|
-
await offerSetupGit(opts, terminal, bin, ui);
|
|
4207
|
-
if (own) ui.outro(`You are logged in as ${login}`);
|
|
4208
|
-
return 0;
|
|
4209
|
-
} catch (e) {
|
|
4210
|
-
if (e instanceof LoginError) {
|
|
4211
|
-
ui.error(e.message);
|
|
4212
|
-
return 1;
|
|
4213
|
-
}
|
|
4214
|
-
return reportError(e);
|
|
4215
|
-
}
|
|
4216
|
-
}
|
|
4217
|
-
function revokeUrl() {
|
|
4218
|
-
return `https://github.com/settings/connections/applications/${clientId()}`;
|
|
4219
|
-
}
|
|
4220
|
-
|
|
4221
|
-
// src/commands/create.ts
|
|
4222
|
-
import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm6 } from "fs/promises";
|
|
4223
|
-
import { tmpdir as tmpdir2 } from "os";
|
|
4224
|
-
import { join as join21, relative as relative4, resolve as resolve5 } from "path";
|
|
4225
|
-
|
|
4226
|
-
// src/core/ask-provider.ts
|
|
4227
|
-
var PROVIDER_QUESTION = "Where does your organization live?";
|
|
4228
|
-
async function askProvider(p) {
|
|
4229
|
-
return p.select({
|
|
4230
|
-
message: PROVIDER_QUESTION,
|
|
4231
|
-
options: Object.values(PROVIDERS).map((entry) => ({
|
|
4232
|
-
value: entry.name,
|
|
4233
|
-
label: entry.available ? entry.label : `${entry.label} (coming later)`,
|
|
4234
|
-
disabled: !entry.available
|
|
4235
|
-
}))
|
|
4236
|
-
});
|
|
4237
|
-
}
|
|
4238
|
-
|
|
4239
|
-
// src/core/new-workspace.ts
|
|
4240
|
-
import { mkdir as mkdir4, readdir as readdir3, rm as rm5, stat as stat3 } from "fs/promises";
|
|
4241
|
-
import { dirname as dirname8, join as join19, relative as relative2 } from "path";
|
|
4242
|
-
|
|
4243
|
-
// src/core/pinned.ts
|
|
4244
|
-
import { execFile as execFile4 } from "child_process";
|
|
4245
|
-
import { readFile as readFile8 } from "fs/promises";
|
|
4246
|
-
import { basename as basename4, join as join17 } from "path";
|
|
4247
|
-
import { promisify as promisify4 } from "util";
|
|
4248
|
-
var execFileP4 = promisify4(execFile4);
|
|
4249
|
-
var PACKAGE = "@rness/cli";
|
|
4250
|
-
var EXACT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
|
|
4251
|
-
function compareVersions(a, b) {
|
|
4252
|
-
const split = (v) => {
|
|
4253
|
-
const dash = v.indexOf("-");
|
|
4254
|
-
const core = dash === -1 ? v : v.slice(0, dash);
|
|
4255
|
-
return [core.split(".").map(Number), dash === -1 ? null : v.slice(dash + 1)];
|
|
4256
|
-
};
|
|
4257
|
-
const [coreA, preA] = split(a);
|
|
4258
|
-
const [coreB, preB] = split(b);
|
|
4259
|
-
for (let i = 0; i < 3; i += 1) {
|
|
4260
|
-
const diff = (coreA[i] ?? 0) - (coreB[i] ?? 0);
|
|
4261
|
-
if (diff !== 0) return diff;
|
|
4262
|
-
}
|
|
4263
|
-
if (preA === preB) return 0;
|
|
4264
|
-
if (preA === null) return 1;
|
|
4265
|
-
if (preB === null) return -1;
|
|
4266
|
-
return preA.localeCompare(preB, "en", { numeric: true });
|
|
4267
|
-
}
|
|
4268
|
-
async function readPackageJson(rnessDir) {
|
|
4269
|
-
try {
|
|
4270
|
-
const parsed = JSON.parse(
|
|
4271
|
-
await readFile8(join17(rnessDir, "package.json"), "utf8")
|
|
4272
|
-
);
|
|
4273
|
-
return parsed !== null && typeof parsed === "object" ? parsed : null;
|
|
4274
|
-
} catch {
|
|
4275
|
-
return null;
|
|
4276
|
-
}
|
|
4277
|
-
}
|
|
4278
|
-
async function readPin(rnessDir) {
|
|
4279
|
-
const pkg = await readPackageJson(rnessDir);
|
|
4280
|
-
if (pkg === null) return null;
|
|
4281
|
-
for (const field of ["devDependencies", "dependencies"]) {
|
|
4282
|
-
const deps = pkg[field];
|
|
4283
|
-
if (deps === null || typeof deps !== "object") continue;
|
|
4284
|
-
const spec = deps[PACKAGE];
|
|
4285
|
-
if (typeof spec === "string") return { spec, field };
|
|
4286
|
-
}
|
|
4287
|
-
return null;
|
|
4288
|
-
}
|
|
4289
|
-
async function writePin(rnessDir, from, to) {
|
|
4290
|
-
const file = join17(rnessDir, "package.json");
|
|
4291
|
-
const text = await readFile8(file, "utf8");
|
|
4292
|
-
const entry = new RegExp(
|
|
4293
|
-
`("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
|
|
4294
|
-
);
|
|
4295
|
-
if (!entry.test(text))
|
|
4296
|
-
throw new Error(`"${PACKAGE}": "${from}" not found in package.json`);
|
|
4297
|
-
await writeFileAtomic(file, text.replace(entry, `$1${to}$2`));
|
|
4298
|
-
}
|
|
4299
|
-
var LOCKFILES = [
|
|
4300
|
-
["pnpm-lock.yaml", "pnpm"],
|
|
4301
|
-
["yarn.lock", "yarn"],
|
|
4302
|
-
["bun.lock", "bun"],
|
|
4303
|
-
["bun.lockb", "bun"],
|
|
4304
|
-
["package-lock.json", "npm"]
|
|
4305
|
-
];
|
|
4306
|
-
async function workspacePackageManager(rnessDir) {
|
|
4307
|
-
const declared2 = (await readPackageJson(rnessDir))?.["packageManager"];
|
|
4308
|
-
if (typeof declared2 === "string") {
|
|
4309
|
-
const name = declared2.split("@")[0] ?? "";
|
|
4310
|
-
if (isPackageManager(name)) return name;
|
|
4311
|
-
}
|
|
4312
|
-
for (const [lockfile, pm] of LOCKFILES)
|
|
4313
|
-
if (await exists(join17(rnessDir, lockfile))) return pm;
|
|
4314
|
-
return "npm";
|
|
4315
|
-
}
|
|
4316
|
-
async function hasLockfile(rnessDir, pm) {
|
|
4317
|
-
for (const [lockfile, owner] of LOCKFILES)
|
|
4318
|
-
if (owner === pm && await exists(join17(rnessDir, lockfile))) return true;
|
|
4319
|
-
return false;
|
|
4320
|
-
}
|
|
4321
|
-
async function pinDrift(rnessDir) {
|
|
4322
|
-
const pin = await readPin(rnessDir);
|
|
4323
|
-
if (pin === null || !EXACT_VERSION.test(pin.spec)) return null;
|
|
4324
|
-
const installed = (await readPinnedCli(rnessDir))?.version ?? null;
|
|
4325
|
-
return installed === pin.spec ? null : { pin: pin.spec, installed };
|
|
4326
|
-
}
|
|
4327
|
-
async function driftWarning(cwd) {
|
|
4328
|
-
let root;
|
|
4329
|
-
let rnessDir;
|
|
4330
|
-
try {
|
|
4331
|
-
const ws = await findWorkspace(cwd);
|
|
4332
|
-
root = ws.root;
|
|
4333
|
-
rnessDir = ws.rnessDir;
|
|
4334
|
-
} catch {
|
|
4335
|
-
return null;
|
|
4336
|
-
}
|
|
4337
|
-
const drift = await pinDrift(rnessDir);
|
|
4338
|
-
if (drift === null) return null;
|
|
4339
|
-
const pm = await workspacePackageManager(rnessDir);
|
|
4340
|
-
return warn(
|
|
4341
|
-
`${basename4(root)}/.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${pm} install in .rness`
|
|
4342
|
-
);
|
|
4343
|
-
}
|
|
4344
|
-
var CHILD_DEADLINE_MS = 12e4;
|
|
4345
|
-
function childEnv(env = process.env) {
|
|
4346
|
-
return {
|
|
4347
|
-
...env,
|
|
4348
|
-
GIT_SSH_COMMAND: `${env["GIT_SSH_COMMAND"] ?? "ssh"} -o BatchMode=yes`,
|
|
4349
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
4350
|
-
};
|
|
4351
|
-
}
|
|
4352
|
-
async function ensureInstalled(rnessDir, opts = {}) {
|
|
4353
|
-
const drift = await pinDrift(rnessDir);
|
|
4354
|
-
if (drift === null) return null;
|
|
4355
|
-
const env = opts.env ?? process.env;
|
|
4356
|
-
if (env["RNESS_NO_INSTALL"] === "1")
|
|
4357
|
-
return { installed: false, pin: drift.pin, reason: "RNESS_NO_INSTALL=1" };
|
|
4358
|
-
const pm = await workspacePackageManager(rnessDir);
|
|
4359
|
-
const frozen = await hasLockfile(rnessDir, pm);
|
|
4360
|
-
const install = opts.install ?? installDependencies;
|
|
4361
|
-
opts.announce?.(drift.pin);
|
|
4362
|
-
try {
|
|
4363
|
-
await install(pm, rnessDir, { frozen });
|
|
4364
|
-
return { installed: true, pin: drift.pin, reason: null };
|
|
4365
|
-
} catch (e) {
|
|
4366
|
-
return {
|
|
4367
|
-
installed: false,
|
|
4368
|
-
pin: drift.pin,
|
|
4369
|
-
reason: e instanceof Error ? e.message : String(e)
|
|
4370
|
-
};
|
|
4371
|
-
}
|
|
4372
|
-
}
|
|
4373
|
-
async function runPinnedSync(root) {
|
|
4374
|
-
const installed = await readPinnedCli(join17(root, ".rness"));
|
|
4375
|
-
if (installed === null) return null;
|
|
4376
|
-
const pinned = join17(installed.dir, ...installed.bin.split("/"));
|
|
4377
|
-
if (!await exists(pinned))
|
|
4378
|
-
return {
|
|
4379
|
-
code: 1,
|
|
4380
|
-
stdout: "",
|
|
4381
|
-
stderr: `@rness/cli in .rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
|
|
4382
|
-
`
|
|
4383
|
-
};
|
|
4384
|
-
try {
|
|
4385
|
-
const { stdout, stderr } = await execFileP4(
|
|
4386
|
-
process.execPath,
|
|
4387
|
-
[pinned, "sync", "--yes"],
|
|
4388
|
-
{
|
|
4389
|
-
cwd: root,
|
|
4390
|
-
maxBuffer: 16 * 1024 * 1024,
|
|
4391
|
-
env: childEnv(),
|
|
4392
|
-
timeout: CHILD_DEADLINE_MS,
|
|
4393
|
-
killSignal: "SIGKILL"
|
|
4394
|
-
}
|
|
4395
|
-
);
|
|
4396
|
-
return { code: 0, stdout, stderr };
|
|
4397
|
-
} catch (e) {
|
|
4398
|
-
const err = e;
|
|
4399
|
-
const deadline = err.killed === true ? `rness sync in .rness did not finish within ${CHILD_DEADLINE_MS / 1e3}s and was stopped
|
|
4400
|
-
` : "";
|
|
4401
|
-
return {
|
|
4402
|
-
code: typeof err.code === "number" ? err.code : 1,
|
|
4403
|
-
stdout: err.stdout ?? "",
|
|
4404
|
-
stderr: `${err.stderr ?? ""}${deadline}`
|
|
4405
|
-
};
|
|
4406
|
-
}
|
|
4407
|
-
}
|
|
4408
|
-
var SYNC_VERBS = ["updated", "unchanged", "stale"];
|
|
4409
|
-
function counted(noun, counts) {
|
|
4410
|
-
const total = [...counts.values()].reduce((a, b) => a + b, 0);
|
|
4411
|
-
const detail = SYNC_VERBS.filter((verb) => counts.has(verb)).map((verb) => `${counts.get(verb) ?? 0} ${verb}`).join(", ");
|
|
4412
|
-
return `${total} ${noun}${total === 1 ? "" : "s"}${detail === "" ? "" : `: ${detail}`}`;
|
|
4413
|
-
}
|
|
4414
|
-
function summariseSync(stdout) {
|
|
4415
|
-
const blocks = /* @__PURE__ */ new Map();
|
|
4416
|
-
const agentFiles = /* @__PURE__ */ new Map();
|
|
4417
|
-
const others = [];
|
|
4418
|
-
for (const line3 of stdout.split("\n")) {
|
|
4419
|
-
if (line3.trim() === "") continue;
|
|
4420
|
-
const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line3);
|
|
4421
|
-
if (m === null) {
|
|
4422
|
-
others.push(line3);
|
|
4423
|
-
continue;
|
|
4424
|
-
}
|
|
4425
|
-
const [, verb = "", path = ""] = m;
|
|
4426
|
-
const counts = /(^|\/)(AGENTS|CLAUDE)\.md$/.test(path) ? blocks : agentFiles;
|
|
4427
|
-
counts.set(verb, (counts.get(verb) ?? 0) + 1);
|
|
4428
|
-
}
|
|
4429
|
-
return {
|
|
4430
|
-
summary: [
|
|
4431
|
-
counted("block", blocks),
|
|
4432
|
-
...agentFiles.size > 0 ? [counted("agent file", agentFiles)] : []
|
|
4433
|
-
].join("; "),
|
|
4434
|
-
others
|
|
4435
|
-
};
|
|
4436
|
-
}
|
|
4437
|
-
function syncOutcome(result, opts = {}) {
|
|
4438
|
-
const { others } = summariseSync(result.stdout);
|
|
4439
|
-
const wrote = /^(updated|unchanged|stale)\s/m.test(result.stdout);
|
|
4440
|
-
const stderr = result.stderr.split("\n").map((l) => l.trimEnd()).filter((l) => l.trim() !== "");
|
|
4441
|
-
const hints = wrote ? [...others, ...stderr] : others;
|
|
4442
|
-
if (opts.sshWorks === true && /Permission denied \(publickey\)/.test(result.stderr))
|
|
4443
|
-
hints.push(...agentHintLines());
|
|
4444
|
-
return {
|
|
4445
|
-
code: wrote ? 0 : result.code,
|
|
4446
|
-
hints,
|
|
4447
|
-
errors: wrote ? [] : stderr
|
|
4448
|
-
};
|
|
5092
|
+
function openInBrowser(url) {
|
|
5093
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url]] : ["xdg-open", [url]];
|
|
5094
|
+
try {
|
|
5095
|
+
const child = spawn3(command, args, { stdio: "ignore", detached: true });
|
|
5096
|
+
child.on("error", () => void 0);
|
|
5097
|
+
child.unref();
|
|
5098
|
+
} catch {
|
|
5099
|
+
}
|
|
4449
5100
|
}
|
|
4450
|
-
async function
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
process.stderr.write(
|
|
4456
|
-
`@rness/cli is not installed in ${shown}/.rness; install in .rness/, then ${rnessCommand()} sync
|
|
4457
|
-
`
|
|
5101
|
+
async function offerSetupGit(opts, terminal, bin, ui) {
|
|
5102
|
+
if (opts.setupGit === false) return;
|
|
5103
|
+
if (!isStableInstall(bin)) {
|
|
5104
|
+
ui.hint(
|
|
5105
|
+
"install rness globally to let git use your login: npm i -g @rness/cli"
|
|
4458
5106
|
);
|
|
4459
|
-
return
|
|
5107
|
+
return;
|
|
4460
5108
|
}
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
5109
|
+
if (opts.setupGit !== true) {
|
|
5110
|
+
if (!terminal.isTty()) return;
|
|
5111
|
+
const p = await terminal.prompts();
|
|
5112
|
+
const ok = await p.confirm({
|
|
5113
|
+
message: "Use rness to authenticate git over HTTPS for github.com?",
|
|
5114
|
+
initialValue: true
|
|
5115
|
+
});
|
|
5116
|
+
if (p.isCancel(ok) || ok !== true) return;
|
|
4468
5117
|
}
|
|
5118
|
+
const changed = await setupGit(process.execPath, bin);
|
|
5119
|
+
ui.line(
|
|
5120
|
+
changed ? "updated" : "unchanged",
|
|
5121
|
+
`git: rness answers for https://github.com (${rnessCommand()} logout undoes it)`,
|
|
5122
|
+
`git now asks rness for your github.com login over HTTPS (${rnessCommand()} logout undoes it)`
|
|
5123
|
+
);
|
|
5124
|
+
}
|
|
5125
|
+
async function manifestAround(cwd) {
|
|
4469
5126
|
try {
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
5127
|
+
return await loadManifest((await findWorkspace(cwd)).rnessDir);
|
|
5128
|
+
} catch {
|
|
5129
|
+
return null;
|
|
5130
|
+
}
|
|
5131
|
+
}
|
|
5132
|
+
function splitScopes(scopes) {
|
|
5133
|
+
return scopes.split(/[\s,]+/).filter((s) => s !== "");
|
|
5134
|
+
}
|
|
5135
|
+
async function loginCommand(opts, deps = {}) {
|
|
5136
|
+
const terminal = deps.terminal ?? defaultTerminal;
|
|
5137
|
+
const own = deps.ui === void 0;
|
|
5138
|
+
const ui = deps.ui ?? await makeUi(terminal);
|
|
5139
|
+
if (own) ui.intro("Log in to GitHub");
|
|
5140
|
+
try {
|
|
5141
|
+
const manifest = await manifestAround(deps.cwd ?? process.cwd());
|
|
5142
|
+
if (manifest?.provider != null && !PROVIDERS[manifest.provider].available) {
|
|
5143
|
+
ui.error(unsupportedProviderMessage(manifest.provider));
|
|
5144
|
+
return 1;
|
|
5145
|
+
}
|
|
5146
|
+
const needsProject = opts.project === true || manifest?.pulse != null;
|
|
5147
|
+
const bin = deps.bin ?? await realpath(process.argv[1] ?? "").catch(() => "");
|
|
5148
|
+
const env = envToken();
|
|
5149
|
+
if (env !== null)
|
|
5150
|
+
ui.hint(`${env.source} is set and wins over a stored login`);
|
|
5151
|
+
const stored = await readAuth();
|
|
5152
|
+
const resolved = stored === null ? null : await resolveToken();
|
|
5153
|
+
const lacksProject = needsProject && resolved?.source === "login" && !splitScopes(stored?.tokens.scopes ?? "").includes(PULSE_SCOPE);
|
|
5154
|
+
if (stored !== null && resolved !== null && !lacksProject) {
|
|
5155
|
+
ui.line(
|
|
5156
|
+
"logged in",
|
|
5157
|
+
`as ${stored.login} (github.com); ${rnessCommand()} logout to switch`
|
|
5158
|
+
);
|
|
5159
|
+
await offerSetupGit(opts, terminal, bin, ui);
|
|
5160
|
+
if (own) ui.outro("Nothing to do", "idle");
|
|
5161
|
+
return 0;
|
|
5162
|
+
}
|
|
5163
|
+
if (lacksProject) ui.line("login", "logging in again for the project scope");
|
|
5164
|
+
const code = await requestDeviceCode(
|
|
5165
|
+
needsProject ? `${SCOPES} ${PULSE_SCOPE}` : SCOPES
|
|
5166
|
+
);
|
|
5167
|
+
if (ui.session)
|
|
5168
|
+
ui.line(
|
|
5169
|
+
"open",
|
|
5170
|
+
code.verificationUri,
|
|
5171
|
+
`Open ${code.verificationUri} and enter the code ${code.userCode}`
|
|
5172
|
+
);
|
|
5173
|
+
else {
|
|
5174
|
+
ui.line("open", code.verificationUri);
|
|
5175
|
+
ui.line("code", code.userCode);
|
|
5176
|
+
}
|
|
5177
|
+
if (terminal.isTty() && process.env["SSH_CONNECTION"] === void 0)
|
|
5178
|
+
(deps.open ?? openInBrowser)(code.verificationUri);
|
|
5179
|
+
if (!ui.session)
|
|
5180
|
+
ui.line("waiting", "for you to approve rness on github.com\u2026");
|
|
5181
|
+
const login = await ui.step(
|
|
4473
5182
|
{
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
5183
|
+
doing: "waiting for you to approve rness on github.com",
|
|
5184
|
+
sentence: "Waiting for your approval on github.com",
|
|
5185
|
+
quiet: true
|
|
5186
|
+
},
|
|
5187
|
+
async () => {
|
|
5188
|
+
const tokens = await pollForToken(code, deps);
|
|
5189
|
+
const user = await getUser({
|
|
5190
|
+
token: tokens.accessToken,
|
|
5191
|
+
apiBase: opts.githubApi ?? DEFAULT_GITHUB_API
|
|
5192
|
+
});
|
|
5193
|
+
await writeAuth({ login: user.login, tokens });
|
|
5194
|
+
return user.login;
|
|
5195
|
+
},
|
|
5196
|
+
(name) => ["logged in", `as ${name} (github.com)`]
|
|
4480
5197
|
);
|
|
4481
|
-
|
|
4482
|
-
|
|
5198
|
+
if (!ui.session) ui.line("logged in", `as ${login} (github.com)`);
|
|
5199
|
+
await offerSetupGit(opts, terminal, bin, ui);
|
|
5200
|
+
if (own) ui.outro(`You are logged in as ${login}`);
|
|
4483
5201
|
return 0;
|
|
4484
5202
|
} catch (e) {
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
return typeof err.code === "number" ? err.code : 1;
|
|
5203
|
+
if (e instanceof LoginError) {
|
|
5204
|
+
ui.error(e.message);
|
|
5205
|
+
return 1;
|
|
5206
|
+
}
|
|
5207
|
+
return reportError(e);
|
|
4491
5208
|
}
|
|
4492
5209
|
}
|
|
5210
|
+
function revokeUrl() {
|
|
5211
|
+
return `https://github.com/settings/connections/applications/${clientId()}`;
|
|
5212
|
+
}
|
|
5213
|
+
|
|
5214
|
+
// src/commands/create.ts
|
|
5215
|
+
import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm6 } from "fs/promises";
|
|
5216
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
5217
|
+
import { join as join21, relative as relative4, resolve as resolve5 } from "path";
|
|
5218
|
+
|
|
5219
|
+
// src/core/ask-provider.ts
|
|
5220
|
+
var PROVIDER_QUESTION = "Where does your organization live?";
|
|
5221
|
+
async function askProvider(p) {
|
|
5222
|
+
return p.select({
|
|
5223
|
+
message: PROVIDER_QUESTION,
|
|
5224
|
+
options: Object.values(PROVIDERS).map((entry) => ({
|
|
5225
|
+
value: entry.name,
|
|
5226
|
+
label: entry.available ? entry.label : `${entry.label} (coming later)`,
|
|
5227
|
+
disabled: !entry.available
|
|
5228
|
+
}))
|
|
5229
|
+
});
|
|
5230
|
+
}
|
|
5231
|
+
|
|
5232
|
+
// src/core/new-workspace.ts
|
|
5233
|
+
import { mkdir as mkdir4, readdir as readdir3, rm as rm5, stat as stat3 } from "fs/promises";
|
|
5234
|
+
import { dirname as dirname8, join as join19, relative as relative2 } from "path";
|
|
4493
5235
|
|
|
4494
5236
|
// src/core/scaffold-git.ts
|
|
4495
5237
|
import { execFile as execFile5 } from "child_process";
|
|
@@ -5782,9 +6524,9 @@ async function clonesToCommit(root, cwd, manifest) {
|
|
|
5782
6524
|
}
|
|
5783
6525
|
return clones;
|
|
5784
6526
|
}
|
|
5785
|
-
function finish(ui, cwd, rnessDir, label,
|
|
6527
|
+
function finish(ui, cwd, rnessDir, label, target2, ending) {
|
|
5786
6528
|
const rel = relative5(cwd, rnessDir) || ".";
|
|
5787
|
-
const message2 = `chore: rness ${
|
|
6529
|
+
const message2 = `chore: rness ${target2}`;
|
|
5788
6530
|
const next = [];
|
|
5789
6531
|
if (ending.commit === "committed") next.push(`git -C ${rel} push`);
|
|
5790
6532
|
if (ending.commit === "refused")
|
|
@@ -5797,12 +6539,12 @@ function finish(ui, cwd, rnessDir, label, target, ending) {
|
|
|
5797
6539
|
next.push("# teammates: git pull \u2014 the next rness command installs it");
|
|
5798
6540
|
if (ui.session) {
|
|
5799
6541
|
if (next.length > 0) ui.note("Next steps", next);
|
|
5800
|
-
ui.outro(`${label} runs @rness/cli ${
|
|
6542
|
+
ui.outro(`${label} runs @rness/cli ${target2}`);
|
|
5801
6543
|
} else
|
|
5802
6544
|
process.stdout.write(
|
|
5803
6545
|
[
|
|
5804
6546
|
"",
|
|
5805
|
-
status("upgraded", `${label} to @rness/cli ${
|
|
6547
|
+
status("upgraded", `${label} to @rness/cli ${target2}`),
|
|
5806
6548
|
"",
|
|
5807
6549
|
...next.length > 0 ? ["Next:", ...next.map((line3) => ` ${line3}`), ""] : []
|
|
5808
6550
|
].join("\n")
|
|
@@ -5838,12 +6580,12 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
5838
6580
|
}
|
|
5839
6581
|
const ui = interactive ? await makeUi(terminal) : { ...plainUi };
|
|
5840
6582
|
ui.intro(`Upgrade workspace ${basename5(ws.root)}`);
|
|
5841
|
-
const
|
|
6583
|
+
const target2 = version ?? await ui.step(
|
|
5842
6584
|
{ doing: "asking the registry for the latest release", quiet: true },
|
|
5843
6585
|
() => latestVersion(),
|
|
5844
6586
|
(v) => v === null ? null : ["latest", `@rness/cli ${v}`]
|
|
5845
6587
|
);
|
|
5846
|
-
if (
|
|
6588
|
+
if (target2 === null) {
|
|
5847
6589
|
process.stderr.write(
|
|
5848
6590
|
`cannot reach the registry; name the version: ${rnessCommand()} upgrade <version>
|
|
5849
6591
|
`
|
|
@@ -5871,44 +6613,44 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
5871
6613
|
return 1;
|
|
5872
6614
|
}
|
|
5873
6615
|
const base = await findScaffoldBase(ws.rnessDir);
|
|
5874
|
-
const scaffoldCurrent = base !== null && base.version ===
|
|
5875
|
-
if (pin.spec ===
|
|
5876
|
-
if (ui.session) ui.outro(`Already at ${
|
|
5877
|
-
else process.stdout.write(`already at ${
|
|
6616
|
+
const scaffoldCurrent = base !== null && base.version === target2;
|
|
6617
|
+
if (pin.spec === target2 && installed === target2 && scaffoldCurrent) {
|
|
6618
|
+
if (ui.session) ui.outro(`Already at ${target2}`);
|
|
6619
|
+
else process.stdout.write(`already at ${target2}
|
|
5878
6620
|
`);
|
|
5879
6621
|
return 0;
|
|
5880
6622
|
}
|
|
5881
|
-
const downgrading = EXACT_VERSION.test(pin.spec) && compareVersions(
|
|
6623
|
+
const downgrading = EXACT_VERSION.test(pin.spec) && compareVersions(target2, pin.spec) < 0;
|
|
5882
6624
|
if (downgrading && version === void 0) {
|
|
5883
6625
|
process.stderr.write(
|
|
5884
|
-
`${label} pins @rness/cli ${pin.spec}, newer than the latest release ${
|
|
6626
|
+
`${label} pins @rness/cli ${pin.spec}, newer than the latest release ${target2}; to downgrade: ${rnessCommand()} upgrade ${target2}
|
|
5885
6627
|
`
|
|
5886
6628
|
);
|
|
5887
6629
|
return 1;
|
|
5888
6630
|
}
|
|
5889
6631
|
const pm = await workspacePackageManager(ws.rnessDir);
|
|
5890
|
-
if (pin.spec ===
|
|
6632
|
+
if (pin.spec === target2 && installed === target2)
|
|
5891
6633
|
ui.line(
|
|
5892
6634
|
"scaffold",
|
|
5893
|
-
`@rness/cli ${
|
|
5894
|
-
`Merging the @rness/cli ${
|
|
6635
|
+
`@rness/cli ${target2} \u2014 merging it into ${label}`,
|
|
6636
|
+
`Merging the @rness/cli ${target2} scaffold into ${label}`
|
|
5895
6637
|
);
|
|
5896
|
-
else if (pin.spec ===
|
|
6638
|
+
else if (pin.spec === target2)
|
|
5897
6639
|
ui.line(
|
|
5898
6640
|
"install",
|
|
5899
|
-
`@rness/cli ${
|
|
6641
|
+
`@rness/cli ${target2} in ${label} (${pm}) \u2014 ${installed ?? "nothing"} is installed`
|
|
5900
6642
|
);
|
|
5901
6643
|
else {
|
|
5902
6644
|
ui.line(
|
|
5903
6645
|
"upgrade",
|
|
5904
|
-
`@rness/cli ${pin.spec} \u2192 ${
|
|
6646
|
+
`@rness/cli ${pin.spec} \u2192 ${target2} in ${label} (${pm})${downgrading ? " \u2014 downgrading" : ""}`
|
|
5905
6647
|
);
|
|
5906
6648
|
ui.line("notes", NOTES, `Release notes: ${NOTES}`);
|
|
5907
6649
|
}
|
|
5908
6650
|
if (interactive) {
|
|
5909
6651
|
const p = await terminal.prompts();
|
|
5910
6652
|
const ok = await p.confirm({
|
|
5911
|
-
message: `Upgrade @rness/cli to ${
|
|
6653
|
+
message: `Upgrade @rness/cli to ${target2}?`
|
|
5912
6654
|
});
|
|
5913
6655
|
if (p.isCancel(ok) || ok !== true) {
|
|
5914
6656
|
ui.cancelled();
|
|
@@ -5918,31 +6660,31 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
5918
6660
|
let merged = false;
|
|
5919
6661
|
if (!scaffoldCurrent) {
|
|
5920
6662
|
const source = await (deps.scaffoldFor ?? scaffoldFor)(
|
|
5921
|
-
|
|
6663
|
+
target2,
|
|
5922
6664
|
ws.rnessDir
|
|
5923
6665
|
);
|
|
5924
6666
|
try {
|
|
5925
6667
|
const files = await renderScaffold(source.dir, {
|
|
5926
|
-
version:
|
|
6668
|
+
version: target2,
|
|
5927
6669
|
packageManager: await declaredManager(ws.rnessDir, pm)
|
|
5928
6670
|
});
|
|
5929
6671
|
if (base === null)
|
|
5930
6672
|
ui.line(
|
|
5931
6673
|
"adopting",
|
|
5932
|
-
`the @rness/cli ${
|
|
5933
|
-
`Adopting the @rness/cli ${
|
|
6674
|
+
`the @rness/cli ${target2} scaffold (no scaffold commit in ${label} yet)`,
|
|
6675
|
+
`Adopting the @rness/cli ${target2} scaffold \u2014 ${label} has no scaffold commit yet`
|
|
5934
6676
|
);
|
|
5935
6677
|
else
|
|
5936
6678
|
ui.line(
|
|
5937
6679
|
"merging",
|
|
5938
|
-
`the @rness/cli ${
|
|
5939
|
-
`Merging the @rness/cli ${
|
|
6680
|
+
`the @rness/cli ${target2} scaffold`,
|
|
6681
|
+
`Merging the @rness/cli ${target2} scaffold`
|
|
5940
6682
|
);
|
|
5941
6683
|
const commit = await buildScaffoldCommit(
|
|
5942
6684
|
ws.rnessDir,
|
|
5943
6685
|
base?.commit ?? null,
|
|
5944
6686
|
base === null ? await withoutRedundantKeeps(ws.rnessDir, files) : files,
|
|
5945
|
-
|
|
6687
|
+
target2
|
|
5946
6688
|
);
|
|
5947
6689
|
const result = await mergeScaffold(
|
|
5948
6690
|
ws.rnessDir,
|
|
@@ -5969,9 +6711,9 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
5969
6711
|
[packageFile, await readFile9(packageFile, "utf8")]
|
|
5970
6712
|
]);
|
|
5971
6713
|
const moved = await readPin(ws.rnessDir);
|
|
5972
|
-
if (moved !== null && moved.spec !==
|
|
5973
|
-
await writePin(ws.rnessDir, moved.spec,
|
|
5974
|
-
const mustInstall = !(pin.spec ===
|
|
6714
|
+
if (moved !== null && moved.spec !== target2)
|
|
6715
|
+
await writePin(ws.rnessDir, moved.spec, target2);
|
|
6716
|
+
const mustInstall = !(pin.spec === target2 && installed === target2);
|
|
5975
6717
|
if (mustInstall) {
|
|
5976
6718
|
try {
|
|
5977
6719
|
await ui.step(
|
|
@@ -6002,9 +6744,9 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
6002
6744
|
return 1;
|
|
6003
6745
|
}
|
|
6004
6746
|
const now = (await readPinnedCli(ws.rnessDir))?.version ?? null;
|
|
6005
|
-
if (now !==
|
|
6747
|
+
if (now !== target2) {
|
|
6006
6748
|
process.stderr.write(
|
|
6007
|
-
`${pm} install left @rness/cli ${now ?? "not installed"} in ${label}, not ${
|
|
6749
|
+
`${pm} install left @rness/cli ${now ?? "not installed"} in ${label}, not ${target2}
|
|
6008
6750
|
`
|
|
6009
6751
|
);
|
|
6010
6752
|
return 1;
|
|
@@ -6013,7 +6755,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
6013
6755
|
ui.gitIsSilent = true;
|
|
6014
6756
|
const code = await syncBlocks(ui, ws.root, basename5(ws.root));
|
|
6015
6757
|
if (code !== 0) return code;
|
|
6016
|
-
const message2 = `chore: rness ${
|
|
6758
|
+
const message2 = `chore: rness ${target2}`;
|
|
6017
6759
|
const committed = await commitUpgrade(ws.rnessDir, message2);
|
|
6018
6760
|
if (committed.kind === "committed")
|
|
6019
6761
|
ui.line(
|
|
@@ -6028,7 +6770,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
|
|
|
6028
6770
|
for (const line3 of committed.output.split("\n"))
|
|
6029
6771
|
if (line3.trim() !== "") ui.hint(line3, "stderr");
|
|
6030
6772
|
}
|
|
6031
|
-
return finish(ui, cwd, ws.rnessDir, label,
|
|
6773
|
+
return finish(ui, cwd, ws.rnessDir, label, target2, {
|
|
6032
6774
|
commit: committed.kind,
|
|
6033
6775
|
clones: await clonesToCommit(
|
|
6034
6776
|
ws.root,
|
|
@@ -6153,11 +6895,11 @@ password=${resolved.token}
|
|
|
6153
6895
|
|
|
6154
6896
|
// src/commands/hook.ts
|
|
6155
6897
|
import { realpath as realpath4 } from "fs/promises";
|
|
6156
|
-
import { isAbsolute as isAbsolute2, posix as
|
|
6898
|
+
import { isAbsolute as isAbsolute2, posix as posix8, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
|
|
6157
6899
|
|
|
6158
6900
|
// src/mcp/tools.ts
|
|
6159
6901
|
import { readFile as readFile10, realpath as realpath3, stat as stat4 } from "fs/promises";
|
|
6160
|
-
import { isAbsolute, join as join24, posix as
|
|
6902
|
+
import { isAbsolute, join as join24, posix as posix7, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
|
|
6161
6903
|
var LABELS = {
|
|
6162
6904
|
standards: "Standards",
|
|
6163
6905
|
adr: "Decisions",
|
|
@@ -6197,7 +6939,7 @@ async function documents(rnessDir, only) {
|
|
|
6197
6939
|
scope: ownerScope(item.rel),
|
|
6198
6940
|
id,
|
|
6199
6941
|
status: typeof status2 === "string" ? status2 : null,
|
|
6200
|
-
title: documentTitle(item.title ??
|
|
6942
|
+
title: documentTitle(item.title ?? posix7.basename(item.rel, ".md"), id),
|
|
6201
6943
|
text: item.body
|
|
6202
6944
|
});
|
|
6203
6945
|
}
|
|
@@ -6227,7 +6969,7 @@ async function context(cwd, args) {
|
|
|
6227
6969
|
} else
|
|
6228
6970
|
scope = resolveScope(
|
|
6229
6971
|
manifest,
|
|
6230
|
-
relative6(ws.root, cwd).split(sep4).join(
|
|
6972
|
+
relative6(ws.root, cwd).split(sep4).join(posix7.sep)
|
|
6231
6973
|
);
|
|
6232
6974
|
const summary = await scopeSummary(ws, manifest, scope);
|
|
6233
6975
|
return {
|
|
@@ -6416,8 +7158,9 @@ function mcpTools(cwd) {
|
|
|
6416
7158
|
|
|
6417
7159
|
// src/pulse/detached.ts
|
|
6418
7160
|
import { spawn as nodeSpawn } from "child_process";
|
|
6419
|
-
import { mkdir as mkdir7, readFile as readFile11, rm as rm8, writeFile as writeFile4 } from "fs/promises";
|
|
7161
|
+
import { mkdir as mkdir7, open as open2, readFile as readFile11, rm as rm8, stat as stat5, writeFile as writeFile4 } from "fs/promises";
|
|
6420
7162
|
import { join as join25 } from "path";
|
|
7163
|
+
import { setTimeout as delay2 } from "timers/promises";
|
|
6421
7164
|
var detached = (args, cwd) => {
|
|
6422
7165
|
const bin = process.argv[1];
|
|
6423
7166
|
if (bin === void 0) return;
|
|
@@ -6452,6 +7195,33 @@ async function takeFailure() {
|
|
|
6452
7195
|
return null;
|
|
6453
7196
|
}
|
|
6454
7197
|
}
|
|
7198
|
+
var PULSE_LOCK_STALE_MS = 15 * 6e4;
|
|
7199
|
+
async function oneSyncAtATime(key, work, options = {}) {
|
|
7200
|
+
await mkdir7(configDir(), { recursive: true, mode: 448 });
|
|
7201
|
+
const file = join25(configDir(), `pulse-${key}.lock`);
|
|
7202
|
+
for (; ; ) {
|
|
7203
|
+
try {
|
|
7204
|
+
const handle = await open2(file, "wx");
|
|
7205
|
+
await handle.close();
|
|
7206
|
+
break;
|
|
7207
|
+
} catch (e) {
|
|
7208
|
+
if (e.code !== "EEXIST") throw e;
|
|
7209
|
+
const age = await stat5(file).then(
|
|
7210
|
+
(s) => Date.now() - s.mtimeMs,
|
|
7211
|
+
() => 0
|
|
7212
|
+
);
|
|
7213
|
+
if (age > PULSE_LOCK_STALE_MS) await rm8(file, { force: true });
|
|
7214
|
+
else if (options.wait !== true) return false;
|
|
7215
|
+
else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
|
|
7216
|
+
}
|
|
7217
|
+
}
|
|
7218
|
+
try {
|
|
7219
|
+
await work();
|
|
7220
|
+
return true;
|
|
7221
|
+
} finally {
|
|
7222
|
+
await rm8(file, { force: true });
|
|
7223
|
+
}
|
|
7224
|
+
}
|
|
6455
7225
|
|
|
6456
7226
|
// src/commands/hook.ts
|
|
6457
7227
|
var NOUNS = {
|
|
@@ -6479,7 +7249,7 @@ function cwdOf(input, env) {
|
|
|
6479
7249
|
if (typeof cwd === "string" && cwd !== "") return cwd;
|
|
6480
7250
|
return env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
|
|
6481
7251
|
}
|
|
6482
|
-
var toPosix2 = (p) => p.split(sep5).join(
|
|
7252
|
+
var toPosix2 = (p) => p.split(sep5).join(posix8.sep);
|
|
6483
7253
|
function count(n, [one, many]) {
|
|
6484
7254
|
return `${n} ${n === 1 ? one : many}`;
|
|
6485
7255
|
}
|
|
@@ -6823,29 +7593,162 @@ async function mcpCommand(opts, io = {
|
|
|
6823
7593
|
return 0;
|
|
6824
7594
|
}
|
|
6825
7595
|
|
|
7596
|
+
// src/commands/pulse.ts
|
|
7597
|
+
import { readFile as readFile13, readdir as readdir4 } from "fs/promises";
|
|
7598
|
+
import { join as join26 } from "path";
|
|
7599
|
+
import { setTimeout as delay3 } from "timers/promises";
|
|
7600
|
+
|
|
7601
|
+
// src/pulse/opened.ts
|
|
7602
|
+
import { mkdir as mkdir8, readFile as readFile12, rename as rename3, rm as rm9, writeFile as writeFile5 } from "fs/promises";
|
|
7603
|
+
import { dirname as dirname9 } from "path";
|
|
7604
|
+
var NAME2 = "rness/opened";
|
|
7605
|
+
function openedFile(rnessDir) {
|
|
7606
|
+
return gitPath(rnessDir, NAME2);
|
|
7607
|
+
}
|
|
7608
|
+
var NUMBER = /^[1-9][0-9]*$/;
|
|
7609
|
+
async function readOpened(file) {
|
|
7610
|
+
const opened = /* @__PURE__ */ new Map();
|
|
7611
|
+
if (file === null) return opened;
|
|
7612
|
+
let data;
|
|
7613
|
+
try {
|
|
7614
|
+
data = JSON.parse(await readFile12(file, "utf8"));
|
|
7615
|
+
} catch {
|
|
7616
|
+
return opened;
|
|
7617
|
+
}
|
|
7618
|
+
if (data === null || typeof data !== "object" || Array.isArray(data))
|
|
7619
|
+
return opened;
|
|
7620
|
+
for (const [key, path] of Object.entries(data))
|
|
7621
|
+
if (NUMBER.test(key) && typeof path === "string" && path !== "")
|
|
7622
|
+
opened.set(Number(key), path);
|
|
7623
|
+
return opened;
|
|
7624
|
+
}
|
|
7625
|
+
async function writeOpened(file, opened) {
|
|
7626
|
+
if (file === null) return;
|
|
7627
|
+
try {
|
|
7628
|
+
if (opened.size === 0) {
|
|
7629
|
+
await rm9(file, { force: true });
|
|
7630
|
+
return;
|
|
7631
|
+
}
|
|
7632
|
+
const entries = [...opened].sort(([a], [b]) => a - b);
|
|
7633
|
+
await mkdir8(dirname9(file), { recursive: true });
|
|
7634
|
+
const temporary = `${file}.${process.pid}`;
|
|
7635
|
+
await writeFile5(
|
|
7636
|
+
temporary,
|
|
7637
|
+
`${JSON.stringify(Object.fromEntries(entries), null, 2)}
|
|
7638
|
+
`
|
|
7639
|
+
);
|
|
7640
|
+
await rename3(temporary, file);
|
|
7641
|
+
} catch {
|
|
7642
|
+
}
|
|
7643
|
+
}
|
|
7644
|
+
|
|
6826
7645
|
// src/pulse/plan.ts
|
|
6827
|
-
|
|
7646
|
+
var differs = (item, w) => item.title !== w.title || item.status !== w.status || w.statusField !== null && item.collectionStatus !== w.status || item.type !== w.type;
|
|
7647
|
+
var rank = (i) => i.issue === null ? 2 : i.issue.open ? 0 : 1;
|
|
7648
|
+
var keptFirst = (a, b) => rank(a) - rank(b) || (a.issue?.number ?? 0) - (b.issue?.number ?? 0);
|
|
7649
|
+
var gone = (item) => item.issue === null ? { kind: "archive", id: item.id } : { kind: "close", id: item.id };
|
|
7650
|
+
var firstLine5 = (body) => body.split(/\r\n?|\n/, 1)[0] ?? "";
|
|
7651
|
+
function strays(have) {
|
|
7652
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
7653
|
+
for (const item of have) {
|
|
7654
|
+
if (item.path !== null || item.issue === null || item.issue.body === null || !item.issue.labelled)
|
|
7655
|
+
continue;
|
|
7656
|
+
const line3 = firstLine5(item.issue.body);
|
|
7657
|
+
byLine.set(line3, [...byLine.get(line3) ?? [], item]);
|
|
7658
|
+
}
|
|
7659
|
+
for (const items of byLine.values()) items.sort(keptFirst);
|
|
7660
|
+
return byLine;
|
|
7661
|
+
}
|
|
7662
|
+
function unwantedPaths(want, have) {
|
|
7663
|
+
const wanted = new Set(want.map((w) => w.path));
|
|
7664
|
+
const paths = /* @__PURE__ */ new Set();
|
|
7665
|
+
for (const item of have)
|
|
7666
|
+
if (item.path !== null && !wanted.has(item.path)) paths.add(item.path);
|
|
7667
|
+
return [...paths];
|
|
7668
|
+
}
|
|
7669
|
+
function isGone(path, item, seen, opened) {
|
|
7670
|
+
return seen.has(path) || item.issue !== null && opened.get(item.issue.number) === path;
|
|
7671
|
+
}
|
|
7672
|
+
function planSync(want, have, seen = /* @__PURE__ */ new Set(), opened = /* @__PURE__ */ new Map()) {
|
|
6828
7673
|
const byPath = /* @__PURE__ */ new Map();
|
|
6829
|
-
const
|
|
7674
|
+
for (const item of have)
|
|
7675
|
+
if (item.path !== null)
|
|
7676
|
+
byPath.set(item.path, [...byPath.get(item.path) ?? [], item]);
|
|
7677
|
+
const kept = /* @__PURE__ */ new Map();
|
|
6830
7678
|
const surplus = [];
|
|
6831
|
-
for (const
|
|
6832
|
-
|
|
6833
|
-
if (
|
|
6834
|
-
|
|
7679
|
+
for (const [path, items] of byPath) {
|
|
7680
|
+
const [keep, ...rest] = [...items].sort(keptFirst);
|
|
7681
|
+
if (keep !== void 0) kept.set(path, keep);
|
|
7682
|
+
surplus.push(...rest.map(gone));
|
|
6835
7683
|
}
|
|
7684
|
+
const left = strays(have);
|
|
7685
|
+
const steps = [];
|
|
6836
7686
|
const wanted = /* @__PURE__ */ new Set();
|
|
6837
7687
|
for (const w of want) {
|
|
6838
7688
|
wanted.add(w.path);
|
|
6839
|
-
const item =
|
|
7689
|
+
const item = kept.get(w.path) ?? left.get(w.body)?.[0];
|
|
6840
7690
|
if (item === void 0) steps.push({ kind: "create", want: w });
|
|
6841
|
-
else if (item.
|
|
6842
|
-
steps.push({ kind: "
|
|
7691
|
+
else if (item.issue === null)
|
|
7692
|
+
steps.push({ kind: "convert", id: item.id, want: w });
|
|
7693
|
+
else if (item.path === null || !item.issue.open || !item.issue.labelled || differs(item, w))
|
|
7694
|
+
steps.push({
|
|
7695
|
+
kind: "update",
|
|
7696
|
+
id: item.id,
|
|
7697
|
+
want: w,
|
|
7698
|
+
reopen: !item.issue.open
|
|
7699
|
+
});
|
|
6843
7700
|
else steps.push({ kind: "unchanged", id: item.id });
|
|
6844
7701
|
}
|
|
6845
|
-
for (const [path, item] of
|
|
6846
|
-
if (!wanted.has(path))
|
|
7702
|
+
for (const [path, item] of kept)
|
|
7703
|
+
if (!wanted.has(path))
|
|
7704
|
+
steps.push(
|
|
7705
|
+
isGone(path, item, seen, opened) ? gone(item) : { kind: "unchanged", id: item.id }
|
|
7706
|
+
);
|
|
6847
7707
|
return [...steps, ...surplus];
|
|
6848
7708
|
}
|
|
7709
|
+
function stillOpened(opened, have, seen, closed, created) {
|
|
7710
|
+
const items = /* @__PURE__ */ new Map();
|
|
7711
|
+
for (const item of have)
|
|
7712
|
+
if (item.issue !== null) items.set(item.issue.number, item.id);
|
|
7713
|
+
const next = /* @__PURE__ */ new Map();
|
|
7714
|
+
for (const [number, path] of opened) {
|
|
7715
|
+
const id = items.get(number);
|
|
7716
|
+
if (id !== void 0 && !closed.has(id) && !seen.has(path))
|
|
7717
|
+
next.set(number, path);
|
|
7718
|
+
}
|
|
7719
|
+
for (const [path, placed] of created) next.set(placed.number, path);
|
|
7720
|
+
return next;
|
|
7721
|
+
}
|
|
7722
|
+
function issuedAfter(steps, have, placed) {
|
|
7723
|
+
const byId = new Map(have.map((i) => [i.id, i]));
|
|
7724
|
+
const issued = /* @__PURE__ */ new Map();
|
|
7725
|
+
for (const step2 of steps) {
|
|
7726
|
+
if (step2.kind === "create" || step2.kind === "convert") {
|
|
7727
|
+
const p = placed.get(step2.want.path);
|
|
7728
|
+
if (p !== void 0) issued.set(step2.want.path, { ...p, body: null });
|
|
7729
|
+
} else if (step2.kind === "update" || step2.kind === "unchanged") {
|
|
7730
|
+
const item = byId.get(step2.id);
|
|
7731
|
+
const path = step2.kind === "update" ? step2.want.path : item?.path;
|
|
7732
|
+
if (item === void 0 || path == null || item.issue === null) continue;
|
|
7733
|
+
issued.set(path, {
|
|
7734
|
+
id: item.id,
|
|
7735
|
+
number: item.issue.number,
|
|
7736
|
+
body: item.issue.body
|
|
7737
|
+
});
|
|
7738
|
+
}
|
|
7739
|
+
}
|
|
7740
|
+
return issued;
|
|
7741
|
+
}
|
|
7742
|
+
function planBodies(bodies, issued) {
|
|
7743
|
+
const steps = [];
|
|
7744
|
+
for (const [path, body] of bodies) {
|
|
7745
|
+
const i = issued.get(path);
|
|
7746
|
+
if (i === void 0) continue;
|
|
7747
|
+
if (i.body === null || digestOf(i.body) !== digestOf(body))
|
|
7748
|
+
steps.push({ kind: "body", id: i.id, body });
|
|
7749
|
+
}
|
|
7750
|
+
return steps;
|
|
7751
|
+
}
|
|
6849
7752
|
|
|
6850
7753
|
// src/commands/pulse.ts
|
|
6851
7754
|
var PROJECT_SCOPE = "project";
|
|
@@ -6861,15 +7764,23 @@ async function fromGithub(work) {
|
|
|
6861
7764
|
throw e;
|
|
6862
7765
|
}
|
|
6863
7766
|
}
|
|
6864
|
-
|
|
7767
|
+
var apiOf = (opts) => opts.githubApi === void 0 ? {} : { apiBase: opts.githubApi };
|
|
7768
|
+
var providerOptions = (opts, wait) => ({
|
|
7769
|
+
...apiOf(opts),
|
|
7770
|
+
...wait === void 0 ? {} : { wait }
|
|
7771
|
+
});
|
|
7772
|
+
async function context2(opts, wait) {
|
|
6865
7773
|
const ws = await findWorkspace(opts.cwd ?? process.cwd());
|
|
6866
7774
|
const manifest = await loadManifest(ws.rnessDir);
|
|
6867
|
-
const provider = await openProvider(manifest,
|
|
7775
|
+
const provider = await openProvider(manifest, providerOptions(opts, wait));
|
|
6868
7776
|
if (manifest.org === null)
|
|
6869
7777
|
throw new Error('the pulse needs an organization: rness.json has no "org"');
|
|
6870
7778
|
return { rnessDir: ws.rnessDir, manifest, org: manifest.org, provider };
|
|
6871
7779
|
}
|
|
6872
|
-
var
|
|
7780
|
+
var waitSaid = (opts, ui) => rateWait(async (ms) => {
|
|
7781
|
+
ui.line("waiting", `${Math.round(ms / 1e3)} s \u2014 GitHub's rate limit`);
|
|
7782
|
+
await (opts.sleep ?? ((n) => delay3(n)))(ms);
|
|
7783
|
+
});
|
|
6873
7784
|
async function missingAccess(provider) {
|
|
6874
7785
|
if (!provider.authenticated) return NEEDS_LOGIN;
|
|
6875
7786
|
const scopes = await fromGithub(provider.scopes());
|
|
@@ -6895,33 +7806,131 @@ var createdByKind = (ui, added) => {
|
|
|
6895
7806
|
const names = added.filter((a) => a.startsWith(`${kind} `)).map((a) => a.slice(kind.length + 1));
|
|
6896
7807
|
if (names.length > 0) ui.line("created", `${kind}s ${names.join(", ")}`);
|
|
6897
7808
|
}
|
|
7809
|
+
for (const a of added) {
|
|
7810
|
+
if (a.startsWith("label ")) ui.line("created", a);
|
|
7811
|
+
else if (a.startsWith("link "))
|
|
7812
|
+
ui.line("linked", `Agent Pulse to ${a.slice("link ".length)}`);
|
|
7813
|
+
}
|
|
6898
7814
|
};
|
|
7815
|
+
async function filesOf2(rnessDir) {
|
|
7816
|
+
const files = /* @__PURE__ */ new Set();
|
|
7817
|
+
const walk = async (rel) => {
|
|
7818
|
+
const dir = rel === "" ? rnessDir : join26(rnessDir, ...rel.split("/"));
|
|
7819
|
+
for (const e of await readdir4(dir, { withFileTypes: true })) {
|
|
7820
|
+
const path = rel === "" ? e.name : `${rel}/${e.name}`;
|
|
7821
|
+
if (e.isDirectory()) {
|
|
7822
|
+
if (e.name !== ".git" && e.name !== "node_modules") await walk(path);
|
|
7823
|
+
} else if (e.isFile()) files.add(path);
|
|
7824
|
+
}
|
|
7825
|
+
};
|
|
7826
|
+
await walk("");
|
|
7827
|
+
return files;
|
|
7828
|
+
}
|
|
7829
|
+
var count2 = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
7830
|
+
function sayDone(ui, org, documents2, steps, writes) {
|
|
7831
|
+
const n = (kind) => steps.filter((s) => s.kind === kind).length;
|
|
7832
|
+
const reopened = steps.filter((s) => s.kind === "update" && s.reopen).length;
|
|
7833
|
+
if (n("create") > 0) ui.line("created", count2(n("create"), "issue"));
|
|
7834
|
+
if (n("convert") > 0)
|
|
7835
|
+
ui.line(
|
|
7836
|
+
"converted",
|
|
7837
|
+
`${count2(n("convert"), "draft")} into issues of ${org}/.rness`
|
|
7838
|
+
);
|
|
7839
|
+
if (reopened > 0) ui.line("reopened", count2(reopened, "issue"));
|
|
7840
|
+
const written = new Set(writes.map((w) => w.id));
|
|
7841
|
+
const bodyOnly = steps.filter(
|
|
7842
|
+
(s) => s.kind === "unchanged" && written.has(s.id)
|
|
7843
|
+
).length;
|
|
7844
|
+
const parts = [
|
|
7845
|
+
["created", n("create")],
|
|
7846
|
+
["converted", n("convert")],
|
|
7847
|
+
["updated", n("update") + bodyOnly],
|
|
7848
|
+
[
|
|
7849
|
+
"unchanged",
|
|
7850
|
+
documents2 - n("create") - n("convert") - n("update") - bodyOnly
|
|
7851
|
+
],
|
|
7852
|
+
["archived", n("close") + n("archive")]
|
|
7853
|
+
].filter(([, k]) => k > 0).map(([word, k]) => `${k} ${word}`);
|
|
7854
|
+
ui.line(
|
|
7855
|
+
"synced",
|
|
7856
|
+
`${plural(documents2)}${parts.length > 0 ? `: ${parts.join(", ")}` : ""}`
|
|
7857
|
+
);
|
|
7858
|
+
}
|
|
6899
7859
|
async function syncBoard(c, board, ui, sayLayout) {
|
|
7860
|
+
await fromGithub(c.provider.checkIssues(c.org));
|
|
6900
7861
|
const tabs = await statusTabs(c.rnessDir);
|
|
6901
7862
|
const want = desiredOf(tabs, c.org);
|
|
6902
7863
|
sayLayout(
|
|
6903
7864
|
ui,
|
|
6904
7865
|
await fromGithub(c.provider.ensureLayout(board, layoutOf(tabs)))
|
|
6905
7866
|
);
|
|
6906
|
-
const
|
|
6907
|
-
|
|
6908
|
-
const
|
|
6909
|
-
const
|
|
6910
|
-
|
|
6911
|
-
|
|
6912
|
-
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
|
|
6917
|
-
|
|
6918
|
-
|
|
7867
|
+
const have = await fromGithub(c.provider.items(board));
|
|
7868
|
+
const record = await openedFile(c.rnessDir);
|
|
7869
|
+
const opened = await readOpened(record);
|
|
7870
|
+
const seen = await seenPaths(c.rnessDir, [
|
|
7871
|
+
...unwantedPaths(want, have),
|
|
7872
|
+
...opened.values()
|
|
7873
|
+
]);
|
|
7874
|
+
const steps = planSync(want, have, seen, opened);
|
|
7875
|
+
const changes = steps.filter((s) => s.kind !== "unchanged").length;
|
|
7876
|
+
let writes = [];
|
|
7877
|
+
let made = 0;
|
|
7878
|
+
try {
|
|
7879
|
+
const placed = /* @__PURE__ */ new Map();
|
|
7880
|
+
const created = /* @__PURE__ */ new Map();
|
|
7881
|
+
const closed = /* @__PURE__ */ new Set();
|
|
7882
|
+
try {
|
|
7883
|
+
for (const step2 of steps) {
|
|
7884
|
+
const p = await fromGithub(c.provider.apply(board, step2));
|
|
7885
|
+
if (p !== null && (step2.kind === "create" || step2.kind === "convert"))
|
|
7886
|
+
placed.set(step2.want.path, p);
|
|
7887
|
+
if (p !== null && step2.kind === "create") created.set(step2.want.path, p);
|
|
7888
|
+
if (step2.kind === "close" || step2.kind === "archive")
|
|
7889
|
+
closed.add(step2.id);
|
|
7890
|
+
if (step2.kind !== "unchanged") made++;
|
|
7891
|
+
}
|
|
7892
|
+
} finally {
|
|
7893
|
+
await writeOpened(
|
|
7894
|
+
record,
|
|
7895
|
+
stillOpened(opened, have, seen, closed, created)
|
|
7896
|
+
);
|
|
7897
|
+
}
|
|
7898
|
+
const issued = issuedAfter(steps, have, placed);
|
|
7899
|
+
const numbers = new Map([...issued].map(([path, i]) => [path, i.number]));
|
|
7900
|
+
const files = await filesOf2(c.rnessDir);
|
|
7901
|
+
const bodies = /* @__PURE__ */ new Map();
|
|
7902
|
+
for (const w of want)
|
|
7903
|
+
bodies.set(
|
|
7904
|
+
w.path,
|
|
7905
|
+
issueBody({
|
|
7906
|
+
path: w.path,
|
|
7907
|
+
text: await readFile13(join26(c.rnessDir, ...w.path.split("/")), "utf8"),
|
|
7908
|
+
org: c.org,
|
|
7909
|
+
numbers,
|
|
7910
|
+
files
|
|
7911
|
+
})
|
|
7912
|
+
);
|
|
7913
|
+
writes = planBodies(bodies, issued);
|
|
7914
|
+
for (const step2 of writes) {
|
|
7915
|
+
await fromGithub(c.provider.apply(board, step2));
|
|
7916
|
+
made++;
|
|
7917
|
+
}
|
|
7918
|
+
} catch (e) {
|
|
7919
|
+
if (!(e instanceof RateLimitError)) throw e;
|
|
7920
|
+
const total = changes + writes.length;
|
|
7921
|
+
throw new Error(
|
|
7922
|
+
`${e.message}: ${total - made} of ${total} changes not made \u2014 the next rness pulse sync makes them`,
|
|
7923
|
+
{ cause: e }
|
|
7924
|
+
);
|
|
7925
|
+
}
|
|
7926
|
+
sayDone(ui, c.org, want.length, steps, writes);
|
|
6919
7927
|
}
|
|
6920
7928
|
async function pulseCreateCommand(opts, deps = {}) {
|
|
6921
7929
|
try {
|
|
6922
7930
|
const terminal = deps.terminal ?? defaultTerminal;
|
|
6923
7931
|
const ui = deps.ui ?? await makeUi(terminal);
|
|
6924
|
-
|
|
7932
|
+
const wait = waitSaid(opts, ui);
|
|
7933
|
+
let c = await context2(opts, wait);
|
|
6925
7934
|
if (c.manifest.pulse !== null)
|
|
6926
7935
|
throw new Error(
|
|
6927
7936
|
`already declared: ${boardUrl(c.org, c.manifest.pulse.project)} \u2014 rness pulse sync`
|
|
@@ -6949,10 +7958,14 @@ async function pulseCreateCommand(opts, deps = {}) {
|
|
|
6949
7958
|
{ terminal, ui, ...opts.cwd === void 0 ? {} : { cwd: opts.cwd } }
|
|
6950
7959
|
);
|
|
6951
7960
|
if (code !== 0) return code;
|
|
6952
|
-
c = {
|
|
7961
|
+
c = {
|
|
7962
|
+
...c,
|
|
7963
|
+
provider: await openProvider(c.manifest, providerOptions(opts, wait))
|
|
7964
|
+
};
|
|
6953
7965
|
const still = await missingAccess(c.provider);
|
|
6954
7966
|
if (still !== null) throw new Error(still);
|
|
6955
7967
|
}
|
|
7968
|
+
await fromGithub(c.provider.checkIssues(c.org));
|
|
6956
7969
|
const login = (await c.provider.identity())?.login ?? "unknown";
|
|
6957
7970
|
ui.line(
|
|
6958
7971
|
"checked",
|
|
@@ -6981,7 +7994,7 @@ async function pulseCreateCommand(opts, deps = {}) {
|
|
|
6981
7994
|
async function pulseSyncCommand(opts, deps = {}) {
|
|
6982
7995
|
try {
|
|
6983
7996
|
const ui = deps.ui ?? await makeUi(deps.terminal ?? defaultTerminal);
|
|
6984
|
-
const c = await context2(opts);
|
|
7997
|
+
const c = await context2(opts, waitSaid(opts, ui));
|
|
6985
7998
|
await syncBoard(c, await declaredBoard(c), ui, eachAdded);
|
|
6986
7999
|
return 0;
|
|
6987
8000
|
} catch (e) {
|
|
@@ -6999,17 +8012,37 @@ async function pulseMarkCommand(opts) {
|
|
|
6999
8012
|
async function mark(opts) {
|
|
7000
8013
|
const c = await context2(opts);
|
|
7001
8014
|
const board = await declaredBoard(c);
|
|
7002
|
-
const
|
|
8015
|
+
const key = `${c.org}-${board.number}`;
|
|
8016
|
+
const lock = opts.sleep === void 0 ? {} : { sleep: opts.sleep };
|
|
8017
|
+
const listed = () => fromGithub(c.provider.items(board, { bodies: false }));
|
|
8018
|
+
let items = await listed();
|
|
7003
8019
|
if (opts.end === true) {
|
|
7004
8020
|
const id = opts.session.split(" \xB7 ").pop() ?? opts.session;
|
|
7005
8021
|
const ids2 = items.filter(
|
|
7006
8022
|
(i) => i.session === opts.session || i.session?.endsWith(` \xB7 ${id}`)
|
|
7007
8023
|
).map((i) => i.id);
|
|
7008
8024
|
await fromGithub(c.provider.mark(board, ids2, null));
|
|
7009
|
-
await syncBoard(c, board, plainUi, eachAdded)
|
|
8025
|
+
await oneSyncAtATime(key, () => syncBoard(c, board, plainUi, eachAdded), {
|
|
8026
|
+
...lock,
|
|
8027
|
+
wait: true
|
|
8028
|
+
});
|
|
7010
8029
|
return 0;
|
|
7011
8030
|
}
|
|
7012
8031
|
const wanted = new Set(opts.paths);
|
|
8032
|
+
const documents2 = new Set(
|
|
8033
|
+
(await statusTabs(c.rnessDir)).flatMap((t) => t.rows.map((r) => r.path))
|
|
8034
|
+
);
|
|
8035
|
+
const onBoard = new Set(
|
|
8036
|
+
items.flatMap((i) => i.path === null ? [] : [i.path])
|
|
8037
|
+
);
|
|
8038
|
+
if ([...wanted].some((p) => documents2.has(p) && !onBoard.has(p))) {
|
|
8039
|
+
const synced = await oneSyncAtATime(
|
|
8040
|
+
key,
|
|
8041
|
+
() => syncBoard(c, board, plainUi, eachAdded),
|
|
8042
|
+
lock
|
|
8043
|
+
);
|
|
8044
|
+
if (synced) items = await listed();
|
|
8045
|
+
}
|
|
7013
8046
|
const ids = items.filter((i) => i.path !== null && wanted.has(i.path)).map((i) => i.id);
|
|
7014
8047
|
await fromGithub(c.provider.mark(board, ids, opts.session));
|
|
7015
8048
|
return 0;
|
|
@@ -7500,6 +8533,7 @@ export {
|
|
|
7500
8533
|
defaultTransport,
|
|
7501
8534
|
findWorkspace,
|
|
7502
8535
|
reportError,
|
|
8536
|
+
findDelegate,
|
|
7503
8537
|
BEGIN,
|
|
7504
8538
|
END,
|
|
7505
8539
|
BLOCK_SIZE_WARNING,
|
|
@@ -7514,7 +8548,6 @@ export {
|
|
|
7514
8548
|
addCommand,
|
|
7515
8549
|
renderMarkdown,
|
|
7516
8550
|
contextCommand,
|
|
7517
|
-
findDelegate,
|
|
7518
8551
|
SCAFFOLD_FILES,
|
|
7519
8552
|
scaffoldDir,
|
|
7520
8553
|
copyScaffold,
|
|
@@ -7528,4 +8561,4 @@ export {
|
|
|
7528
8561
|
OWN_COMMANDS,
|
|
7529
8562
|
catchUp
|
|
7530
8563
|
};
|
|
7531
|
-
//# sourceMappingURL=chunk-
|
|
8564
|
+
//# sourceMappingURL=chunk-AXTFDXN2.js.map
|