@rness/cli 0.21.0 → 0.22.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.
@@ -856,16 +856,33 @@ function readHooks(value, at, fields2, holdsPlans) {
856
856
  ).withColon();
857
857
  return hooks;
858
858
  }
859
+ var PULSE = "pulse";
860
+ var presetFor = (name) => name === PULSE ? "agent-pulse" : "collection";
861
+ var PRESET_NAME = /^[a-z][a-z0-9-]*(?:\/[1-9][0-9]*)?$/;
862
+ function readPresetEntry(name, value, knows = isPresetRevision) {
863
+ const at = `boards.${name}`;
864
+ const preset = presetFor(name);
865
+ if (!PRESET_NAME.test(value))
866
+ throw new BoardRefused(at, `must name a preset, as "${preset}"`);
867
+ if (presetNameOf(value) !== preset)
868
+ throw new BoardRefused(
869
+ at,
870
+ name === PULSE ? `names ${value}: ${PULSE} is made from ${preset}` : `names ${value}: a collection's board is made from ${preset}`
871
+ );
872
+ if (value.includes("/") && !knows(value))
873
+ throw new BoardRefused(at, `names ${value}, which this rness does not have`);
874
+ return value;
875
+ }
859
876
  function parseBoard(name, value, knows = isPresetRevision) {
860
- const at = `projects.${name}`;
877
+ const at = `boards.${name}`;
861
878
  if (!isRecord(value))
862
879
  throw new BoardRefused(
863
880
  at,
864
- 'must be an object, as { "number": 5, "collections": \u2026, "views": \u2026 }'
881
+ 'must be an object, as { "collections": \u2026, "views": \u2026 }'
865
882
  );
866
883
  unknownKeys(value, BOARD_KEYS, at, "a board");
867
- const number = value["number"];
868
- if (typeof number !== "number" || !Number.isInteger(number) || number < 1)
884
+ const number = value["number"] ?? null;
885
+ if (number !== null && (typeof number !== "number" || !Number.isInteger(number) || number < 1))
869
886
  throw new BoardRefused(`${at}.number`, "must be a project number");
870
887
  const preset = value["preset"];
871
888
  if (preset !== void 0 && (typeof preset !== "string" || !PRESET.test(preset)))
@@ -1230,8 +1247,10 @@ var KEYS = [
1230
1247
  "provider",
1231
1248
  "org",
1232
1249
  "agents",
1233
- "pulse",
1250
+ "boards",
1251
+ // Former names of `boards`, still read: `sync` renames them (spec 0033 §3).
1234
1252
  "projects",
1253
+ "pulse",
1235
1254
  "repos",
1236
1255
  "scopes"
1237
1256
  ];
@@ -1287,52 +1306,57 @@ function readProvider(value) {
1287
1306
  return name;
1288
1307
  }
1289
1308
  function readPulse(value) {
1290
- if (value === void 0) return null;
1291
1309
  if (!isRecord2(value) || Object.keys(value).length !== 1 || typeof value.project !== "number" || !Number.isInteger(value.project) || value.project < 1)
1292
1310
  fail2(
1293
1311
  `"pulse" must be { "project": <number> } (got ${JSON.stringify(value)})`
1294
1312
  );
1295
1313
  return { [PULSE]: value.project };
1296
1314
  }
1297
- var projectNumber = (entry) => typeof entry === "number" ? entry : entry.number;
1298
- var PULSE = "pulse";
1299
- var PROJECT_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1315
+ var BOARD_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1300
1316
  var isProjectNumber = (n) => typeof n === "number" && Number.isInteger(n) && n >= 1;
1301
- function readProjects(value) {
1302
- if (value === void 0) return { projects: null, refused: [] };
1317
+ function readBoardEntries(value) {
1303
1318
  const entries = isRecord2(value) ? Object.entries(value) : [];
1304
- const numbers = entries.map(
1305
- ([, n]) => isRecord2(n) && isProjectNumber(n["number"]) ? n["number"] : n
1319
+ const numbers = entries.flatMap(
1320
+ ([, n]) => isRecord2(n) ? isProjectNumber(n["number"]) ? [n["number"]] : [] : [n]
1306
1321
  );
1307
1322
  if (entries.length === 0 || !entries.every(
1308
- ([name, n]) => PROJECT_NAME.test(name) && (isProjectNumber(n) || isRecord2(n))
1323
+ ([name, n]) => BOARD_NAME.test(name) && (isProjectNumber(n) || typeof n === "string" || isRecord2(n))
1309
1324
  ) || new Set(numbers).size !== numbers.length)
1310
1325
  fail2(
1311
- `"projects" must map names to distinct project numbers, as { "pulse": 4, "marketing": 5 } (got ${JSON.stringify(value)})`
1326
+ `"boards" must map names to boards on distinct projects, as { "pulse": "agent-pulse", "marketing": 5 } (got ${JSON.stringify(value)})`
1312
1327
  );
1313
- const projects = {};
1328
+ const boards = {};
1314
1329
  const refused = [];
1315
1330
  for (const [name, entry] of entries) {
1316
1331
  if (isProjectNumber(entry)) {
1317
- projects[name] = entry;
1332
+ boards[name] = entry;
1318
1333
  continue;
1319
1334
  }
1320
1335
  try {
1321
- projects[name] = parseBoard(name, entry);
1336
+ boards[name] = typeof entry === "string" ? readPresetEntry(name, entry) : parseBoard(name, entry);
1322
1337
  } catch (e) {
1323
1338
  if (!(e instanceof BoardRefused)) throw e;
1324
1339
  refused.push({ name, reason: e.message, source: entry });
1325
1340
  }
1326
1341
  }
1327
1342
  return {
1328
- projects: Object.keys(projects).length === 0 ? null : projects,
1343
+ boards: Object.keys(boards).length === 0 ? null : boards,
1329
1344
  refused
1330
1345
  };
1331
1346
  }
1332
- function readProjectsOrPulse(data) {
1333
- if (data.pulse !== void 0 && data.projects !== void 0)
1334
- fail2('"pulse" and "projects" at once: keep "projects" only');
1335
- return data.projects !== void 0 ? readProjects(data.projects) : { projects: readPulse(data.pulse), refused: [] };
1347
+ var HOLDERS = ["boards", "projects", "pulse"];
1348
+ function readBoardsOrFormer(data) {
1349
+ const present = HOLDERS.filter((k) => data[k] !== void 0);
1350
+ if (present.length > 1)
1351
+ fail2(
1352
+ `${present.map((k) => JSON.stringify(k)).join(" and ")} at once: keep "boards" only`
1353
+ );
1354
+ const key = present[0];
1355
+ if (key === void 0) return { boards: null, refused: [] };
1356
+ if (key === "pulse")
1357
+ return { boards: readPulse(data.pulse), refused: [], formerKey: key };
1358
+ const read = readBoardEntries(data[key]);
1359
+ return key === "boards" ? read : { ...read, formerKey: key };
1336
1360
  }
1337
1361
  function providerOf(manifest) {
1338
1362
  if (manifest.provider !== null) return manifest.provider;
@@ -1399,34 +1423,36 @@ function parseManifest(raw) {
1399
1423
  for (const key of Object.keys(data)) {
1400
1424
  if (!KEYS.includes(key)) fail2(`unknown key ${JSON.stringify(key)}`);
1401
1425
  }
1402
- const { projects, refused } = readProjectsOrPulse(data);
1426
+ const { boards, refused, formerKey } = readBoardsOrFormer(data);
1403
1427
  return {
1404
1428
  contract: 1,
1405
1429
  provider: readProvider(data.provider),
1406
1430
  org: readOrg(data.org),
1407
1431
  agents: readAgents(data.agents),
1408
- projects,
1432
+ boards,
1409
1433
  ...refused.length === 0 ? {} : { refused },
1434
+ ...formerKey === void 0 ? {} : { formerKey },
1410
1435
  repos: readRepos(data.repos),
1411
1436
  scopes: readScopes(data.scopes)
1412
1437
  };
1413
1438
  }
1414
- var projectEntry = (name, value) => ` ${JSON.stringify(name)}: ${JSON.stringify(value, null, 2).replaceAll("\n", "\n ")}`;
1415
- function projectsLines(manifest) {
1439
+ var boardEntry = (name, value) => ` ${JSON.stringify(name)}: ${JSON.stringify(value, null, 2).replaceAll("\n", "\n ")}`;
1440
+ var isScalar = (v) => typeof v === "number" || typeof v === "string";
1441
+ function boardsLines(manifest) {
1416
1442
  const entries = [
1417
- ...Object.entries(manifest.projects ?? {}).map(
1418
- ([name, p]) => [name, typeof p === "number" ? p : p.source]
1443
+ ...Object.entries(manifest.boards ?? {}).map(
1444
+ ([name, b]) => [name, isScalar(b) ? b : b.source]
1419
1445
  ),
1420
1446
  ...(manifest.refused ?? []).map((r) => [r.name, r.source])
1421
1447
  ];
1422
1448
  if (entries.length === 0) return [];
1423
- if (entries.every(([, v]) => typeof v === "number"))
1449
+ if (entries.every(([, v]) => isScalar(v)))
1424
1450
  return [
1425
- ` "projects": { ${entries.map(([name, n]) => `${JSON.stringify(name)}: ${String(n)}`).join(", ")} },`
1451
+ ` "boards": { ${entries.map(([name, v]) => `${JSON.stringify(name)}: ${JSON.stringify(v)}`).join(", ")} },`
1426
1452
  ];
1427
1453
  return [
1428
- ' "projects": {',
1429
- entries.map(([name, v]) => projectEntry(name, v)).join(",\n"),
1454
+ ' "boards": {',
1455
+ entries.map(([name, v]) => boardEntry(name, v)).join(",\n"),
1430
1456
  " },"
1431
1457
  ];
1432
1458
  }
@@ -1447,7 +1473,7 @@ async function writeManifest(rnessDir, manifest) {
1447
1473
  lines.push(
1448
1474
  ` "agents": [${manifest.agents.map((a) => JSON.stringify(a)).join(", ")}],`
1449
1475
  );
1450
- lines.push(...projectsLines(manifest));
1476
+ lines.push(...boardsLines(manifest));
1451
1477
  lines.push(
1452
1478
  ' "repos": {',
1453
1479
  repos.join(",\n"),
@@ -1489,7 +1515,7 @@ function parseRepoSpec(spec, org, host) {
1489
1515
  }
1490
1516
 
1491
1517
  // src/version.ts
1492
- var VERSION = "0.21.0";
1518
+ var VERSION = "0.22.0";
1493
1519
 
1494
1520
  // src/core/pm.ts
1495
1521
  import { execFile as execFile2 } from "child_process";
@@ -1715,7 +1741,7 @@ async function documentsIn(ws, manifest, scope, collections, statuses) {
1715
1741
  return paths;
1716
1742
  }
1717
1743
 
1718
- // src/pulse/urls.ts
1744
+ // src/board/urls.ts
1719
1745
  var boardUrl = (org, project) => `https://github.com/orgs/${org}/projects/${project}`;
1720
1746
  var itemUrl = (org, project, path) => `${boardUrl(org, project)}?filterQuery=${encodeURIComponent(`path:"${path}"`)}`;
1721
1747
  var blobUrl = (org, path) => `https://github.com/${org}/.rness/blob/main/${encodeURI(path)}`;
@@ -3053,7 +3079,7 @@ import { join as join8 } from "path";
3053
3079
  var GITHUB_CLIENT_ID = "Ov23liv1ia2UWeWttsOK";
3054
3080
  var GITHUB_WEB = "https://github.com";
3055
3081
  var SCOPES = "repo read:org";
3056
- var PULSE_SCOPE = "project";
3082
+ var BOARD_SCOPE = "project";
3057
3083
  function clientId() {
3058
3084
  return process.env["RNESS_GITHUB_CLIENT_ID"] ?? GITHUB_CLIENT_ID;
3059
3085
  }
@@ -3312,7 +3338,7 @@ async function resolveToken(deps = {}) {
3312
3338
  return refreshed === null ? null : answer(refreshed);
3313
3339
  }
3314
3340
 
3315
- // src/pulse/body.ts
3341
+ // src/board/body.ts
3316
3342
  import { createHash } from "crypto";
3317
3343
  import { posix as posix4 } from "path";
3318
3344
  var BODY_LIMIT = 65536;
@@ -3462,7 +3488,7 @@ ${content}`;
3462
3488
  ${comment2(digest(text))}`;
3463
3489
  }
3464
3490
 
3465
- // src/pulse/collection.ts
3491
+ // src/board/collection.ts
3466
3492
  import { readFile as readFile5, readdir as readdir2 } from "fs/promises";
3467
3493
  import { join as join9 } from "path";
3468
3494
  var FIELD_TYPES2 = ["text", "date", "select", "number"];
@@ -3654,7 +3680,7 @@ async function readUpdates(rnessDir, dir) {
3654
3680
  return updates;
3655
3681
  }
3656
3682
 
3657
- // src/pulse/layout.ts
3683
+ // src/board/layout.ts
3658
3684
  var statusOf2 = (status2) => status2 === null || status2 === "?" ? null : status2;
3659
3685
  var LIFECYCLE = [
3660
3686
  "Draft",
@@ -3873,7 +3899,7 @@ function desiredOf(board, collections, org, fronts = /* @__PURE__ */ new Map(),
3873
3899
  }));
3874
3900
  }
3875
3901
 
3876
- // src/pulse/plan.ts
3902
+ // src/board/plan.ts
3877
3903
  var lower = (labels) => new Set(labels.map((l) => l.toLowerCase()));
3878
3904
  function labelsDiffer(item, w) {
3879
3905
  if (w.labels.length === 0 && w.unlabels.length === 0) return false;
@@ -4093,7 +4119,7 @@ var GitHubBoards = class {
4093
4119
  const found = await repository(org, MEMORY, LABEL, this.#o);
4094
4120
  if (found === null)
4095
4121
  throw new Error(
4096
- `the pulse needs ${org}/${MEMORY} on GitHub: it is not there, or this login cannot see it`
4122
+ `boards need ${org}/${MEMORY} on GitHub: it is not there, or this login cannot see it`
4097
4123
  );
4098
4124
  this.#memories.set(org, found);
4099
4125
  return found;
@@ -4101,7 +4127,7 @@ var GitHubBoards = class {
4101
4127
  async checkIssues(org) {
4102
4128
  if (!(await this.#memory(org)).issues)
4103
4129
  throw new Error(
4104
- `the pulse needs Issues on ${org}/${MEMORY}: turn them on in its Settings`
4130
+ `boards need Issues on ${org}/${MEMORY}: turn them on in its Settings`
4105
4131
  );
4106
4132
  }
4107
4133
  /** The project only: the caller declares it before `ensureLayout` builds it. */
@@ -4602,7 +4628,7 @@ var GitHubBoards = class {
4602
4628
  const id = (await this.#memory(board.org)).labelId;
4603
4629
  if (id === null)
4604
4630
  throw new Error(
4605
- `${board.org}/${MEMORY} has no label ${LABEL}: run rness pulse sync`
4631
+ `${board.org}/${MEMORY} has no label ${LABEL}: run rness board push`
4606
4632
  );
4607
4633
  return id;
4608
4634
  }
@@ -4739,7 +4765,7 @@ var GitHubOAuthProvider = class {
4739
4765
  }
4740
4766
  #boards() {
4741
4767
  if (this.#token === null)
4742
- throw new Error("the pulse needs a GitHub login: run rness login");
4768
+ throw new Error("boards need a GitHub login: run rness login");
4743
4769
  this.#boardClient ??= new GitHubBoards(this.#api(this.#token.token));
4744
4770
  return this.#boardClient;
4745
4771
  }
@@ -5406,14 +5432,14 @@ async function loginCommand(opts, deps = {}) {
5406
5432
  ui.error(unsupportedProviderMessage(manifest.provider));
5407
5433
  return 1;
5408
5434
  }
5409
- const needsProject = opts.project === true || manifest?.projects != null;
5435
+ const needsProject = opts.project === true || manifest?.boards != null;
5410
5436
  const bin = deps.bin ?? await realpath2(process.argv[1] ?? "").catch(() => "");
5411
5437
  const env = envToken();
5412
5438
  if (env !== null)
5413
5439
  ui.hint(`${env.source} is set and wins over a stored login`);
5414
5440
  const stored = await readAuth();
5415
5441
  const resolved = stored === null ? null : await resolveToken();
5416
- const lacksProject = needsProject && resolved?.source === "login" && !splitScopes(stored?.tokens.scopes ?? "").includes(PULSE_SCOPE);
5442
+ const lacksProject = needsProject && resolved?.source === "login" && !splitScopes(stored?.tokens.scopes ?? "").includes(BOARD_SCOPE);
5417
5443
  if (stored !== null && resolved !== null && !lacksProject) {
5418
5444
  ui.line(
5419
5445
  "logged in",
@@ -5425,7 +5451,7 @@ async function loginCommand(opts, deps = {}) {
5425
5451
  }
5426
5452
  if (lacksProject) ui.line("login", "logging in again for the project scope");
5427
5453
  const code = await requestDeviceCode(
5428
- needsProject ? `${SCOPES} ${PULSE_SCOPE}` : SCOPES
5454
+ needsProject ? `${SCOPES} ${BOARD_SCOPE}` : SCOPES
5429
5455
  );
5430
5456
  if (ui.session)
5431
5457
  ui.line(
@@ -5471,7 +5497,7 @@ async function loginCommand(opts, deps = {}) {
5471
5497
  }
5472
5498
  }
5473
5499
 
5474
- // src/pulse/journal-state.ts
5500
+ // src/board/journal-state.ts
5475
5501
  import { mkdir as mkdir2, readFile as readFile7, rename as rename2, rm as rm4, writeFile as writeFile2 } from "fs/promises";
5476
5502
  import { dirname as dirname4, join as join12 } from "path";
5477
5503
  var notedOf = (value) => {
@@ -5583,13 +5609,182 @@ async function takeNotices() {
5583
5609
  return Array.isArray(now) ? now.filter((l) => typeof l === "string") : [];
5584
5610
  }
5585
5611
 
5586
- // src/commands/pulse.ts
5612
+ // src/commands/board.ts
5587
5613
  import { readFile as readFile12, readdir as readdir4 } from "fs/promises";
5588
5614
  import { join as join15 } from "path";
5589
5615
  import { setTimeout as delay3 } from "timers/promises";
5590
5616
 
5617
+ // src/board/detached.ts
5618
+ import { spawn as nodeSpawn } from "child_process";
5619
+ import { mkdir as mkdir3, open as open2, readFile as readFile8, rm as rm5, stat as stat2, writeFile as writeFile3 } from "fs/promises";
5620
+ import { join as join13 } from "path";
5621
+ import { setTimeout as delay2 } from "timers/promises";
5622
+ var detached = (args, cwd) => {
5623
+ const bin = process.argv[1];
5624
+ if (bin === void 0) return;
5625
+ const child = nodeSpawn(process.execPath, [bin, ...args], {
5626
+ detached: true,
5627
+ stdio: "ignore",
5628
+ cwd,
5629
+ env: { ...process.env, RNESS_NO_DELEGATE: "1" }
5630
+ });
5631
+ child.on("error", () => void 0);
5632
+ child.unref();
5633
+ };
5634
+ var failureFile = () => join13(configDir(), "board.json");
5635
+ var formerFailureFile = () => join13(configDir(), "pulse.json");
5636
+ async function recordFailure(reason3) {
5637
+ try {
5638
+ await mkdir3(configDir(), { recursive: true, mode: 448 });
5639
+ await writeFile3(
5640
+ failureFile(),
5641
+ `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: reason3 })}
5642
+ `
5643
+ );
5644
+ } catch {
5645
+ }
5646
+ }
5647
+ async function takeFailure() {
5648
+ return await takeFrom(failureFile()) ?? await takeFrom(formerFailureFile());
5649
+ }
5650
+ async function takeFrom(file) {
5651
+ try {
5652
+ const data = JSON.parse(await readFile8(file, "utf8"));
5653
+ await rm5(file, { force: true });
5654
+ const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
5655
+ return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
5656
+ } catch {
5657
+ return null;
5658
+ }
5659
+ }
5660
+ var LOCK_STALE_MS2 = 15 * 6e4;
5661
+ async function oneSyncAtATime(key, work, options = {}) {
5662
+ await mkdir3(configDir(), { recursive: true, mode: 448 });
5663
+ const file = join13(configDir(), `board-${key}.lock`);
5664
+ for (; ; ) {
5665
+ try {
5666
+ const handle = await open2(file, "wx");
5667
+ await handle.close();
5668
+ break;
5669
+ } catch (e) {
5670
+ if (e.code !== "EEXIST") throw e;
5671
+ const age = await stat2(file).then(
5672
+ (s) => Date.now() - s.mtimeMs,
5673
+ () => 0
5674
+ );
5675
+ if (age > LOCK_STALE_MS2) await rm5(file, { force: true });
5676
+ else if (options.wait !== true) return false;
5677
+ else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
5678
+ }
5679
+ }
5680
+ try {
5681
+ await work();
5682
+ return true;
5683
+ } finally {
5684
+ await rm5(file, { force: true });
5685
+ }
5686
+ }
5687
+
5688
+ // src/board/noted.ts
5689
+ import { mkdir as mkdir4, readFile as readFile9, rename as rename3, rm as rm6, writeFile as writeFile4 } from "fs/promises";
5690
+ import { dirname as dirname5 } from "path";
5691
+ var NAME2 = "rness/noted";
5692
+ function notedFile(rnessDir) {
5693
+ return gitPath(rnessDir, NAME2);
5694
+ }
5695
+ async function readNoted(file) {
5696
+ if (file === null) return /* @__PURE__ */ new Set();
5697
+ try {
5698
+ const data = JSON.parse(await readFile9(file, "utf8"));
5699
+ return new Set(
5700
+ Array.isArray(data) ? data.filter((l) => typeof l === "string") : []
5701
+ );
5702
+ } catch {
5703
+ return /* @__PURE__ */ new Set();
5704
+ }
5705
+ }
5706
+ async function writeNoted(file, noted) {
5707
+ if (file === null) return;
5708
+ try {
5709
+ if (noted.size === 0) {
5710
+ await rm6(file, { force: true });
5711
+ return;
5712
+ }
5713
+ await mkdir4(dirname5(file), { recursive: true });
5714
+ const temporary = `${file}.${process.pid}`;
5715
+ await writeFile4(
5716
+ temporary,
5717
+ `${JSON.stringify([...noted].sort(), null, 2)}
5718
+ `
5719
+ );
5720
+ await rename3(temporary, file);
5721
+ } catch {
5722
+ }
5723
+ }
5724
+ async function onceSaid(rnessDir, board, lines) {
5725
+ const file = await notedFile(rnessDir);
5726
+ const noted = await readNoted(file);
5727
+ const prefix = `${board}: `;
5728
+ const notes = lines.filter((l) => l.startsWith("note "));
5729
+ const now = new Set(notes.map((l) => `${prefix}${l}`));
5730
+ const said = lines.filter(
5731
+ (l) => !l.startsWith("note ") || !noted.has(`${prefix}${l}`)
5732
+ );
5733
+ const next = /* @__PURE__ */ new Set([
5734
+ ...[...noted].filter((l) => !l.startsWith(prefix)),
5735
+ ...now
5736
+ ]);
5737
+ if (next.size !== noted.size || [...next].some((l) => !noted.has(l)))
5738
+ await writeNoted(file, next);
5739
+ return said;
5740
+ }
5741
+
5742
+ // src/board/opened.ts
5743
+ import { mkdir as mkdir5, readFile as readFile10, rename as rename4, rm as rm7, writeFile as writeFile5 } from "fs/promises";
5744
+ import { dirname as dirname6 } from "path";
5745
+ var NAME3 = "rness/opened";
5746
+ function openedFile(rnessDir) {
5747
+ return gitPath(rnessDir, NAME3);
5748
+ }
5749
+ var NUMBER = /^[1-9][0-9]*$/;
5750
+ async function readOpened(file) {
5751
+ const opened = /* @__PURE__ */ new Map();
5752
+ if (file === null) return opened;
5753
+ let data;
5754
+ try {
5755
+ data = JSON.parse(await readFile10(file, "utf8"));
5756
+ } catch {
5757
+ return opened;
5758
+ }
5759
+ if (data === null || typeof data !== "object" || Array.isArray(data))
5760
+ return opened;
5761
+ for (const [key, path] of Object.entries(data))
5762
+ if (NUMBER.test(key) && typeof path === "string" && path !== "")
5763
+ opened.set(Number(key), path);
5764
+ return opened;
5765
+ }
5766
+ async function writeOpened(file, opened) {
5767
+ if (file === null) return;
5768
+ try {
5769
+ if (opened.size === 0) {
5770
+ await rm7(file, { force: true });
5771
+ return;
5772
+ }
5773
+ const entries = [...opened].sort(([a], [b]) => a - b);
5774
+ await mkdir5(dirname6(file), { recursive: true });
5775
+ const temporary = `${file}.${process.pid}`;
5776
+ await writeFile5(
5777
+ temporary,
5778
+ `${JSON.stringify(Object.fromEntries(entries), null, 2)}
5779
+ `
5780
+ );
5781
+ await rename4(temporary, file);
5782
+ } catch {
5783
+ }
5784
+ }
5785
+
5591
5786
  // src/core/board-migration.ts
5592
- import { readFile as readFile8 } from "fs/promises";
5787
+ import { readFile as readFile11 } from "fs/promises";
5593
5788
  import { parseDocument } from "yaml";
5594
5789
 
5595
5790
  // src/core/board-merge.ts
@@ -5667,7 +5862,7 @@ function mergeViews(at, base, ours, theirs, ref, kept) {
5667
5862
  return merged;
5668
5863
  }
5669
5864
  function mergeBoard(name, base, ours, theirs, ref) {
5670
- const at = `projects.${name}`;
5865
+ const at = `boards.${name}`;
5671
5866
  const kept = [];
5672
5867
  const { number, preset: _, ...team } = ours;
5673
5868
  const keys = [
@@ -5699,8 +5894,8 @@ function mergeBoard(name, base, ours, theirs, ref) {
5699
5894
  }
5700
5895
 
5701
5896
  // src/core/preset-board.ts
5702
- import { stat as stat2 } from "fs/promises";
5703
- import { join as join13 } from "path";
5897
+ import { stat as stat3 } from "fs/promises";
5898
+ import { join as join14 } from "path";
5704
5899
  var ORDER = [
5705
5900
  "number",
5706
5901
  "preset",
@@ -5722,30 +5917,30 @@ var ordered = (board) => Object.fromEntries(
5722
5917
  );
5723
5918
  async function exists2(path) {
5724
5919
  try {
5725
- await stat2(path);
5920
+ await stat3(path);
5726
5921
  return true;
5727
5922
  } catch {
5728
5923
  return false;
5729
5924
  }
5730
5925
  }
5731
- async function presetSource(name, rnessDir, number) {
5732
- if (name === PULSE) {
5733
- const preset2 = currentPreset("agent-pulse");
5926
+ async function presetSource(name, rnessDir, number, ref) {
5927
+ const kind = presetFor(name);
5928
+ const preset = ref?.includes("/") === true ? ref : currentPreset(kind);
5929
+ const numbered = number === null ? {} : { number };
5930
+ if (name === PULSE)
5734
5931
  return ordered({
5735
- number,
5736
- preset: preset2,
5737
- ...presetTemplate(preset2, { collection: name })
5932
+ ...numbered,
5933
+ preset,
5934
+ ...presetTemplate(preset, { collection: name })
5738
5935
  });
5739
- }
5740
- const preset = currentPreset("collection");
5741
5936
  const board = {
5742
- number,
5937
+ ...numbered,
5743
5938
  preset,
5744
5939
  ...presetTemplate(preset, { collection: name })
5745
5940
  };
5746
- if (await exists2(join13(rnessDir, name, "README.md")))
5941
+ if (await exists2(join14(rnessDir, name, "README.md")))
5747
5942
  board["readme"] = `${name}/README.md`;
5748
- if (await exists2(join13(rnessDir, name, "updates")))
5943
+ if (await exists2(join14(rnessDir, name, "updates")))
5749
5944
  board["updates"] = `${name}/updates`;
5750
5945
  const shape = await readShape(rnessDir, name);
5751
5946
  if (shape.description !== null) board["description"] = shape.description;
@@ -5777,23 +5972,24 @@ async function presetSource(name, rnessDir, number) {
5777
5972
  return ordered(board);
5778
5973
  }
5779
5974
  async function declaredBoard(name, entry, rnessDir) {
5780
- if (typeof entry !== "number") return entry;
5781
- return parseBoard(name, await presetSource(name, rnessDir, entry));
5975
+ if (typeof entry === "number")
5976
+ return parseBoard(name, await presetSource(name, rnessDir, entry));
5977
+ if (typeof entry === "string")
5978
+ return parseBoard(name, await presetSource(name, rnessDir, null, entry));
5979
+ return entry;
5782
5980
  }
5783
5981
  async function readBoards(rnessDir) {
5784
- let projects;
5982
+ let declared;
5785
5983
  try {
5786
- projects = (await loadManifest(rnessDir)).projects ?? {};
5984
+ declared = (await loadManifest(rnessDir)).boards ?? {};
5787
5985
  } catch {
5788
5986
  return [];
5789
5987
  }
5790
5988
  const boards = [];
5791
- for (const [name, entry] of Object.entries(projects))
5989
+ for (const [name, entry] of Object.entries(declared))
5792
5990
  try {
5793
- boards.push({
5794
- name,
5795
- declaration: await declaredBoard(name, entry, rnessDir)
5796
- });
5991
+ const declaration = await declaredBoard(name, entry, rnessDir);
5992
+ if (declaration.number !== null) boards.push({ name, declaration });
5797
5993
  } catch {
5798
5994
  }
5799
5995
  return boards;
@@ -5814,7 +6010,7 @@ var BUNDLED = {
5814
6010
  var README = (collection) => `${collection}/README.md`;
5815
6011
  async function readmeOf(rnessDir, collection) {
5816
6012
  try {
5817
- return await readFile8(
6013
+ return await readFile11(
5818
6014
  await containedPath(rnessDir, README(collection)),
5819
6015
  "utf8"
5820
6016
  );
@@ -5887,20 +6083,31 @@ function moveInto(name, collection, source, declared) {
5887
6083
  conflicts.push(`fields.${field}`);
5888
6084
  }
5889
6085
  if (conflicts.length > 0)
5890
- return `${README(collection)} declares ${conflicts.join(", ")} otherwise than projects.${name}: keep one, in rness.json`;
6086
+ return `${README(collection)} declares ${conflicts.join(", ")} otherwise than boards.${name}: keep one, in rness.json`;
5891
6087
  source["fields"] = fields2;
5892
6088
  return null;
5893
6089
  }
5894
6090
  async function migrateBoards(rnessDir, manifest, presets = BUNDLED) {
5895
6091
  const migration = {
5896
6092
  changed: false,
5897
- projects: manifest.projects,
6093
+ boards: manifest.boards,
5898
6094
  readmes: /* @__PURE__ */ new Map(),
5899
6095
  lines: []
5900
6096
  };
5901
- if (manifest.projects === null) return migration;
5902
- const projects = {};
5903
- for (const [name, entry] of Object.entries(manifest.projects)) {
6097
+ if (manifest.formerKey !== void 0) {
6098
+ migration.lines.push([
6099
+ "renamed",
6100
+ `${manifest.formerKey} to boards in .rness/rness.json`
6101
+ ]);
6102
+ migration.changed = true;
6103
+ }
6104
+ if (manifest.boards === null) return migration;
6105
+ const boards = {};
6106
+ for (const [name, entry] of Object.entries(manifest.boards)) {
6107
+ if (typeof entry === "string") {
6108
+ boards[name] = entry;
6109
+ continue;
6110
+ }
5904
6111
  let source;
5905
6112
  if (typeof entry === "number") {
5906
6113
  source = await presetSource(name, rnessDir, entry);
@@ -5950,186 +6157,21 @@ async function migrateBoards(rnessDir, manifest, presets = BUNDLED) {
5950
6157
  }
5951
6158
  }
5952
6159
  try {
5953
- projects[name] = typeof entry === "number" || migration.changed ? parseBoard(name, source, presets.knows) : entry;
6160
+ boards[name] = typeof entry === "number" || migration.changed ? parseBoard(name, source, presets.knows) : entry;
5954
6161
  } catch (e) {
5955
6162
  if (!(e instanceof BoardRefused)) throw e;
5956
6163
  migration.lines.push(["refused", e.message]);
5957
- projects[name] = entry;
6164
+ boards[name] = entry;
5958
6165
  }
5959
6166
  }
5960
- migration.projects = projects;
6167
+ migration.boards = boards;
5961
6168
  return migration;
5962
6169
  }
5963
6170
 
5964
- // src/pulse/detached.ts
5965
- import { spawn as nodeSpawn } from "child_process";
5966
- import { mkdir as mkdir3, open as open2, readFile as readFile9, rm as rm5, stat as stat3, writeFile as writeFile3 } from "fs/promises";
5967
- import { join as join14 } from "path";
5968
- import { setTimeout as delay2 } from "timers/promises";
5969
- var detached = (args, cwd) => {
5970
- const bin = process.argv[1];
5971
- if (bin === void 0) return;
5972
- const child = nodeSpawn(process.execPath, [bin, ...args], {
5973
- detached: true,
5974
- stdio: "ignore",
5975
- cwd,
5976
- env: { ...process.env, RNESS_NO_DELEGATE: "1" }
5977
- });
5978
- child.on("error", () => void 0);
5979
- child.unref();
5980
- };
5981
- var failureFile = () => join14(configDir(), "pulse.json");
5982
- async function recordFailure(reason3) {
5983
- try {
5984
- await mkdir3(configDir(), { recursive: true, mode: 448 });
5985
- await writeFile3(
5986
- failureFile(),
5987
- `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: reason3 })}
5988
- `
5989
- );
5990
- } catch {
5991
- }
5992
- }
5993
- async function takeFailure() {
5994
- try {
5995
- const data = JSON.parse(await readFile9(failureFile(), "utf8"));
5996
- await rm5(failureFile(), { force: true });
5997
- const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
5998
- return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
5999
- } catch {
6000
- return null;
6001
- }
6002
- }
6003
- var PULSE_LOCK_STALE_MS = 15 * 6e4;
6004
- async function oneSyncAtATime(key, work, options = {}) {
6005
- await mkdir3(configDir(), { recursive: true, mode: 448 });
6006
- const file = join14(configDir(), `pulse-${key}.lock`);
6007
- for (; ; ) {
6008
- try {
6009
- const handle = await open2(file, "wx");
6010
- await handle.close();
6011
- break;
6012
- } catch (e) {
6013
- if (e.code !== "EEXIST") throw e;
6014
- const age = await stat3(file).then(
6015
- (s) => Date.now() - s.mtimeMs,
6016
- () => 0
6017
- );
6018
- if (age > PULSE_LOCK_STALE_MS) await rm5(file, { force: true });
6019
- else if (options.wait !== true) return false;
6020
- else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
6021
- }
6022
- }
6023
- try {
6024
- await work();
6025
- return true;
6026
- } finally {
6027
- await rm5(file, { force: true });
6028
- }
6029
- }
6030
-
6031
- // src/pulse/noted.ts
6032
- import { mkdir as mkdir4, readFile as readFile10, rename as rename3, rm as rm6, writeFile as writeFile4 } from "fs/promises";
6033
- import { dirname as dirname5 } from "path";
6034
- var NAME2 = "rness/noted";
6035
- function notedFile(rnessDir) {
6036
- return gitPath(rnessDir, NAME2);
6037
- }
6038
- async function readNoted(file) {
6039
- if (file === null) return /* @__PURE__ */ new Set();
6040
- try {
6041
- const data = JSON.parse(await readFile10(file, "utf8"));
6042
- return new Set(
6043
- Array.isArray(data) ? data.filter((l) => typeof l === "string") : []
6044
- );
6045
- } catch {
6046
- return /* @__PURE__ */ new Set();
6047
- }
6048
- }
6049
- async function writeNoted(file, noted) {
6050
- if (file === null) return;
6051
- try {
6052
- if (noted.size === 0) {
6053
- await rm6(file, { force: true });
6054
- return;
6055
- }
6056
- await mkdir4(dirname5(file), { recursive: true });
6057
- const temporary = `${file}.${process.pid}`;
6058
- await writeFile4(
6059
- temporary,
6060
- `${JSON.stringify([...noted].sort(), null, 2)}
6061
- `
6062
- );
6063
- await rename3(temporary, file);
6064
- } catch {
6065
- }
6066
- }
6067
- async function onceSaid(rnessDir, board, lines) {
6068
- const file = await notedFile(rnessDir);
6069
- const noted = await readNoted(file);
6070
- const prefix = `${board}: `;
6071
- const notes = lines.filter((l) => l.startsWith("note "));
6072
- const now = new Set(notes.map((l) => `${prefix}${l}`));
6073
- const said = lines.filter(
6074
- (l) => !l.startsWith("note ") || !noted.has(`${prefix}${l}`)
6075
- );
6076
- const next = /* @__PURE__ */ new Set([
6077
- ...[...noted].filter((l) => !l.startsWith(prefix)),
6078
- ...now
6079
- ]);
6080
- if (next.size !== noted.size || [...next].some((l) => !noted.has(l)))
6081
- await writeNoted(file, next);
6082
- return said;
6083
- }
6084
-
6085
- // src/pulse/opened.ts
6086
- import { mkdir as mkdir5, readFile as readFile11, rename as rename4, rm as rm7, writeFile as writeFile5 } from "fs/promises";
6087
- import { dirname as dirname6 } from "path";
6088
- var NAME3 = "rness/opened";
6089
- function openedFile(rnessDir) {
6090
- return gitPath(rnessDir, NAME3);
6091
- }
6092
- var NUMBER = /^[1-9][0-9]*$/;
6093
- async function readOpened(file) {
6094
- const opened = /* @__PURE__ */ new Map();
6095
- if (file === null) return opened;
6096
- let data;
6097
- try {
6098
- data = JSON.parse(await readFile11(file, "utf8"));
6099
- } catch {
6100
- return opened;
6101
- }
6102
- if (data === null || typeof data !== "object" || Array.isArray(data))
6103
- return opened;
6104
- for (const [key, path] of Object.entries(data))
6105
- if (NUMBER.test(key) && typeof path === "string" && path !== "")
6106
- opened.set(Number(key), path);
6107
- return opened;
6108
- }
6109
- async function writeOpened(file, opened) {
6110
- if (file === null) return;
6111
- try {
6112
- if (opened.size === 0) {
6113
- await rm7(file, { force: true });
6114
- return;
6115
- }
6116
- const entries = [...opened].sort(([a], [b]) => a - b);
6117
- await mkdir5(dirname6(file), { recursive: true });
6118
- const temporary = `${file}.${process.pid}`;
6119
- await writeFile5(
6120
- temporary,
6121
- `${JSON.stringify(Object.fromEntries(entries), null, 2)}
6122
- `
6123
- );
6124
- await rename4(temporary, file);
6125
- } catch {
6126
- }
6127
- }
6128
-
6129
- // src/commands/pulse.ts
6171
+ // src/commands/board.ts
6130
6172
  var PROJECT_SCOPE = "project";
6131
- var NEEDS_LOGIN = "the pulse needs a GitHub login: run rness login";
6132
- var NEEDS_SCOPE = "the pulse needs the project scope: run rness login";
6173
+ var NEEDS_LOGIN = "boards need a GitHub login: run rness login";
6174
+ var NEEDS_SCOPE = "boards need the project scope: run rness login";
6133
6175
  async function fromGithub(work) {
6134
6176
  try {
6135
6177
  return await work;
@@ -6149,7 +6191,7 @@ async function context(opts, wait) {
6149
6191
  const manifest = await loadManifest(ws.rnessDir);
6150
6192
  const provider = await openProvider(manifest, providerOptions(opts, wait));
6151
6193
  if (manifest.org === null)
6152
- throw new Error('the pulse needs an organization: rness.json has no "org"');
6194
+ throw new Error('boards need an organization: rness.json has no "org"');
6153
6195
  return {
6154
6196
  root: ws.root,
6155
6197
  rnessDir: ws.rnessDir,
@@ -6167,17 +6209,19 @@ async function missingAccess(provider) {
6167
6209
  const scopes = await fromGithub(provider.scopes());
6168
6210
  return scopes?.includes(PROJECT_SCOPE) === true ? null : NEEDS_SCOPE;
6169
6211
  }
6212
+ var isCreated = (d) => d.number !== null;
6170
6213
  var takesAll = (d) => d.declaration.collections === "all";
6171
6214
  var namedCollections = (d) => d.collections === "all" ? [] : Object.keys(d.collections);
6172
6215
  async function declarations(c, ui) {
6173
- const projects = c.manifest.projects ?? {};
6216
+ const boards = c.manifest.boards ?? {};
6174
6217
  for (const r of c.manifest.refused ?? [])
6175
6218
  ui?.line("skipped", `${r.name}: ${r.reason}`);
6176
6219
  const all = [];
6177
- for (const [name, entry] of Object.entries(projects)) {
6220
+ const notCreated = [];
6221
+ for (const [name, entry] of Object.entries(boards)) {
6178
6222
  const declaration = await declaredBoard(name, entry, c.rnessDir);
6179
6223
  const still = [];
6180
- if (typeof entry !== "number")
6224
+ if (typeof entry === "object")
6181
6225
  for (const collection of namedCollections(declaration)) {
6182
6226
  const keys = Object.keys(
6183
6227
  await readmeDeclarations(c.rnessDir, collection)
@@ -6192,19 +6236,28 @@ async function declarations(c, ui) {
6192
6236
  );
6193
6237
  continue;
6194
6238
  }
6195
- all.push({ name, declaration });
6239
+ if (isCreated(declaration)) all.push({ name, declaration });
6240
+ else if (typeof entry !== "number")
6241
+ notCreated.push({ name, entry, declaration });
6196
6242
  }
6197
- return all.sort((a, b) => Number(takesAll(a)) - Number(takesAll(b)));
6243
+ return {
6244
+ created: all.sort((a, b) => Number(takesAll(a)) - Number(takesAll(b))),
6245
+ notCreated
6246
+ };
6247
+ }
6248
+ function noBoard(c) {
6249
+ return new Error(
6250
+ c.manifest.refused === void 0 ? 'no board declared \u2014 declare one in .rness/rness.json, as "boards": { "pulse": "agent-pulse" }, then rness board push' : `no board rness.json declares is valid: ${c.manifest.refused.map((r) => r.reason).join("; ")}`
6251
+ );
6198
6252
  }
6199
6253
  async function declaredBoards(c, ui) {
6200
- if (c.manifest.projects === null)
6201
- throw new Error(
6202
- c.manifest.refused === void 0 ? "no pulse declared \u2014 rness pulse create" : `no board rness.json declares is valid: ${c.manifest.refused.map((r) => r.reason).join("; ")}`
6203
- );
6254
+ if (c.manifest.boards === null) throw noBoard(c);
6255
+ const { created } = await declarations(c, ui);
6256
+ if (created.length === 0) return [];
6204
6257
  const missing = await missingAccess(c.provider);
6205
6258
  if (missing !== null) throw new Error(missing);
6206
6259
  const declared = [];
6207
- for (const { name, declaration } of await declarations(c, ui)) {
6260
+ for (const { name, declaration } of created) {
6208
6261
  const board = await fromGithub(c.provider.board(c.org, declaration.number));
6209
6262
  if (board === null)
6210
6263
  throw new Error(`GitHub has no project ${declaration.number} in ${c.org}`);
@@ -6442,7 +6495,7 @@ async function syncBoard(c, board, ui, sayLayout, scope) {
6442
6495
  if (!(e instanceof RateLimitError)) throw e;
6443
6496
  const total = changes + writes.length;
6444
6497
  throw new Error(
6445
- `${e.message}: ${total - made} of ${total} changes not made \u2014 the next rness pulse sync makes them`,
6498
+ `${e.message}: ${total - made} of ${total} changes not made \u2014 the next rness board push makes them`,
6446
6499
  { cause: e }
6447
6500
  );
6448
6501
  }
@@ -6470,125 +6523,131 @@ async function syncAll(c, declared, ui, sayLayout) {
6470
6523
  );
6471
6524
  }
6472
6525
  }
6473
- async function pulseCreateCommand(opts, deps = {}) {
6474
- try {
6475
- const terminal = deps.terminal ?? defaultTerminal;
6476
- const ui = deps.ui ?? await makeUi(terminal);
6477
- const wait = waitSaid(opts, ui);
6478
- let c = await context(opts, wait);
6479
- const name = opts.collection ?? PULSE;
6480
- const declared = c.manifest.projects?.[name];
6481
- if (opts.collection === PULSE)
6482
- throw new Error('"pulse" names Agent Pulse: rness pulse create');
6483
- if (declared !== void 0)
6484
- throw new Error(
6485
- `already declared: ${boardUrl(c.org, projectNumber(declared))} \u2014 rness pulse sync`
6486
- );
6487
- const refused = c.manifest.refused?.find((r) => r.name === name);
6488
- if (refused !== void 0)
6489
- throw new Error(`already declared, and refused: ${refused.reason}`);
6490
- const tab = opts.collection === void 0 ? null : (await statusTabs(c.rnessDir)).find(
6491
- (t) => t.name === opts.collection
6492
- ) ?? null;
6493
- if (opts.collection !== void 0 && tab === null)
6494
- throw new Error(
6495
- `${opts.collection} is no collection of .rness: none of its documents carries a status`
6496
- );
6497
- const detected = providerOf(c.manifest);
6498
- if (!PROVIDERS[detected].available)
6499
- throw new Error(
6500
- `the pulse needs a GitHub organization, and this workspace's repositories look like ${detected} (detected): write "provider": "github" in rness.json if the organization is on GitHub`
6501
- );
6502
- const missing = await missingAccess(c.provider);
6503
- if (missing !== null) {
6504
- if (opts.yes === true || !terminal.isTty()) throw new Error(missing);
6505
- const p = await terminal.prompts();
6506
- const ok = await p.confirm({
6507
- message: missing === NEEDS_LOGIN ? "The pulse needs a GitHub login with the project scope. Log in now?" : "The pulse needs the project scope of your GitHub login. Log in again to grant it?"
6508
- });
6509
- if (p.isCancel(ok) || ok !== true) throw new Error(missing);
6510
- const code = await loginCommand(
6511
- {
6512
- project: true,
6513
- // The git question is `rness login`'s own, not the pulse's to ask.
6514
- setupGit: false,
6515
- ...opts.githubApi === void 0 ? {} : { githubApi: opts.githubApi }
6516
- },
6517
- { terminal, ui, ...opts.cwd === void 0 ? {} : { cwd: opts.cwd } }
6518
- );
6519
- if (code !== 0) return code;
6520
- c = {
6521
- ...c,
6522
- provider: await openProvider(c.manifest, providerOptions(opts, wait))
6523
- };
6524
- const still = await missingAccess(c.provider);
6525
- if (still !== null) throw new Error(still);
6526
- }
6527
- await fromGithub(c.provider.checkIssues(c.org));
6528
- const login = (await c.provider.identity())?.login ?? "unknown";
6529
- ui.line(
6530
- "checked",
6531
- `${detected}, logged in as ${login}, scope ${PROJECT_SCOPE}`
6526
+ async function checkBoardName(rnessDir, name) {
6527
+ if (name === PULSE) return;
6528
+ const tabs = await statusTabs(rnessDir);
6529
+ if (!tabs.some((t) => t.name === name))
6530
+ throw new Error(
6531
+ `"boards.${name}": ${name} is no collection of .rness: none of its documents carries a status`
6532
+ );
6533
+ }
6534
+ var titleOf = (b) => b.declaration.title ?? labelOf(b.name);
6535
+ async function createBoards(c, boards, opts, terminal, ui, wait) {
6536
+ for (const b of boards) await checkBoardName(c.rnessDir, b.name);
6537
+ const detected = providerOf(c.manifest);
6538
+ if (!PROVIDERS[detected].available)
6539
+ throw new Error(
6540
+ `boards need a GitHub organization, and this workspace's repositories look like ${detected} (detected): write "provider": "github" in rness.json if the organization is on GitHub`
6532
6541
  );
6533
- const source = await presetSource(name, c.rnessDir, 1);
6534
- const readme = opts.collection === void 0 ? null : await readmeText(c.rnessDir, opts.collection);
6535
- const title = typeof source["title"] === "string" ? source["title"] : name;
6542
+ const names = boards.map((b) => b.name);
6543
+ if (opts.confirmed !== true && opts.yes !== true) {
6544
+ if (!terminal.isTty()) return { c, made: [], left: names };
6545
+ const p = await terminal.prompts();
6546
+ const ok = await p.confirm({
6547
+ message: `Create ${boards.map((b) => `${titleOf(b)} (${b.name})`).join(", ")} on GitHub, in ${c.org}?`
6548
+ });
6549
+ if (p.isCancel(ok) || ok !== true) return { c, made: [], left: names };
6550
+ }
6551
+ const missing = await missingAccess(c.provider);
6552
+ if (missing !== null) {
6553
+ if (opts.yes === true || !terminal.isTty()) throw new Error(missing);
6554
+ const p = await terminal.prompts();
6555
+ const ok = await p.confirm({
6556
+ message: missing === NEEDS_LOGIN ? "Boards need a GitHub login with the project scope. Log in now?" : "Boards need the project scope of your GitHub login. Log in again to grant it?"
6557
+ });
6558
+ if (p.isCancel(ok) || ok !== true) throw new Error(missing);
6559
+ const code = await loginCommand(
6560
+ {
6561
+ project: true,
6562
+ // The git question is `rness login`'s own, not the board's to ask.
6563
+ setupGit: false,
6564
+ ...opts.githubApi === void 0 ? {} : { githubApi: opts.githubApi }
6565
+ },
6566
+ { terminal, ui, ...opts.cwd === void 0 ? {} : { cwd: opts.cwd } }
6567
+ );
6568
+ if (code !== 0) throw new Error(missing);
6569
+ c = {
6570
+ ...c,
6571
+ provider: await openProvider(c.manifest, providerOptions(opts, wait))
6572
+ };
6573
+ const still = await missingAccess(c.provider);
6574
+ if (still !== null) throw new Error(still);
6575
+ }
6576
+ await fromGithub(c.provider.checkIssues(c.org));
6577
+ const login = (await c.provider.identity())?.login ?? "unknown";
6578
+ ui.line(
6579
+ "checked",
6580
+ `${detected}, logged in as ${login}, scope ${PROJECT_SCOPE}`
6581
+ );
6582
+ const made = [];
6583
+ for (const b of boards) {
6584
+ const source = typeof b.entry === "string" ? await presetSource(b.name, c.rnessDir, null, b.entry) : b.declaration.source;
6585
+ const readme = typeof b.entry === "string" && b.name !== PULSE ? await readmeText(c.rnessDir, b.name) : null;
6586
+ const title = titleOf(b);
6536
6587
  const board = await fromGithub(c.provider.createBoard(c.org, title));
6537
6588
  const manifest = {
6538
6589
  ...c.manifest,
6539
6590
  provider: detected,
6540
- projects: {
6541
- ...c.manifest.projects,
6542
- [name]: parseBoard(name, { ...source, number: board.number })
6591
+ boards: {
6592
+ ...c.manifest.boards,
6593
+ [b.name]: parseBoard(b.name, { number: board.number, ...source })
6543
6594
  }
6544
6595
  };
6545
6596
  await writeManifest(c.rnessDir, manifest);
6546
- if (readme !== null && opts.collection !== void 0) {
6597
+ if (readme !== null) {
6547
6598
  const cleaned = withoutDeclarations(readme);
6548
6599
  if (cleaned !== readme)
6549
- await writeFileAtomic(
6550
- join15(c.rnessDir, opts.collection, "README.md"),
6551
- cleaned
6552
- );
6600
+ await writeFileAtomic(join15(c.rnessDir, b.name, "README.md"), cleaned);
6553
6601
  }
6554
6602
  c = { ...c, manifest };
6603
+ made.push(b.name);
6555
6604
  ui.line("created", `${title} \u2014 ${board.url}`);
6605
+ }
6606
+ return { c, made, left: [] };
6607
+ }
6608
+ async function boardPushCommand(opts, deps = {}) {
6609
+ try {
6610
+ const terminal = deps.terminal ?? defaultTerminal;
6611
+ const ui = deps.ui ?? await makeUi(terminal);
6612
+ const wait = waitSaid(opts, ui);
6613
+ let c = await context(opts, wait);
6614
+ if (c.manifest.boards === null) throw noBoard(c);
6615
+ const { notCreated } = await declarations(c, ui);
6616
+ let made = [];
6617
+ let left = [];
6618
+ if (notCreated.length > 0)
6619
+ ({ c, made, left } = await createBoards(
6620
+ c,
6621
+ notCreated,
6622
+ opts,
6623
+ terminal,
6624
+ ui,
6625
+ wait
6626
+ ));
6556
6627
  try {
6628
+ const declared = await declaredBoards(c);
6557
6629
  await syncAll(
6558
6630
  c,
6559
- await declaredBoards(c),
6631
+ declared,
6560
6632
  ui,
6561
- (n) => n === name ? createdByKind : eachAdded
6633
+ (n) => made.includes(n) ? createdByKind : eachAdded
6562
6634
  );
6563
6635
  } finally {
6564
- ui.line(
6565
- "declared",
6566
- `${name} in .rness/rness.json \u2014 commit it: git -C .rness commit -am "chore: rness pulse${opts.collection === void 0 ? "" : ` ${opts.collection}`}"`
6567
- );
6636
+ if (made.length > 0)
6637
+ ui.line(
6638
+ "declared",
6639
+ `${made.join(", ")} in .rness/rness.json \u2014 commit it: git -C .rness commit -am "chore: board ${made.join(", ")}"`
6640
+ );
6568
6641
  }
6642
+ if (left.length > 0)
6643
+ throw new Error(
6644
+ `not created: ${left.join(", ")} \u2014 rness board push in a terminal, or with --yes, creates ${left.length === 1 ? "it" : "them"}`
6645
+ );
6569
6646
  return 0;
6570
6647
  } catch (e) {
6571
6648
  return reportError(e);
6572
6649
  }
6573
6650
  }
6574
- async function pulseSyncCommand(opts, deps = {}) {
6575
- try {
6576
- const ui = deps.ui ?? await makeUi(deps.terminal ?? defaultTerminal);
6577
- const c = await context(opts, waitSaid(opts, ui));
6578
- await syncAll(c, await declaredBoards(c, ui), ui, () => eachAdded);
6579
- return 0;
6580
- } catch (e) {
6581
- return reportError(e);
6582
- }
6583
- }
6584
- async function pulseMarkCommand(opts) {
6585
- try {
6586
- return await mark(opts);
6587
- } catch (e) {
6588
- await recordFailure(e instanceof Error ? e.message : String(e));
6589
- return 0;
6590
- }
6591
- }
6592
6651
  function markFields(d) {
6593
6652
  const fields2 = Object.entries(d.fields);
6594
6653
  const agent = fields2.find(
@@ -6658,29 +6717,7 @@ async function clearOn(c, d, session) {
6658
6717
  }).map((i) => i.id);
6659
6718
  await fromGithub(c.provider.mark(d.board, ids, null, fields2));
6660
6719
  }
6661
- async function mark(opts) {
6662
- const c = await context(opts);
6663
- const declared = await declaredBoards(c);
6664
- const lock = opts.sleep === void 0 ? {} : { sleep: opts.sleep };
6665
- if (opts.end === true) {
6666
- for (const d of declared) await clearOn(c, d, opts.session);
6667
- await oneSyncAtATime(
6668
- lockKey(c, declared),
6669
- () => syncAll(c, declared, plainUi, () => eachAdded),
6670
- { ...lock, wait: true }
6671
- );
6672
- return 0;
6673
- }
6674
- const wanted = /* @__PURE__ */ new Map();
6675
- for (const d of declared) {
6676
- const paths = opts.paths.filter((p) => holds(d, p));
6677
- if (paths.length > 0) wanted.set(d, new Set(paths));
6678
- }
6679
- if (wanted.size === 0) return 0;
6680
- await markOn(c, declared, wanted, opts.session, lock);
6681
- return 0;
6682
- }
6683
- async function pulseRunCommand(opts) {
6720
+ async function boardRunCommand(opts) {
6684
6721
  const failures = [];
6685
6722
  try {
6686
6723
  await run(opts, failures);
@@ -6703,7 +6740,7 @@ async function run(opts, failures) {
6703
6740
  if (event === "session-end") {
6704
6741
  for (const d of acting("journal-summary"))
6705
6742
  try {
6706
- const { postSummary } = await import("./summary-UCMFESSA.js");
6743
+ const { postSummary } = await import("./summary-47SFWNSQ.js");
6707
6744
  await postSummary({
6708
6745
  c,
6709
6746
  board: d,
@@ -6764,6 +6801,33 @@ async function run(opts, failures) {
6764
6801
  )
6765
6802
  );
6766
6803
  }
6804
+ async function pulseCreateCommand(collection, opts, deps = {}) {
6805
+ try {
6806
+ const name = collection ?? PULSE;
6807
+ const ws = await findWorkspace(opts.cwd ?? process.cwd());
6808
+ const manifest = await loadManifest(ws.rnessDir);
6809
+ if (manifest.org === null)
6810
+ throw new Error('boards need an organization: rness.json has no "org"');
6811
+ const refused = manifest.refused?.find((r) => r.name === name);
6812
+ if (refused !== void 0)
6813
+ throw new Error(`already declared, and refused: ${refused.reason}`);
6814
+ let said = "rness pulse create is now rness board push";
6815
+ if (manifest.boards?.[name] === void 0) {
6816
+ await checkBoardName(ws.rnessDir, name);
6817
+ const preset = presetFor(name);
6818
+ await writeManifest(ws.rnessDir, {
6819
+ ...manifest,
6820
+ boards: { ...manifest.boards, [name]: preset }
6821
+ });
6822
+ said = `boards are declared in rness.json now: added "${name}": "${preset}", then rness board push`;
6823
+ }
6824
+ process.stderr.write(`${said}
6825
+ `);
6826
+ return await boardPushCommand({ ...opts, confirmed: true }, deps);
6827
+ } catch (e) {
6828
+ return reportError(e);
6829
+ }
6830
+ }
6767
6831
 
6768
6832
  export {
6769
6833
  exists,
@@ -6787,6 +6851,7 @@ export {
6787
6851
  presetTemplate,
6788
6852
  BoardRefused,
6789
6853
  contractOf,
6854
+ PULSE,
6790
6855
  parseBoard,
6791
6856
  clone,
6792
6857
  isClean,
@@ -6809,7 +6874,6 @@ export {
6809
6874
  NAME_RULE,
6810
6875
  ORG_NAME,
6811
6876
  workspaceName,
6812
- PULSE,
6813
6877
  loadManifest,
6814
6878
  parseManifest,
6815
6879
  writeManifest,
@@ -6871,12 +6935,12 @@ export {
6871
6935
  readBoards,
6872
6936
  readmeDeclarations,
6873
6937
  migrateBoards,
6938
+ detached,
6939
+ takeFailure,
6874
6940
  openInBrowser,
6875
6941
  canOpenBrowser,
6876
6942
  removeGitSetup,
6877
6943
  loginCommand,
6878
- detached,
6879
- takeFailure,
6880
6944
  notesPosted,
6881
6945
  notePosted,
6882
6946
  issuesNoted,
@@ -6889,9 +6953,8 @@ export {
6889
6953
  fromGithub,
6890
6954
  context,
6891
6955
  declaredBoards,
6892
- pulseCreateCommand,
6893
- pulseSyncCommand,
6894
- pulseMarkCommand,
6895
- pulseRunCommand
6956
+ boardPushCommand,
6957
+ boardRunCommand,
6958
+ pulseCreateCommand
6896
6959
  };
6897
- //# sourceMappingURL=chunk-D3D2WCGW.js.map
6960
+ //# sourceMappingURL=chunk-XCO5ZT2H.js.map