@rness/cli 0.21.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -217,7 +217,9 @@ function documentNumber(rel) {
217
217
  }
218
218
  function documentTitle(heading, number) {
219
219
  if (number === null) return heading;
220
- const rest = new RegExp(`^${number}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
220
+ const rest = new RegExp(`^${number}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(
221
+ heading
222
+ )?.[1];
221
223
  return rest ?? heading;
222
224
  }
223
225
 
@@ -288,7 +290,7 @@ async function checkContract(rnessDir, override) {
288
290
  for (const key of PLACEMENT_KEYS) {
289
291
  if (Object.hasOwn(item.fields, key)) {
290
292
  problems.push(
291
- `${where}: front matter "${key}" is not supported \u2014 the directory decides the scope; move the file`
293
+ `${where}: front matter "${key}" is not supported: the directory decides the scope; move the file`
292
294
  );
293
295
  }
294
296
  }
@@ -856,16 +858,33 @@ function readHooks(value, at, fields2, holdsPlans) {
856
858
  ).withColon();
857
859
  return hooks;
858
860
  }
861
+ var PULSE = "pulse";
862
+ var presetFor = (name) => name === PULSE ? "agent-pulse" : "collection";
863
+ var PRESET_NAME = /^[a-z][a-z0-9-]*(?:\/[1-9][0-9]*)?$/;
864
+ function readPresetEntry(name, value, knows = isPresetRevision) {
865
+ const at = `boards.${name}`;
866
+ const preset = presetFor(name);
867
+ if (!PRESET_NAME.test(value))
868
+ throw new BoardRefused(at, `must name a preset, as "${preset}"`);
869
+ if (presetNameOf(value) !== preset)
870
+ throw new BoardRefused(
871
+ at,
872
+ name === PULSE ? `names ${value}: ${PULSE} is made from ${preset}` : `names ${value}: a collection's board is made from ${preset}`
873
+ );
874
+ if (value.includes("/") && !knows(value))
875
+ throw new BoardRefused(at, `names ${value}, which this rness does not have`);
876
+ return value;
877
+ }
859
878
  function parseBoard(name, value, knows = isPresetRevision) {
860
- const at = `projects.${name}`;
879
+ const at = `boards.${name}`;
861
880
  if (!isRecord(value))
862
881
  throw new BoardRefused(
863
882
  at,
864
- 'must be an object, as { "number": 5, "collections": \u2026, "views": \u2026 }'
883
+ 'must be an object, as { "collections": \u2026, "views": \u2026 }'
865
884
  );
866
885
  unknownKeys(value, BOARD_KEYS, at, "a board");
867
- const number = value["number"];
868
- if (typeof number !== "number" || !Number.isInteger(number) || number < 1)
886
+ const number = value["number"] ?? null;
887
+ if (number !== null && (typeof number !== "number" || !Number.isInteger(number) || number < 1))
869
888
  throw new BoardRefused(`${at}.number`, "must be a project number");
870
889
  const preset = value["preset"];
871
890
  if (preset !== void 0 && (typeof preset !== "string" || !PRESET.test(preset)))
@@ -1230,8 +1249,10 @@ var KEYS = [
1230
1249
  "provider",
1231
1250
  "org",
1232
1251
  "agents",
1233
- "pulse",
1252
+ "boards",
1253
+ // Former names of `boards`, still read: `sync` renames them (spec 0033 §3).
1234
1254
  "projects",
1255
+ "pulse",
1235
1256
  "repos",
1236
1257
  "scopes"
1237
1258
  ];
@@ -1287,52 +1308,57 @@ function readProvider(value) {
1287
1308
  return name;
1288
1309
  }
1289
1310
  function readPulse(value) {
1290
- if (value === void 0) return null;
1291
1311
  if (!isRecord2(value) || Object.keys(value).length !== 1 || typeof value.project !== "number" || !Number.isInteger(value.project) || value.project < 1)
1292
1312
  fail2(
1293
1313
  `"pulse" must be { "project": <number> } (got ${JSON.stringify(value)})`
1294
1314
  );
1295
1315
  return { [PULSE]: value.project };
1296
1316
  }
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._-]*$/;
1317
+ var BOARD_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
1300
1318
  var isProjectNumber = (n) => typeof n === "number" && Number.isInteger(n) && n >= 1;
1301
- function readProjects(value) {
1302
- if (value === void 0) return { projects: null, refused: [] };
1319
+ function readBoardEntries(value) {
1303
1320
  const entries = isRecord2(value) ? Object.entries(value) : [];
1304
- const numbers = entries.map(
1305
- ([, n]) => isRecord2(n) && isProjectNumber(n["number"]) ? n["number"] : n
1321
+ const numbers = entries.flatMap(
1322
+ ([, n]) => isRecord2(n) ? isProjectNumber(n["number"]) ? [n["number"]] : [] : [n]
1306
1323
  );
1307
1324
  if (entries.length === 0 || !entries.every(
1308
- ([name, n]) => PROJECT_NAME.test(name) && (isProjectNumber(n) || isRecord2(n))
1325
+ ([name, n]) => BOARD_NAME.test(name) && (isProjectNumber(n) || typeof n === "string" || isRecord2(n))
1309
1326
  ) || new Set(numbers).size !== numbers.length)
1310
1327
  fail2(
1311
- `"projects" must map names to distinct project numbers, as { "pulse": 4, "marketing": 5 } (got ${JSON.stringify(value)})`
1328
+ `"boards" must map names to boards on distinct projects, as { "pulse": "agent-pulse", "marketing": 5 } (got ${JSON.stringify(value)})`
1312
1329
  );
1313
- const projects = {};
1330
+ const boards = {};
1314
1331
  const refused = [];
1315
1332
  for (const [name, entry] of entries) {
1316
1333
  if (isProjectNumber(entry)) {
1317
- projects[name] = entry;
1334
+ boards[name] = entry;
1318
1335
  continue;
1319
1336
  }
1320
1337
  try {
1321
- projects[name] = parseBoard(name, entry);
1338
+ boards[name] = typeof entry === "string" ? readPresetEntry(name, entry) : parseBoard(name, entry);
1322
1339
  } catch (e) {
1323
1340
  if (!(e instanceof BoardRefused)) throw e;
1324
1341
  refused.push({ name, reason: e.message, source: entry });
1325
1342
  }
1326
1343
  }
1327
1344
  return {
1328
- projects: Object.keys(projects).length === 0 ? null : projects,
1345
+ boards: Object.keys(boards).length === 0 ? null : boards,
1329
1346
  refused
1330
1347
  };
1331
1348
  }
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: [] };
1349
+ var HOLDERS = ["boards", "projects", "pulse"];
1350
+ function readBoardsOrFormer(data) {
1351
+ const present = HOLDERS.filter((k) => data[k] !== void 0);
1352
+ if (present.length > 1)
1353
+ fail2(
1354
+ `${present.map((k) => JSON.stringify(k)).join(" and ")} at once: keep "boards" only`
1355
+ );
1356
+ const key = present[0];
1357
+ if (key === void 0) return { boards: null, refused: [] };
1358
+ if (key === "pulse")
1359
+ return { boards: readPulse(data.pulse), refused: [], formerKey: key };
1360
+ const read = readBoardEntries(data[key]);
1361
+ return key === "boards" ? read : { ...read, formerKey: key };
1336
1362
  }
1337
1363
  function providerOf(manifest) {
1338
1364
  if (manifest.provider !== null) return manifest.provider;
@@ -1399,34 +1425,36 @@ function parseManifest(raw) {
1399
1425
  for (const key of Object.keys(data)) {
1400
1426
  if (!KEYS.includes(key)) fail2(`unknown key ${JSON.stringify(key)}`);
1401
1427
  }
1402
- const { projects, refused } = readProjectsOrPulse(data);
1428
+ const { boards, refused, formerKey } = readBoardsOrFormer(data);
1403
1429
  return {
1404
1430
  contract: 1,
1405
1431
  provider: readProvider(data.provider),
1406
1432
  org: readOrg(data.org),
1407
1433
  agents: readAgents(data.agents),
1408
- projects,
1434
+ boards,
1409
1435
  ...refused.length === 0 ? {} : { refused },
1436
+ ...formerKey === void 0 ? {} : { formerKey },
1410
1437
  repos: readRepos(data.repos),
1411
1438
  scopes: readScopes(data.scopes)
1412
1439
  };
1413
1440
  }
1414
- var projectEntry = (name, value) => ` ${JSON.stringify(name)}: ${JSON.stringify(value, null, 2).replaceAll("\n", "\n ")}`;
1415
- function projectsLines(manifest) {
1441
+ var boardEntry = (name, value) => ` ${JSON.stringify(name)}: ${JSON.stringify(value, null, 2).replaceAll("\n", "\n ")}`;
1442
+ var isScalar = (v) => typeof v === "number" || typeof v === "string";
1443
+ function boardsLines(manifest) {
1416
1444
  const entries = [
1417
- ...Object.entries(manifest.projects ?? {}).map(
1418
- ([name, p]) => [name, typeof p === "number" ? p : p.source]
1445
+ ...Object.entries(manifest.boards ?? {}).map(
1446
+ ([name, b]) => [name, isScalar(b) ? b : b.source]
1419
1447
  ),
1420
1448
  ...(manifest.refused ?? []).map((r) => [r.name, r.source])
1421
1449
  ];
1422
1450
  if (entries.length === 0) return [];
1423
- if (entries.every(([, v]) => typeof v === "number"))
1451
+ if (entries.every(([, v]) => isScalar(v)))
1424
1452
  return [
1425
- ` "projects": { ${entries.map(([name, n]) => `${JSON.stringify(name)}: ${String(n)}`).join(", ")} },`
1453
+ ` "boards": { ${entries.map(([name, v]) => `${JSON.stringify(name)}: ${JSON.stringify(v)}`).join(", ")} },`
1426
1454
  ];
1427
1455
  return [
1428
- ' "projects": {',
1429
- entries.map(([name, v]) => projectEntry(name, v)).join(",\n"),
1456
+ ' "boards": {',
1457
+ entries.map(([name, v]) => boardEntry(name, v)).join(",\n"),
1430
1458
  " },"
1431
1459
  ];
1432
1460
  }
@@ -1447,7 +1475,7 @@ async function writeManifest(rnessDir, manifest) {
1447
1475
  lines.push(
1448
1476
  ` "agents": [${manifest.agents.map((a) => JSON.stringify(a)).join(", ")}],`
1449
1477
  );
1450
- lines.push(...projectsLines(manifest));
1478
+ lines.push(...boardsLines(manifest));
1451
1479
  lines.push(
1452
1480
  ' "repos": {',
1453
1481
  repos.join(",\n"),
@@ -1489,7 +1517,7 @@ function parseRepoSpec(spec, org, host) {
1489
1517
  }
1490
1518
 
1491
1519
  // src/version.ts
1492
- var VERSION = "0.21.0";
1520
+ var VERSION = "0.22.1";
1493
1521
 
1494
1522
  // src/core/pm.ts
1495
1523
  import { execFile as execFile2 } from "child_process";
@@ -1715,7 +1743,7 @@ async function documentsIn(ws, manifest, scope, collections, statuses) {
1715
1743
  return paths;
1716
1744
  }
1717
1745
 
1718
- // src/pulse/urls.ts
1746
+ // src/board/urls.ts
1719
1747
  var boardUrl = (org, project) => `https://github.com/orgs/${org}/projects/${project}`;
1720
1748
  var itemUrl = (org, project, path) => `${boardUrl(org, project)}?filterQuery=${encodeURIComponent(`path:"${path}"`)}`;
1721
1749
  var blobUrl = (org, path) => `https://github.com/${org}/.rness/blob/main/${encodeURI(path)}`;
@@ -3053,7 +3081,7 @@ import { join as join8 } from "path";
3053
3081
  var GITHUB_CLIENT_ID = "Ov23liv1ia2UWeWttsOK";
3054
3082
  var GITHUB_WEB = "https://github.com";
3055
3083
  var SCOPES = "repo read:org";
3056
- var PULSE_SCOPE = "project";
3084
+ var BOARD_SCOPE = "project";
3057
3085
  function clientId() {
3058
3086
  return process.env["RNESS_GITHUB_CLIENT_ID"] ?? GITHUB_CLIENT_ID;
3059
3087
  }
@@ -3312,7 +3340,7 @@ async function resolveToken(deps = {}) {
3312
3340
  return refreshed === null ? null : answer(refreshed);
3313
3341
  }
3314
3342
 
3315
- // src/pulse/body.ts
3343
+ // src/board/body.ts
3316
3344
  import { createHash } from "crypto";
3317
3345
  import { posix as posix4 } from "path";
3318
3346
  var BODY_LIMIT = 65536;
@@ -3462,7 +3490,7 @@ ${content}`;
3462
3490
  ${comment2(digest(text))}`;
3463
3491
  }
3464
3492
 
3465
- // src/pulse/collection.ts
3493
+ // src/board/collection.ts
3466
3494
  import { readFile as readFile5, readdir as readdir2 } from "fs/promises";
3467
3495
  import { join as join9 } from "path";
3468
3496
  var FIELD_TYPES2 = ["text", "date", "select", "number"];
@@ -3654,7 +3682,7 @@ async function readUpdates(rnessDir, dir) {
3654
3682
  return updates;
3655
3683
  }
3656
3684
 
3657
- // src/pulse/layout.ts
3685
+ // src/board/layout.ts
3658
3686
  var statusOf2 = (status2) => status2 === null || status2 === "?" ? null : status2;
3659
3687
  var LIFECYCLE = [
3660
3688
  "Draft",
@@ -3852,7 +3880,7 @@ function desiredOf(board, collections, org, fronts = /* @__PURE__ */ new Map(),
3852
3880
  }
3853
3881
  return {
3854
3882
  path: row.path,
3855
- title: `${row.id} \u2014 ${row.title}`,
3883
+ title: `${row.id} - ${row.title}`,
3856
3884
  body: headerOf(row.path, org),
3857
3885
  status: status2,
3858
3886
  statusField: c.field,
@@ -3873,7 +3901,7 @@ function desiredOf(board, collections, org, fronts = /* @__PURE__ */ new Map(),
3873
3901
  }));
3874
3902
  }
3875
3903
 
3876
- // src/pulse/plan.ts
3904
+ // src/board/plan.ts
3877
3905
  var lower = (labels) => new Set(labels.map((l) => l.toLowerCase()));
3878
3906
  function labelsDiffer(item, w) {
3879
3907
  if (w.labels.length === 0 && w.unlabels.length === 0) return false;
@@ -4093,7 +4121,7 @@ var GitHubBoards = class {
4093
4121
  const found = await repository(org, MEMORY, LABEL, this.#o);
4094
4122
  if (found === null)
4095
4123
  throw new Error(
4096
- `the pulse needs ${org}/${MEMORY} on GitHub: it is not there, or this login cannot see it`
4124
+ `boards need ${org}/${MEMORY} on GitHub: it is not there, or this login cannot see it`
4097
4125
  );
4098
4126
  this.#memories.set(org, found);
4099
4127
  return found;
@@ -4101,7 +4129,7 @@ var GitHubBoards = class {
4101
4129
  async checkIssues(org) {
4102
4130
  if (!(await this.#memory(org)).issues)
4103
4131
  throw new Error(
4104
- `the pulse needs Issues on ${org}/${MEMORY}: turn them on in its Settings`
4132
+ `boards need Issues on ${org}/${MEMORY}: turn them on in its Settings`
4105
4133
  );
4106
4134
  }
4107
4135
  /** The project only: the caller declares it before `ensureLayout` builds it. */
@@ -4271,7 +4299,7 @@ var GitHubBoards = class {
4271
4299
  if (there.layout !== LAYOUTS[view.layout] || view.layout === "board" && there.columnField !== view.columns) {
4272
4300
  await deleteView(there.id, this.#o);
4273
4301
  added.push(
4274
- `view ${view.name} remade${view.layout === "board" ? ` \u2014 columns ${view.columns}` : ""}`
4302
+ `view ${view.name} remade${view.layout === "board" ? `, columns ${view.columns}` : ""}`
4275
4303
  );
4276
4304
  await create(view);
4277
4305
  gone2(view.name);
@@ -4602,7 +4630,7 @@ var GitHubBoards = class {
4602
4630
  const id = (await this.#memory(board.org)).labelId;
4603
4631
  if (id === null)
4604
4632
  throw new Error(
4605
- `${board.org}/${MEMORY} has no label ${LABEL}: run rness pulse sync`
4633
+ `${board.org}/${MEMORY} has no label ${LABEL}: run rness board push`
4606
4634
  );
4607
4635
  return id;
4608
4636
  }
@@ -4739,7 +4767,7 @@ var GitHubOAuthProvider = class {
4739
4767
  }
4740
4768
  #boards() {
4741
4769
  if (this.#token === null)
4742
- throw new Error("the pulse needs a GitHub login: run rness login");
4770
+ throw new Error("boards need a GitHub login: run rness login");
4743
4771
  this.#boardClient ??= new GitHubBoards(this.#api(this.#token.token));
4744
4772
  return this.#boardClient;
4745
4773
  }
@@ -5117,7 +5145,7 @@ var PROVIDERS = {
5117
5145
  gitlab: { name: "gitlab", label: "GitLab", available: false },
5118
5146
  atlassian: {
5119
5147
  name: "atlassian",
5120
- label: "Atlassian \u2014 Bitbucket + Jira",
5148
+ label: "Atlassian (Bitbucket + Jira)",
5121
5149
  available: false
5122
5150
  }
5123
5151
  };
@@ -5338,7 +5366,7 @@ function sessionUi(p) {
5338
5366
  spin.stop(line === null ? label : label_(line));
5339
5367
  return result;
5340
5368
  } catch (e) {
5341
- spin.error(`${label} \u2014 failed`);
5369
+ spin.error(`${label}: failed`);
5342
5370
  throw e;
5343
5371
  }
5344
5372
  }
@@ -5406,14 +5434,14 @@ async function loginCommand(opts, deps = {}) {
5406
5434
  ui.error(unsupportedProviderMessage(manifest.provider));
5407
5435
  return 1;
5408
5436
  }
5409
- const needsProject = opts.project === true || manifest?.projects != null;
5437
+ const needsProject = opts.project === true || manifest?.boards != null;
5410
5438
  const bin = deps.bin ?? await realpath2(process.argv[1] ?? "").catch(() => "");
5411
5439
  const env = envToken();
5412
5440
  if (env !== null)
5413
5441
  ui.hint(`${env.source} is set and wins over a stored login`);
5414
5442
  const stored = await readAuth();
5415
5443
  const resolved = stored === null ? null : await resolveToken();
5416
- const lacksProject = needsProject && resolved?.source === "login" && !splitScopes(stored?.tokens.scopes ?? "").includes(PULSE_SCOPE);
5444
+ const lacksProject = needsProject && resolved?.source === "login" && !splitScopes(stored?.tokens.scopes ?? "").includes(BOARD_SCOPE);
5417
5445
  if (stored !== null && resolved !== null && !lacksProject) {
5418
5446
  ui.line(
5419
5447
  "logged in",
@@ -5425,7 +5453,7 @@ async function loginCommand(opts, deps = {}) {
5425
5453
  }
5426
5454
  if (lacksProject) ui.line("login", "logging in again for the project scope");
5427
5455
  const code = await requestDeviceCode(
5428
- needsProject ? `${SCOPES} ${PULSE_SCOPE}` : SCOPES
5456
+ needsProject ? `${SCOPES} ${BOARD_SCOPE}` : SCOPES
5429
5457
  );
5430
5458
  if (ui.session)
5431
5459
  ui.line(
@@ -5471,7 +5499,7 @@ async function loginCommand(opts, deps = {}) {
5471
5499
  }
5472
5500
  }
5473
5501
 
5474
- // src/pulse/journal-state.ts
5502
+ // src/board/journal-state.ts
5475
5503
  import { mkdir as mkdir2, readFile as readFile7, rename as rename2, rm as rm4, writeFile as writeFile2 } from "fs/promises";
5476
5504
  import { dirname as dirname4, join as join12 } from "path";
5477
5505
  var notedOf = (value) => {
@@ -5583,13 +5611,182 @@ async function takeNotices() {
5583
5611
  return Array.isArray(now) ? now.filter((l) => typeof l === "string") : [];
5584
5612
  }
5585
5613
 
5586
- // src/commands/pulse.ts
5614
+ // src/commands/board.ts
5587
5615
  import { readFile as readFile12, readdir as readdir4 } from "fs/promises";
5588
5616
  import { join as join15 } from "path";
5589
5617
  import { setTimeout as delay3 } from "timers/promises";
5590
5618
 
5619
+ // src/board/detached.ts
5620
+ import { spawn as nodeSpawn } from "child_process";
5621
+ import { mkdir as mkdir3, open as open2, readFile as readFile8, rm as rm5, stat as stat2, writeFile as writeFile3 } from "fs/promises";
5622
+ import { join as join13 } from "path";
5623
+ import { setTimeout as delay2 } from "timers/promises";
5624
+ var detached = (args, cwd) => {
5625
+ const bin = process.argv[1];
5626
+ if (bin === void 0) return;
5627
+ const child = nodeSpawn(process.execPath, [bin, ...args], {
5628
+ detached: true,
5629
+ stdio: "ignore",
5630
+ cwd,
5631
+ env: { ...process.env, RNESS_NO_DELEGATE: "1" }
5632
+ });
5633
+ child.on("error", () => void 0);
5634
+ child.unref();
5635
+ };
5636
+ var failureFile = () => join13(configDir(), "board.json");
5637
+ var formerFailureFile = () => join13(configDir(), "pulse.json");
5638
+ async function recordFailure(reason3) {
5639
+ try {
5640
+ await mkdir3(configDir(), { recursive: true, mode: 448 });
5641
+ await writeFile3(
5642
+ failureFile(),
5643
+ `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), reason: reason3 })}
5644
+ `
5645
+ );
5646
+ } catch {
5647
+ }
5648
+ }
5649
+ async function takeFailure() {
5650
+ return await takeFrom(failureFile()) ?? await takeFrom(formerFailureFile());
5651
+ }
5652
+ async function takeFrom(file) {
5653
+ try {
5654
+ const data = JSON.parse(await readFile8(file, "utf8"));
5655
+ await rm5(file, { force: true });
5656
+ const reason3 = data !== null && typeof data === "object" ? data["reason"] : void 0;
5657
+ return typeof reason3 === "string" && reason3 !== "" ? reason3 : null;
5658
+ } catch {
5659
+ return null;
5660
+ }
5661
+ }
5662
+ var LOCK_STALE_MS2 = 15 * 6e4;
5663
+ async function oneSyncAtATime(key, work, options = {}) {
5664
+ await mkdir3(configDir(), { recursive: true, mode: 448 });
5665
+ const file = join13(configDir(), `board-${key}.lock`);
5666
+ for (; ; ) {
5667
+ try {
5668
+ const handle = await open2(file, "wx");
5669
+ await handle.close();
5670
+ break;
5671
+ } catch (e) {
5672
+ if (e.code !== "EEXIST") throw e;
5673
+ const age = await stat2(file).then(
5674
+ (s) => Date.now() - s.mtimeMs,
5675
+ () => 0
5676
+ );
5677
+ if (age > LOCK_STALE_MS2) await rm5(file, { force: true });
5678
+ else if (options.wait !== true) return false;
5679
+ else await (options.sleep ?? ((ms) => delay2(ms)))(1e3);
5680
+ }
5681
+ }
5682
+ try {
5683
+ await work();
5684
+ return true;
5685
+ } finally {
5686
+ await rm5(file, { force: true });
5687
+ }
5688
+ }
5689
+
5690
+ // src/board/noted.ts
5691
+ import { mkdir as mkdir4, readFile as readFile9, rename as rename3, rm as rm6, writeFile as writeFile4 } from "fs/promises";
5692
+ import { dirname as dirname5 } from "path";
5693
+ var NAME2 = "rness/noted";
5694
+ function notedFile(rnessDir) {
5695
+ return gitPath(rnessDir, NAME2);
5696
+ }
5697
+ async function readNoted(file) {
5698
+ if (file === null) return /* @__PURE__ */ new Set();
5699
+ try {
5700
+ const data = JSON.parse(await readFile9(file, "utf8"));
5701
+ return new Set(
5702
+ Array.isArray(data) ? data.filter((l) => typeof l === "string") : []
5703
+ );
5704
+ } catch {
5705
+ return /* @__PURE__ */ new Set();
5706
+ }
5707
+ }
5708
+ async function writeNoted(file, noted) {
5709
+ if (file === null) return;
5710
+ try {
5711
+ if (noted.size === 0) {
5712
+ await rm6(file, { force: true });
5713
+ return;
5714
+ }
5715
+ await mkdir4(dirname5(file), { recursive: true });
5716
+ const temporary = `${file}.${process.pid}`;
5717
+ await writeFile4(
5718
+ temporary,
5719
+ `${JSON.stringify([...noted].sort(), null, 2)}
5720
+ `
5721
+ );
5722
+ await rename3(temporary, file);
5723
+ } catch {
5724
+ }
5725
+ }
5726
+ async function onceSaid(rnessDir, board, lines) {
5727
+ const file = await notedFile(rnessDir);
5728
+ const noted = await readNoted(file);
5729
+ const prefix = `${board}: `;
5730
+ const notes = lines.filter((l) => l.startsWith("note "));
5731
+ const now = new Set(notes.map((l) => `${prefix}${l}`));
5732
+ const said = lines.filter(
5733
+ (l) => !l.startsWith("note ") || !noted.has(`${prefix}${l}`)
5734
+ );
5735
+ const next = /* @__PURE__ */ new Set([
5736
+ ...[...noted].filter((l) => !l.startsWith(prefix)),
5737
+ ...now
5738
+ ]);
5739
+ if (next.size !== noted.size || [...next].some((l) => !noted.has(l)))
5740
+ await writeNoted(file, next);
5741
+ return said;
5742
+ }
5743
+
5744
+ // src/board/opened.ts
5745
+ import { mkdir as mkdir5, readFile as readFile10, rename as rename4, rm as rm7, writeFile as writeFile5 } from "fs/promises";
5746
+ import { dirname as dirname6 } from "path";
5747
+ var NAME3 = "rness/opened";
5748
+ function openedFile(rnessDir) {
5749
+ return gitPath(rnessDir, NAME3);
5750
+ }
5751
+ var NUMBER = /^[1-9][0-9]*$/;
5752
+ async function readOpened(file) {
5753
+ const opened = /* @__PURE__ */ new Map();
5754
+ if (file === null) return opened;
5755
+ let data;
5756
+ try {
5757
+ data = JSON.parse(await readFile10(file, "utf8"));
5758
+ } catch {
5759
+ return opened;
5760
+ }
5761
+ if (data === null || typeof data !== "object" || Array.isArray(data))
5762
+ return opened;
5763
+ for (const [key, path] of Object.entries(data))
5764
+ if (NUMBER.test(key) && typeof path === "string" && path !== "")
5765
+ opened.set(Number(key), path);
5766
+ return opened;
5767
+ }
5768
+ async function writeOpened(file, opened) {
5769
+ if (file === null) return;
5770
+ try {
5771
+ if (opened.size === 0) {
5772
+ await rm7(file, { force: true });
5773
+ return;
5774
+ }
5775
+ const entries = [...opened].sort(([a], [b]) => a - b);
5776
+ await mkdir5(dirname6(file), { recursive: true });
5777
+ const temporary = `${file}.${process.pid}`;
5778
+ await writeFile5(
5779
+ temporary,
5780
+ `${JSON.stringify(Object.fromEntries(entries), null, 2)}
5781
+ `
5782
+ );
5783
+ await rename4(temporary, file);
5784
+ } catch {
5785
+ }
5786
+ }
5787
+
5591
5788
  // src/core/board-migration.ts
5592
- import { readFile as readFile8 } from "fs/promises";
5789
+ import { readFile as readFile11 } from "fs/promises";
5593
5790
  import { parseDocument } from "yaml";
5594
5791
 
5595
5792
  // src/core/board-merge.ts
@@ -5667,7 +5864,7 @@ function mergeViews(at, base, ours, theirs, ref, kept) {
5667
5864
  return merged;
5668
5865
  }
5669
5866
  function mergeBoard(name, base, ours, theirs, ref) {
5670
- const at = `projects.${name}`;
5867
+ const at = `boards.${name}`;
5671
5868
  const kept = [];
5672
5869
  const { number, preset: _, ...team } = ours;
5673
5870
  const keys = [
@@ -5699,8 +5896,8 @@ function mergeBoard(name, base, ours, theirs, ref) {
5699
5896
  }
5700
5897
 
5701
5898
  // src/core/preset-board.ts
5702
- import { stat as stat2 } from "fs/promises";
5703
- import { join as join13 } from "path";
5899
+ import { stat as stat3 } from "fs/promises";
5900
+ import { join as join14 } from "path";
5704
5901
  var ORDER = [
5705
5902
  "number",
5706
5903
  "preset",
@@ -5722,30 +5919,30 @@ var ordered = (board) => Object.fromEntries(
5722
5919
  );
5723
5920
  async function exists2(path) {
5724
5921
  try {
5725
- await stat2(path);
5922
+ await stat3(path);
5726
5923
  return true;
5727
5924
  } catch {
5728
5925
  return false;
5729
5926
  }
5730
5927
  }
5731
- async function presetSource(name, rnessDir, number) {
5732
- if (name === PULSE) {
5733
- const preset2 = currentPreset("agent-pulse");
5928
+ async function presetSource(name, rnessDir, number, ref) {
5929
+ const kind = presetFor(name);
5930
+ const preset = ref?.includes("/") === true ? ref : currentPreset(kind);
5931
+ const numbered = number === null ? {} : { number };
5932
+ if (name === PULSE)
5734
5933
  return ordered({
5735
- number,
5736
- preset: preset2,
5737
- ...presetTemplate(preset2, { collection: name })
5934
+ ...numbered,
5935
+ preset,
5936
+ ...presetTemplate(preset, { collection: name })
5738
5937
  });
5739
- }
5740
- const preset = currentPreset("collection");
5741
5938
  const board = {
5742
- number,
5939
+ ...numbered,
5743
5940
  preset,
5744
5941
  ...presetTemplate(preset, { collection: name })
5745
5942
  };
5746
- if (await exists2(join13(rnessDir, name, "README.md")))
5943
+ if (await exists2(join14(rnessDir, name, "README.md")))
5747
5944
  board["readme"] = `${name}/README.md`;
5748
- if (await exists2(join13(rnessDir, name, "updates")))
5945
+ if (await exists2(join14(rnessDir, name, "updates")))
5749
5946
  board["updates"] = `${name}/updates`;
5750
5947
  const shape = await readShape(rnessDir, name);
5751
5948
  if (shape.description !== null) board["description"] = shape.description;
@@ -5777,23 +5974,24 @@ async function presetSource(name, rnessDir, number) {
5777
5974
  return ordered(board);
5778
5975
  }
5779
5976
  async function declaredBoard(name, entry, rnessDir) {
5780
- if (typeof entry !== "number") return entry;
5781
- return parseBoard(name, await presetSource(name, rnessDir, entry));
5977
+ if (typeof entry === "number")
5978
+ return parseBoard(name, await presetSource(name, rnessDir, entry));
5979
+ if (typeof entry === "string")
5980
+ return parseBoard(name, await presetSource(name, rnessDir, null, entry));
5981
+ return entry;
5782
5982
  }
5783
5983
  async function readBoards(rnessDir) {
5784
- let projects;
5984
+ let declared;
5785
5985
  try {
5786
- projects = (await loadManifest(rnessDir)).projects ?? {};
5986
+ declared = (await loadManifest(rnessDir)).boards ?? {};
5787
5987
  } catch {
5788
5988
  return [];
5789
5989
  }
5790
5990
  const boards = [];
5791
- for (const [name, entry] of Object.entries(projects))
5991
+ for (const [name, entry] of Object.entries(declared))
5792
5992
  try {
5793
- boards.push({
5794
- name,
5795
- declaration: await declaredBoard(name, entry, rnessDir)
5796
- });
5993
+ const declaration = await declaredBoard(name, entry, rnessDir);
5994
+ if (declaration.number !== null) boards.push({ name, declaration });
5797
5995
  } catch {
5798
5996
  }
5799
5997
  return boards;
@@ -5814,7 +6012,7 @@ var BUNDLED = {
5814
6012
  var README = (collection) => `${collection}/README.md`;
5815
6013
  async function readmeOf(rnessDir, collection) {
5816
6014
  try {
5817
- return await readFile8(
6015
+ return await readFile11(
5818
6016
  await containedPath(rnessDir, README(collection)),
5819
6017
  "utf8"
5820
6018
  );
@@ -5887,20 +6085,31 @@ function moveInto(name, collection, source, declared) {
5887
6085
  conflicts.push(`fields.${field}`);
5888
6086
  }
5889
6087
  if (conflicts.length > 0)
5890
- return `${README(collection)} declares ${conflicts.join(", ")} otherwise than projects.${name}: keep one, in rness.json`;
6088
+ return `${README(collection)} declares ${conflicts.join(", ")} otherwise than boards.${name}: keep one, in rness.json`;
5891
6089
  source["fields"] = fields2;
5892
6090
  return null;
5893
6091
  }
5894
6092
  async function migrateBoards(rnessDir, manifest, presets = BUNDLED) {
5895
6093
  const migration = {
5896
6094
  changed: false,
5897
- projects: manifest.projects,
6095
+ boards: manifest.boards,
5898
6096
  readmes: /* @__PURE__ */ new Map(),
5899
6097
  lines: []
5900
6098
  };
5901
- if (manifest.projects === null) return migration;
5902
- const projects = {};
5903
- for (const [name, entry] of Object.entries(manifest.projects)) {
6099
+ if (manifest.formerKey !== void 0) {
6100
+ migration.lines.push([
6101
+ "renamed",
6102
+ `${manifest.formerKey} to boards in .rness/rness.json`
6103
+ ]);
6104
+ migration.changed = true;
6105
+ }
6106
+ if (manifest.boards === null) return migration;
6107
+ const boards = {};
6108
+ for (const [name, entry] of Object.entries(manifest.boards)) {
6109
+ if (typeof entry === "string") {
6110
+ boards[name] = entry;
6111
+ continue;
6112
+ }
5904
6113
  let source;
5905
6114
  if (typeof entry === "number") {
5906
6115
  source = await presetSource(name, rnessDir, entry);
@@ -5950,186 +6159,21 @@ async function migrateBoards(rnessDir, manifest, presets = BUNDLED) {
5950
6159
  }
5951
6160
  }
5952
6161
  try {
5953
- projects[name] = typeof entry === "number" || migration.changed ? parseBoard(name, source, presets.knows) : entry;
6162
+ boards[name] = typeof entry === "number" || migration.changed ? parseBoard(name, source, presets.knows) : entry;
5954
6163
  } catch (e) {
5955
6164
  if (!(e instanceof BoardRefused)) throw e;
5956
6165
  migration.lines.push(["refused", e.message]);
5957
- projects[name] = entry;
6166
+ boards[name] = entry;
5958
6167
  }
5959
6168
  }
5960
- migration.projects = projects;
6169
+ migration.boards = boards;
5961
6170
  return migration;
5962
6171
  }
5963
6172
 
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
6173
+ // src/commands/board.ts
6130
6174
  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";
6175
+ var NEEDS_LOGIN = "boards need a GitHub login: run rness login";
6176
+ var NEEDS_SCOPE = "boards need the project scope: run rness login";
6133
6177
  async function fromGithub(work) {
6134
6178
  try {
6135
6179
  return await work;
@@ -6149,7 +6193,7 @@ async function context(opts, wait) {
6149
6193
  const manifest = await loadManifest(ws.rnessDir);
6150
6194
  const provider = await openProvider(manifest, providerOptions(opts, wait));
6151
6195
  if (manifest.org === null)
6152
- throw new Error('the pulse needs an organization: rness.json has no "org"');
6196
+ throw new Error('boards need an organization: rness.json has no "org"');
6153
6197
  return {
6154
6198
  root: ws.root,
6155
6199
  rnessDir: ws.rnessDir,
@@ -6159,7 +6203,7 @@ async function context(opts, wait) {
6159
6203
  };
6160
6204
  }
6161
6205
  var waitSaid = (opts, ui) => rateWait(async (ms) => {
6162
- ui.line("waiting", `${Math.round(ms / 1e3)} s \u2014 GitHub's rate limit`);
6206
+ ui.line("waiting", `${Math.round(ms / 1e3)} s: GitHub's rate limit`);
6163
6207
  await (opts.sleep ?? ((n) => delay3(n)))(ms);
6164
6208
  });
6165
6209
  async function missingAccess(provider) {
@@ -6167,17 +6211,19 @@ async function missingAccess(provider) {
6167
6211
  const scopes = await fromGithub(provider.scopes());
6168
6212
  return scopes?.includes(PROJECT_SCOPE) === true ? null : NEEDS_SCOPE;
6169
6213
  }
6214
+ var isCreated = (d) => d.number !== null;
6170
6215
  var takesAll = (d) => d.declaration.collections === "all";
6171
6216
  var namedCollections = (d) => d.collections === "all" ? [] : Object.keys(d.collections);
6172
6217
  async function declarations(c, ui) {
6173
- const projects = c.manifest.projects ?? {};
6218
+ const boards = c.manifest.boards ?? {};
6174
6219
  for (const r of c.manifest.refused ?? [])
6175
6220
  ui?.line("skipped", `${r.name}: ${r.reason}`);
6176
6221
  const all = [];
6177
- for (const [name, entry] of Object.entries(projects)) {
6222
+ const notCreated = [];
6223
+ for (const [name, entry] of Object.entries(boards)) {
6178
6224
  const declaration = await declaredBoard(name, entry, c.rnessDir);
6179
6225
  const still = [];
6180
- if (typeof entry !== "number")
6226
+ if (typeof entry === "object")
6181
6227
  for (const collection of namedCollections(declaration)) {
6182
6228
  const keys = Object.keys(
6183
6229
  await readmeDeclarations(c.rnessDir, collection)
@@ -6188,23 +6234,32 @@ async function declarations(c, ui) {
6188
6234
  if (still.length > 0) {
6189
6235
  ui?.line(
6190
6236
  "skipped",
6191
- `${name}: ${still.join(", ")}, which rness.json declares now \u2014 rness sync moves it`
6237
+ `${name}: ${still.join(", ")}, which rness.json declares now: rness sync moves it`
6192
6238
  );
6193
6239
  continue;
6194
6240
  }
6195
- all.push({ name, declaration });
6241
+ if (isCreated(declaration)) all.push({ name, declaration });
6242
+ else if (typeof entry !== "number")
6243
+ notCreated.push({ name, entry, declaration });
6196
6244
  }
6197
- return all.sort((a, b) => Number(takesAll(a)) - Number(takesAll(b)));
6245
+ return {
6246
+ created: all.sort((a, b) => Number(takesAll(a)) - Number(takesAll(b))),
6247
+ notCreated
6248
+ };
6249
+ }
6250
+ function noBoard(c) {
6251
+ return new Error(
6252
+ c.manifest.refused === void 0 ? 'no board declared: 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("; ")}`
6253
+ );
6198
6254
  }
6199
6255
  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
- );
6256
+ if (c.manifest.boards === null) throw noBoard(c);
6257
+ const { created } = await declarations(c, ui);
6258
+ if (created.length === 0) return [];
6204
6259
  const missing = await missingAccess(c.provider);
6205
6260
  if (missing !== null) throw new Error(missing);
6206
6261
  const declared = [];
6207
- for (const { name, declaration } of await declarations(c, ui)) {
6262
+ for (const { name, declaration } of created) {
6208
6263
  const board = await fromGithub(c.provider.board(c.org, declaration.number));
6209
6264
  if (board === null)
6210
6265
  throw new Error(`GitHub has no project ${declaration.number} in ${c.org}`);
@@ -6442,7 +6497,7 @@ async function syncBoard(c, board, ui, sayLayout, scope) {
6442
6497
  if (!(e instanceof RateLimitError)) throw e;
6443
6498
  const total = changes + writes.length;
6444
6499
  throw new Error(
6445
- `${e.message}: ${total - made} of ${total} changes not made \u2014 the next rness pulse sync makes them`,
6500
+ `${e.message}: ${total - made} of ${total} changes not made: the next rness board push makes them`,
6446
6501
  { cause: e }
6447
6502
  );
6448
6503
  }
@@ -6470,125 +6525,131 @@ async function syncAll(c, declared, ui, sayLayout) {
6470
6525
  );
6471
6526
  }
6472
6527
  }
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}`
6528
+ async function checkBoardName(rnessDir, name) {
6529
+ if (name === PULSE) return;
6530
+ const tabs = await statusTabs(rnessDir);
6531
+ if (!tabs.some((t) => t.name === name))
6532
+ throw new Error(
6533
+ `"boards.${name}": ${name} is no collection of .rness: none of its documents carries a status`
6534
+ );
6535
+ }
6536
+ var titleOf = (b) => b.declaration.title ?? labelOf(b.name);
6537
+ async function createBoards(c, boards, opts, terminal, ui, wait) {
6538
+ for (const b of boards) await checkBoardName(c.rnessDir, b.name);
6539
+ const detected = providerOf(c.manifest);
6540
+ if (!PROVIDERS[detected].available)
6541
+ throw new Error(
6542
+ `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
6543
  );
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;
6544
+ const names = boards.map((b) => b.name);
6545
+ if (opts.confirmed !== true && opts.yes !== true) {
6546
+ if (!terminal.isTty()) return { c, made: [], left: names };
6547
+ const p = await terminal.prompts();
6548
+ const ok = await p.confirm({
6549
+ message: `Create ${boards.map((b) => `${titleOf(b)} (${b.name})`).join(", ")} on GitHub, in ${c.org}?`
6550
+ });
6551
+ if (p.isCancel(ok) || ok !== true) return { c, made: [], left: names };
6552
+ }
6553
+ const missing = await missingAccess(c.provider);
6554
+ if (missing !== null) {
6555
+ if (opts.yes === true || !terminal.isTty()) throw new Error(missing);
6556
+ const p = await terminal.prompts();
6557
+ const ok = await p.confirm({
6558
+ 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?"
6559
+ });
6560
+ if (p.isCancel(ok) || ok !== true) throw new Error(missing);
6561
+ const code = await loginCommand(
6562
+ {
6563
+ project: true,
6564
+ // The git question is `rness login`'s own, not the board's to ask.
6565
+ setupGit: false,
6566
+ ...opts.githubApi === void 0 ? {} : { githubApi: opts.githubApi }
6567
+ },
6568
+ { terminal, ui, ...opts.cwd === void 0 ? {} : { cwd: opts.cwd } }
6569
+ );
6570
+ if (code !== 0) throw new Error(missing);
6571
+ c = {
6572
+ ...c,
6573
+ provider: await openProvider(c.manifest, providerOptions(opts, wait))
6574
+ };
6575
+ const still = await missingAccess(c.provider);
6576
+ if (still !== null) throw new Error(still);
6577
+ }
6578
+ await fromGithub(c.provider.checkIssues(c.org));
6579
+ const login = (await c.provider.identity())?.login ?? "unknown";
6580
+ ui.line(
6581
+ "checked",
6582
+ `${detected}, logged in as ${login}, scope ${PROJECT_SCOPE}`
6583
+ );
6584
+ const made = [];
6585
+ for (const b of boards) {
6586
+ const source = typeof b.entry === "string" ? await presetSource(b.name, c.rnessDir, null, b.entry) : b.declaration.source;
6587
+ const readme = typeof b.entry === "string" && b.name !== PULSE ? await readmeText(c.rnessDir, b.name) : null;
6588
+ const title = titleOf(b);
6536
6589
  const board = await fromGithub(c.provider.createBoard(c.org, title));
6537
6590
  const manifest = {
6538
6591
  ...c.manifest,
6539
6592
  provider: detected,
6540
- projects: {
6541
- ...c.manifest.projects,
6542
- [name]: parseBoard(name, { ...source, number: board.number })
6593
+ boards: {
6594
+ ...c.manifest.boards,
6595
+ [b.name]: parseBoard(b.name, { number: board.number, ...source })
6543
6596
  }
6544
6597
  };
6545
6598
  await writeManifest(c.rnessDir, manifest);
6546
- if (readme !== null && opts.collection !== void 0) {
6599
+ if (readme !== null) {
6547
6600
  const cleaned = withoutDeclarations(readme);
6548
6601
  if (cleaned !== readme)
6549
- await writeFileAtomic(
6550
- join15(c.rnessDir, opts.collection, "README.md"),
6551
- cleaned
6552
- );
6602
+ await writeFileAtomic(join15(c.rnessDir, b.name, "README.md"), cleaned);
6553
6603
  }
6554
6604
  c = { ...c, manifest };
6555
- ui.line("created", `${title} \u2014 ${board.url}`);
6605
+ made.push(b.name);
6606
+ ui.line("created", `${title} - ${board.url}`);
6607
+ }
6608
+ return { c, made, left: [] };
6609
+ }
6610
+ async function boardPushCommand(opts, deps = {}) {
6611
+ try {
6612
+ const terminal = deps.terminal ?? defaultTerminal;
6613
+ const ui = deps.ui ?? await makeUi(terminal);
6614
+ const wait = waitSaid(opts, ui);
6615
+ let c = await context(opts, wait);
6616
+ if (c.manifest.boards === null) throw noBoard(c);
6617
+ const { notCreated } = await declarations(c, ui);
6618
+ let made = [];
6619
+ let left = [];
6620
+ if (notCreated.length > 0)
6621
+ ({ c, made, left } = await createBoards(
6622
+ c,
6623
+ notCreated,
6624
+ opts,
6625
+ terminal,
6626
+ ui,
6627
+ wait
6628
+ ));
6556
6629
  try {
6630
+ const declared = await declaredBoards(c);
6557
6631
  await syncAll(
6558
6632
  c,
6559
- await declaredBoards(c),
6633
+ declared,
6560
6634
  ui,
6561
- (n) => n === name ? createdByKind : eachAdded
6635
+ (n) => made.includes(n) ? createdByKind : eachAdded
6562
6636
  );
6563
6637
  } 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
- );
6638
+ if (made.length > 0)
6639
+ ui.line(
6640
+ "declared",
6641
+ `${made.join(", ")} in .rness/rness.json - commit it: git -C .rness commit -am "chore: board ${made.join(", ")}"`
6642
+ );
6568
6643
  }
6644
+ if (left.length > 0)
6645
+ throw new Error(
6646
+ `not created: ${left.join(", ")} - rness board push in a terminal, or with --yes, creates ${left.length === 1 ? "it" : "them"}`
6647
+ );
6569
6648
  return 0;
6570
6649
  } catch (e) {
6571
6650
  return reportError(e);
6572
6651
  }
6573
6652
  }
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
6653
  function markFields(d) {
6593
6654
  const fields2 = Object.entries(d.fields);
6594
6655
  const agent = fields2.find(
@@ -6658,29 +6719,7 @@ async function clearOn(c, d, session) {
6658
6719
  }).map((i) => i.id);
6659
6720
  await fromGithub(c.provider.mark(d.board, ids, null, fields2));
6660
6721
  }
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) {
6722
+ async function boardRunCommand(opts) {
6684
6723
  const failures = [];
6685
6724
  try {
6686
6725
  await run(opts, failures);
@@ -6703,7 +6742,7 @@ async function run(opts, failures) {
6703
6742
  if (event === "session-end") {
6704
6743
  for (const d of acting("journal-summary"))
6705
6744
  try {
6706
- const { postSummary } = await import("./summary-UCMFESSA.js");
6745
+ const { postSummary } = await import("./summary-5DVUIOJM.js");
6707
6746
  await postSummary({
6708
6747
  c,
6709
6748
  board: d,
@@ -6764,6 +6803,33 @@ async function run(opts, failures) {
6764
6803
  )
6765
6804
  );
6766
6805
  }
6806
+ async function pulseCreateCommand(collection, opts, deps = {}) {
6807
+ try {
6808
+ const name = collection ?? PULSE;
6809
+ const ws = await findWorkspace(opts.cwd ?? process.cwd());
6810
+ const manifest = await loadManifest(ws.rnessDir);
6811
+ if (manifest.org === null)
6812
+ throw new Error('boards need an organization: rness.json has no "org"');
6813
+ const refused = manifest.refused?.find((r) => r.name === name);
6814
+ if (refused !== void 0)
6815
+ throw new Error(`already declared, and refused: ${refused.reason}`);
6816
+ let said = "rness pulse create is now rness board push";
6817
+ if (manifest.boards?.[name] === void 0) {
6818
+ await checkBoardName(ws.rnessDir, name);
6819
+ const preset = presetFor(name);
6820
+ await writeManifest(ws.rnessDir, {
6821
+ ...manifest,
6822
+ boards: { ...manifest.boards, [name]: preset }
6823
+ });
6824
+ said = `boards are declared in rness.json now: added "${name}": "${preset}", then rness board push`;
6825
+ }
6826
+ process.stderr.write(`${said}
6827
+ `);
6828
+ return await boardPushCommand({ ...opts, confirmed: true }, deps);
6829
+ } catch (e) {
6830
+ return reportError(e);
6831
+ }
6832
+ }
6767
6833
 
6768
6834
  export {
6769
6835
  exists,
@@ -6787,6 +6853,7 @@ export {
6787
6853
  presetTemplate,
6788
6854
  BoardRefused,
6789
6855
  contractOf,
6856
+ PULSE,
6790
6857
  parseBoard,
6791
6858
  clone,
6792
6859
  isClean,
@@ -6809,7 +6876,6 @@ export {
6809
6876
  NAME_RULE,
6810
6877
  ORG_NAME,
6811
6878
  workspaceName,
6812
- PULSE,
6813
6879
  loadManifest,
6814
6880
  parseManifest,
6815
6881
  writeManifest,
@@ -6871,12 +6937,12 @@ export {
6871
6937
  readBoards,
6872
6938
  readmeDeclarations,
6873
6939
  migrateBoards,
6940
+ detached,
6941
+ takeFailure,
6874
6942
  openInBrowser,
6875
6943
  canOpenBrowser,
6876
6944
  removeGitSetup,
6877
6945
  loginCommand,
6878
- detached,
6879
- takeFailure,
6880
6946
  notesPosted,
6881
6947
  notePosted,
6882
6948
  issuesNoted,
@@ -6889,9 +6955,8 @@ export {
6889
6955
  fromGithub,
6890
6956
  context,
6891
6957
  declaredBoards,
6892
- pulseCreateCommand,
6893
- pulseSyncCommand,
6894
- pulseMarkCommand,
6895
- pulseRunCommand
6958
+ boardPushCommand,
6959
+ boardRunCommand,
6960
+ pulseCreateCommand
6896
6961
  };
6897
- //# sourceMappingURL=chunk-D3D2WCGW.js.map
6962
+ //# sourceMappingURL=chunk-OUZQGG6V.js.map