@rness/cli 0.10.0 → 0.11.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.
@@ -1198,6 +1198,12 @@ function badge(text, tone = "intro", stream = process.stdout) {
1198
1198
  });
1199
1199
  return painted === padded ? text : painted;
1200
1200
  }
1201
+ function inverse(text, stream = process.stdout) {
1202
+ const plain = `[${text.slice(1, -1)}]`;
1203
+ if (noColor()) return plain;
1204
+ const painted = styleText("inverse", text, { stream, validateStream: true });
1205
+ return painted === text ? plain : painted;
1206
+ }
1201
1207
  function grey(text, stream = process.stdout) {
1202
1208
  if (noColor()) return text;
1203
1209
  return styleText("gray", text, { stream, validateStream: true });
@@ -1213,8 +1219,8 @@ function warn(text) {
1213
1219
  var WORD = [" \u2588\u2580\u2588 \u2588\u2584 \u2588 \u2588\u2580\u2580 \u2588\u2580\u2580 \u2588\u2580\u2580", " \u2588\u2580\u2584 \u2588 \u2580\u2588 \u2588\u2588\u2584 \u2584\u2584\u2588 \u2584\u2584\u2588"];
1214
1220
  var FROM = [63, 185, 80];
1215
1221
  var TO = [57, 197, 207];
1216
- function gradient(line2) {
1217
- const chars = [...line2];
1222
+ function gradient(line3) {
1223
+ const chars = [...line3];
1218
1224
  const last = Math.max(1, chars.length - 1);
1219
1225
  const painted = chars.map((ch, i) => {
1220
1226
  if (ch === " ") return ch;
@@ -1231,7 +1237,7 @@ function banner(version, stream = process.stdout) {
1231
1237
  const coloured = paint("done", "x", stream) !== "x";
1232
1238
  const trueColour = coloured && typeof stream.hasColors === "function" && stream.hasColors(2 ** 24);
1233
1239
  const word = WORD.map(
1234
- (line2) => trueColour ? gradient(line2) : paint("done", line2, stream)
1240
+ (line3) => trueColour ? gradient(line3) : paint("done", line3, stream)
1235
1241
  );
1236
1242
  return [
1237
1243
  "",
@@ -1405,7 +1411,7 @@ function reportError(e) {
1405
1411
  }
1406
1412
 
1407
1413
  // src/version.ts
1408
- var VERSION = "0.10.0";
1414
+ var VERSION = "0.11.0";
1409
1415
 
1410
1416
  // src/core/block.ts
1411
1417
  import { createHash } from "crypto";
@@ -1457,8 +1463,8 @@ ${rules.join("\n\n")}`;
1457
1463
  const text = [BEGIN, header(scope, hash), body, END].join("\n");
1458
1464
  return { text, hash, bytes: Buffer.byteLength(text, "utf8") };
1459
1465
  }
1460
- function parseHeader(line2) {
1461
- const m = HEADER.exec(line2.trimEnd());
1466
+ function parseHeader(line3) {
1467
+ const m = HEADER.exec(line3.trimEnd());
1462
1468
  if (m === null) return null;
1463
1469
  return { version: m[1] ?? null, scope: m[2] ?? "", hash: m[3] ?? "" };
1464
1470
  }
@@ -1629,8 +1635,8 @@ import { join as join8 } from "path";
1629
1635
  function findBlock(lines) {
1630
1636
  const begins = [];
1631
1637
  const ends = [];
1632
- lines.forEach((line2, i) => {
1633
- const l = line2.trimEnd();
1638
+ lines.forEach((line3, i) => {
1639
+ const l = line3.trimEnd();
1634
1640
  if (l === BEGIN) begins.push(i);
1635
1641
  if (l === END) ends.push(i);
1636
1642
  });
@@ -1746,6 +1752,48 @@ function hooks(rness) {
1746
1752
  }
1747
1753
  ];
1748
1754
  }
1755
+ var PLUGIN = ".claude/skills/rness";
1756
+ var PLUGIN_JSON = `${JSON.stringify(
1757
+ {
1758
+ name: "rness",
1759
+ description: "The rness workspace in Claude Code: /rness:status."
1760
+ },
1761
+ null,
1762
+ 2
1763
+ )}
1764
+ `;
1765
+ function statusSkill(rness) {
1766
+ const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status --cwd "\${CLAUDE_PROJECT_DIR}"`;
1767
+ return `---
1768
+ name: status
1769
+ description: The status of every decision, specification, plan and other tracked document of the rness workspace, one table per directory. Read-only.
1770
+ argument-hint: '[tab]'
1771
+ disable-model-invocation: true
1772
+ allowed-tools: Bash(${command})
1773
+ ---
1774
+
1775
+ !\`${command}\`
1776
+
1777
+ Show the output above to the user as it is: the heading and the tables,
1778
+ nothing added, nothing summarised, no other tool. Arguments: \`$ARGUMENTS\`.
1779
+ When they name a tab, show only that tab's section. If the output says the
1780
+ module cannot be found, say instead that ${rness} is not installed next to
1781
+ this repository.
1782
+
1783
+ End with this line: _For the view with tabs and scrolling: Ctrl+Z, then
1784
+ \`npx @rness/cli status\` (q to close), then \`fg\`._
1785
+ `;
1786
+ }
1787
+ function plugin(rness, at) {
1788
+ return [
1789
+ { file: `${PLUGIN}/.claude-plugin/plugin.json`, at, content: PLUGIN_JSON },
1790
+ {
1791
+ file: `${PLUGIN}/skills/status/SKILL.md`,
1792
+ at,
1793
+ content: statusSkill(rness)
1794
+ }
1795
+ ];
1796
+ }
1749
1797
  var TARGETS = {
1750
1798
  claude: {
1751
1799
  name: "claude",
@@ -1786,13 +1834,15 @@ var TARGETS = {
1786
1834
  }
1787
1835
  ]
1788
1836
  },
1837
+ ...plugin("../../.rness", "clones"),
1789
1838
  // A session opened at the workspace root reads this file, one opened
1790
1839
  // in a clone does not (spec 0011 §1): the hooks only, `.rness` inside.
1791
1840
  {
1792
1841
  file: ".claude/settings.json",
1793
1842
  at: "root",
1794
1843
  guarantees: hooks(".rness")
1795
- }
1844
+ },
1845
+ ...plugin(".rness", "root")
1796
1846
  ]
1797
1847
  }
1798
1848
  };
@@ -1907,6 +1957,10 @@ function describeGuarantee(g) {
1907
1957
  }
1908
1958
 
1909
1959
  // src/core/agent-targets.ts
1960
+ function wholeDiff(existing, content) {
1961
+ if (existing === content) return null;
1962
+ return existing === null ? "missing" : "differs from what rness writes";
1963
+ }
1910
1964
  async function filesOf(root, manifest, targets) {
1911
1965
  const files = [];
1912
1966
  if (!await exists(join9(root, "org"))) return files;
@@ -1914,7 +1968,7 @@ async function filesOf(root, manifest, targets) {
1914
1968
  for (const owned of target.files)
1915
1969
  if (owned.at === "root")
1916
1970
  files.push({
1917
- guarantees: owned.guarantees,
1971
+ ...owned,
1918
1972
  file: join9(root, ...owned.file.split("/")),
1919
1973
  label: owned.file
1920
1974
  });
@@ -1924,7 +1978,7 @@ async function filesOf(root, manifest, targets) {
1924
1978
  for (const owned of target.files)
1925
1979
  if (owned.at === "clones")
1926
1980
  files.push({
1927
- guarantees: owned.guarantees,
1981
+ ...owned,
1928
1982
  file: join9(root, "org", repo, ...owned.file.split("/")),
1929
1983
  label: `org/${repo}/${owned.file}`
1930
1984
  });
@@ -1947,12 +2001,23 @@ function writtenFiles(manifest) {
1947
2001
  }
1948
2002
  async function agentTargets(root, manifest, opts) {
1949
2003
  const outcomes = [];
1950
- for (const { guarantees, file, label } of await filesOf(
1951
- root,
1952
- manifest,
1953
- declared(manifest)
1954
- )) {
2004
+ for (const owned of await filesOf(root, manifest, declared(manifest))) {
2005
+ const { file, label } = owned;
1955
2006
  const existing = await readOrNull(file);
2007
+ if ("content" in owned) {
2008
+ const diff = wholeDiff(existing, owned.content);
2009
+ if (diff === null)
2010
+ outcomes.push({ label, status: "unchanged", detail: null });
2011
+ else if (opts.check)
2012
+ outcomes.push({ label, status: "stale", detail: diff });
2013
+ else {
2014
+ await mkdir2(dirname4(file), { recursive: true });
2015
+ await writeFileAtomic(file, owned.content);
2016
+ outcomes.push({ label, status: "updated", detail: null });
2017
+ }
2018
+ continue;
2019
+ }
2020
+ const { guarantees } = owned;
1956
2021
  if (opts.check) {
1957
2022
  const missing = missingGuarantees(existing, guarantees);
1958
2023
  if ("invalid" in missing)
@@ -1988,15 +2053,16 @@ async function leftoverTargets(root, manifest) {
1988
2053
  (t) => !agents.includes(t.name)
1989
2054
  );
1990
2055
  const leftovers = [];
1991
- for (const { guarantees, file, label } of await filesOf(
1992
- root,
1993
- manifest,
1994
- undeclared
1995
- )) {
1996
- const existing = await readOrNull(file);
2056
+ for (const owned of await filesOf(root, manifest, undeclared)) {
2057
+ const existing = await readOrNull(owned.file);
1997
2058
  if (existing === null) continue;
1998
- const missing = missingGuarantees(existing, guarantees);
1999
- if (!("invalid" in missing) && missing.length === 0) leftovers.push(label);
2059
+ if ("content" in owned) {
2060
+ if (existing === owned.content) leftovers.push(owned.label);
2061
+ continue;
2062
+ }
2063
+ const missing = missingGuarantees(existing, owned.guarantees);
2064
+ if (!("invalid" in missing) && missing.length === 0)
2065
+ leftovers.push(owned.label);
2000
2066
  }
2001
2067
  return leftovers;
2002
2068
  }
@@ -2106,8 +2172,8 @@ var plainUi = {
2106
2172
  { label: options.doing, animate: options.git !== true },
2107
2173
  work
2108
2174
  );
2109
- const line2 = options.quiet === true ? null : done?.(result) ?? null;
2110
- if (line2 !== null) this.line(line2[0], line2[1]);
2175
+ const line3 = options.quiet === true ? null : done?.(result) ?? null;
2176
+ if (line3 !== null) this.line(line3[0], line3[1]);
2111
2177
  return result;
2112
2178
  }
2113
2179
  };
@@ -2140,7 +2206,7 @@ function sessionUi(p) {
2140
2206
  p.log.warn(text);
2141
2207
  },
2142
2208
  note: (title, lines) => {
2143
- p.note(lines.join("\n"), title, { format: (line2) => grey(line2) });
2209
+ p.note(lines.join("\n"), title, { format: (line3) => grey(line3) });
2144
2210
  },
2145
2211
  hint: (text) => {
2146
2212
  p.log.message(paint("idle", text));
@@ -2154,20 +2220,20 @@ function sessionUi(p) {
2154
2220
  async step(options, work, done) {
2155
2221
  const label = options.sentence ?? tidy(options.doing);
2156
2222
  const finish2 = (result) => done?.(result) ?? null;
2157
- const label_ = (line2) => line2[2] ?? `${line2[1]} ${line2[0]}`;
2223
+ const label_ = (line3) => line3[2] ?? `${line3[1]} ${line3[0]}`;
2158
2224
  if (options.git === true && !this.gitIsSilent) {
2159
2225
  p.log.message(paint("idle", `${label}\u2026`));
2160
2226
  const result = await work();
2161
- const line2 = finish2(result);
2162
- if (line2 !== null) say(line2[0], label_(line2));
2227
+ const line3 = finish2(result);
2228
+ if (line3 !== null) say(line3[0], label_(line3));
2163
2229
  return result;
2164
2230
  }
2165
2231
  const spin = p.spinner();
2166
2232
  spin.start(label);
2167
2233
  try {
2168
2234
  const result = await work();
2169
- const line2 = finish2(result);
2170
- spin.stop(line2 === null ? label : label_(line2));
2235
+ const line3 = finish2(result);
2236
+ spin.stop(line3 === null ? label : label_(line3));
2171
2237
  return result;
2172
2238
  } catch (e) {
2173
2239
  spin.error(`${label} \u2014 failed`);
@@ -3185,11 +3251,11 @@ function summariseSync(stdout) {
3185
3251
  const blocks = /* @__PURE__ */ new Map();
3186
3252
  const agentFiles = /* @__PURE__ */ new Map();
3187
3253
  const others = [];
3188
- for (const line2 of stdout.split("\n")) {
3189
- if (line2.trim() === "") continue;
3190
- const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line2);
3254
+ for (const line3 of stdout.split("\n")) {
3255
+ if (line3.trim() === "") continue;
3256
+ const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line3);
3191
3257
  if (m === null) {
3192
- others.push(line2);
3258
+ others.push(line3);
3193
3259
  continue;
3194
3260
  }
3195
3261
  const [, verb = "", path = ""] = m;
@@ -3419,8 +3485,8 @@ async function mergeScaffold(rnessDir, commit, base) {
3419
3485
  "--name-status",
3420
3486
  "HEAD"
3421
3487
  ]);
3422
- for (const line2 of status2.split("\n")) {
3423
- const [letter = "", path = ""] = line2.split(" ");
3488
+ for (const line3 of status2.split("\n")) {
3489
+ const [letter = "", path = ""] = line3.split(" ");
3424
3490
  if (path === "" || conflicts.includes(path)) continue;
3425
3491
  if (letter === "A") changes.push(["added", path]);
3426
3492
  else if (letter === "D") changes.push(["removed", path]);
@@ -3566,8 +3632,8 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
3566
3632
  return 1;
3567
3633
  }
3568
3634
  const outcome = syncOutcome(result, { sshWorks: sshWorks === true });
3569
- for (const line2 of outcome.hints) ui.hint(line2);
3570
- for (const line2 of outcome.errors) ui.error(line2);
3635
+ for (const line3 of outcome.hints) ui.hint(line3);
3636
+ for (const line3 of outcome.errors) ui.error(line3);
3571
3637
  return outcome.code;
3572
3638
  }
3573
3639
 
@@ -3672,7 +3738,7 @@ async function createBlank(input) {
3672
3738
  `Workspace \`${name}\` is ready in ${shown}/.`,
3673
3739
  "",
3674
3740
  "Next:",
3675
- ...next.map((line2) => ` ${line2}`),
3741
+ ...next.map((line3) => ` ${line3}`),
3676
3742
  ""
3677
3743
  ].join("\n")
3678
3744
  );
@@ -4315,7 +4381,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
4315
4381
  `Workspace for \`${org}\` is ready in ${shown}/.`,
4316
4382
  "",
4317
4383
  "Next:",
4318
- ...next.map((line2) => ` ${line2}`),
4384
+ ...next.map((line3) => ` ${line3}`),
4319
4385
  ""
4320
4386
  ].join("\n")
4321
4387
  );
@@ -4592,7 +4658,7 @@ function finish(ui, cwd, rnessDir, label, target, ending) {
4592
4658
  "",
4593
4659
  status("upgraded", `${label} to @rness/cli ${target}`),
4594
4660
  "",
4595
- ...next.length > 0 ? ["Next:", ...next.map((line2) => ` ${line2}`), ""] : []
4661
+ ...next.length > 0 ? ["Next:", ...next.map((line3) => ` ${line3}`), ""] : []
4596
4662
  ].join("\n")
4597
4663
  );
4598
4664
  return ending.commit === "refused" ? 1 : 0;
@@ -4813,8 +4879,8 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps =
4813
4879
  ui.error(
4814
4880
  `the commit of ${label} was refused; everything is staged \u2014 deal with what git said, then commit`
4815
4881
  );
4816
- for (const line2 of committed.output.split("\n"))
4817
- if (line2.trim() !== "") ui.hint(line2, "stderr");
4882
+ for (const line3 of committed.output.split("\n"))
4883
+ if (line3.trim() !== "") ui.hint(line3, "stderr");
4818
4884
  }
4819
4885
  return finish(ui, cwd, ws.rnessDir, label, target, {
4820
4886
  commit: committed.kind,
@@ -4923,9 +4989,9 @@ async function readStdin() {
4923
4989
  async function gitCredentialCommand(operation, input) {
4924
4990
  if (operation !== "get") return 0;
4925
4991
  const fields = /* @__PURE__ */ new Map();
4926
- for (const line2 of (input ?? await readStdin()).split("\n")) {
4927
- const eq = line2.indexOf("=");
4928
- if (eq > 0) fields.set(line2.slice(0, eq), line2.slice(eq + 1).trimEnd());
4992
+ for (const line3 of (input ?? await readStdin()).split("\n")) {
4993
+ const eq = line3.indexOf("=");
4994
+ if (eq > 0) fields.set(line3.slice(0, eq), line3.slice(eq + 1).trimEnd());
4929
4995
  }
4930
4996
  if (fields.get("protocol") !== "https" || fields.get("host") !== "github.com")
4931
4997
  return 0;
@@ -4939,11 +5005,24 @@ password=${resolved.token}
4939
5005
 
4940
5006
  // src/commands/hook.ts
4941
5007
  import { realpath as realpath4 } from "fs/promises";
4942
- import { isAbsolute as isAbsolute2, posix as posix5, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
5008
+ import { isAbsolute as isAbsolute2, posix as posix6, relative as relative7, resolve as resolve7, sep as sep5 } from "path";
4943
5009
 
4944
5010
  // src/mcp/tools.ts
4945
5011
  import { readFile as readFile10, realpath as realpath3, stat as stat4 } from "fs/promises";
4946
- import { isAbsolute, join as join23, posix as posix4, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
5012
+ import { isAbsolute, join as join23, posix as posix5, relative as relative6, resolve as resolve6, sep as sep4 } from "path";
5013
+
5014
+ // src/core/documents.ts
5015
+ import { posix as posix4 } from "path";
5016
+ function documentNumber(rel) {
5017
+ return /^(\d{4})-/.exec(posix4.basename(rel))?.[1] ?? null;
5018
+ }
5019
+ function documentTitle(heading, number) {
5020
+ if (number === null) return heading;
5021
+ const rest = new RegExp(`^${number}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
5022
+ return rest ?? heading;
5023
+ }
5024
+
5025
+ // src/mcp/tools.ts
4947
5026
  var LABELS = {
4948
5027
  standards: "Standards",
4949
5028
  adr: "Decisions",
@@ -4968,11 +5047,6 @@ async function workspaceAt(cwd) {
4968
5047
  const ws = await findWorkspace(cwd);
4969
5048
  return { ws, manifest: await loadManifest(ws.rnessDir) };
4970
5049
  }
4971
- function titleOf(heading, id) {
4972
- if (id === null) return heading;
4973
- const rest = new RegExp(`^${id}\\s*[\u2014\u2013:-]\\s*(.+)$`).exec(heading)?.[1];
4974
- return rest ?? heading;
4975
- }
4976
5050
  async function documents(rnessDir, only) {
4977
5051
  const docs = [];
4978
5052
  for (const collection of COLLECTIONS) {
@@ -4981,14 +5055,14 @@ async function documents(rnessDir, only) {
4981
5055
  const path = `${collection}/${item.rel}`;
4982
5056
  if (path === TEMPLATE) continue;
4983
5057
  const status2 = item.fields?.["status"];
4984
- const id = /^(\d{4})-/.exec(posix4.basename(item.rel))?.[1] ?? null;
5058
+ const id = documentNumber(item.rel);
4985
5059
  docs.push({
4986
5060
  collection,
4987
5061
  path,
4988
5062
  scope: ownerScope(item.rel),
4989
5063
  id,
4990
5064
  status: typeof status2 === "string" ? status2 : null,
4991
- title: titleOf(item.title ?? posix4.basename(item.rel, ".md"), id),
5065
+ title: documentTitle(item.title ?? posix5.basename(item.rel, ".md"), id),
4992
5066
  text: item.body
4993
5067
  });
4994
5068
  }
@@ -5018,7 +5092,7 @@ async function context(cwd, args) {
5018
5092
  } else
5019
5093
  scope = resolveScope(
5020
5094
  manifest,
5021
- relative6(ws.root, cwd).split(sep4).join(posix4.sep)
5095
+ relative6(ws.root, cwd).split(sep4).join(posix5.sep)
5022
5096
  );
5023
5097
  const summary = await scopeSummary(ws, manifest, scope);
5024
5098
  return {
@@ -5231,7 +5305,7 @@ function cwdOf(input, env) {
5231
5305
  if (typeof cwd === "string" && cwd !== "") return cwd;
5232
5306
  return env["CLAUDE_PROJECT_DIR"] ?? process.cwd();
5233
5307
  }
5234
- var toPosix2 = (p) => p.split(sep5).join(posix5.sep);
5308
+ var toPosix2 = (p) => p.split(sep5).join(posix6.sep);
5235
5309
  function count(n, [one, many]) {
5236
5310
  return `${n} ${n === 1 ? one : many}`;
5237
5311
  }
@@ -5383,10 +5457,10 @@ function errorLine(id, code, message2, data) {
5383
5457
  function createProtocol(opts) {
5384
5458
  const serverInfo = { name: opts.name, version: opts.version };
5385
5459
  return {
5386
- async handle(line2) {
5460
+ async handle(line3) {
5387
5461
  let message2;
5388
5462
  try {
5389
- message2 = JSON.parse(line2);
5463
+ message2 = JSON.parse(line3);
5390
5464
  } catch {
5391
5465
  return errorLine(null, -32700, "Parse error");
5392
5466
  }
@@ -5495,15 +5569,349 @@ async function mcpCommand(opts, io = {
5495
5569
  tools: mcpTools(opts.cwd ?? process.cwd())
5496
5570
  });
5497
5571
  const lines = createInterface({ input: io.input, crlfDelay: Infinity });
5498
- for await (const line2 of lines) {
5499
- if (line2.trim() === "") continue;
5500
- const answer = await protocol.handle(line2);
5572
+ for await (const line3 of lines) {
5573
+ if (line3.trim() === "") continue;
5574
+ const answer = await protocol.handle(line3);
5501
5575
  if (answer !== null) io.output.write(`${answer}
5502
5576
  `);
5503
5577
  }
5504
5578
  return 0;
5505
5579
  }
5506
5580
 
5581
+ // src/commands/status.ts
5582
+ import { emitKeypressEvents } from "readline";
5583
+
5584
+ // src/core/status.ts
5585
+ import { readdir as readdir3 } from "fs/promises";
5586
+ import { join as join24, posix as posix7 } from "path";
5587
+ var FIXED = [
5588
+ ["adr", "ADR"],
5589
+ ["specs", "Specs"],
5590
+ ["plans", "Plans"]
5591
+ ];
5592
+ var NOT_DISCOVERED = /* @__PURE__ */ new Set([
5593
+ ...FIXED.map(([name]) => name),
5594
+ "standards",
5595
+ "skills",
5596
+ "node_modules"
5597
+ ]);
5598
+ var TEMPLATE2 = "adr/0000-template.md";
5599
+ function statusOf(item) {
5600
+ const status2 = item.fields?.["status"];
5601
+ return typeof status2 === "string" && status2 !== "" ? status2 : null;
5602
+ }
5603
+ function rowOf(dir, item) {
5604
+ const base = posix7.basename(item.rel, ".md");
5605
+ const date = /^(\d{4}-\d{2}-\d{2})-/.exec(base)?.[1] ?? null;
5606
+ const number = date === null ? documentNumber(item.rel) : null;
5607
+ return {
5608
+ id: date ?? number ?? base,
5609
+ title: documentTitle(item.title ?? base, number),
5610
+ status: statusOf(item),
5611
+ path: `${dir}/${item.rel}`
5612
+ };
5613
+ }
5614
+ function newestFirst(a, b) {
5615
+ const [x, y] = [posix7.basename(a.path), posix7.basename(b.path)];
5616
+ if (x !== y) return x < y ? 1 : -1;
5617
+ return a.path < b.path ? 1 : a.path > b.path ? -1 : 0;
5618
+ }
5619
+ async function statusTabs(rnessDir) {
5620
+ const tabs = [];
5621
+ for (const [name, label] of FIXED) {
5622
+ const rows = (await collectMarkdown(join24(rnessDir, name))).map((item) => rowOf(name, item)).filter((row) => row.path !== TEMPLATE2);
5623
+ tabs.push({ name, label, rows: rows.sort(newestFirst) });
5624
+ }
5625
+ const entries = await readdir3(rnessDir, { withFileTypes: true }).catch(
5626
+ () => []
5627
+ );
5628
+ const discovered = entries.filter(
5629
+ (e) => e.isDirectory() && !e.name.startsWith(".") && !NOT_DISCOVERED.has(e.name)
5630
+ ).map((e) => e.name).sort();
5631
+ for (const name of discovered) {
5632
+ const rows = (await collectMarkdown(join24(rnessDir, name))).filter((item) => statusOf(item) !== null).map((item) => rowOf(name, item));
5633
+ if (rows.length > 0)
5634
+ tabs.push({
5635
+ name,
5636
+ label: `${name.charAt(0).toUpperCase()}${name.slice(1)}`,
5637
+ rows: rows.sort(newestFirst)
5638
+ });
5639
+ }
5640
+ return tabs;
5641
+ }
5642
+ function findTab(tabs, wanted) {
5643
+ const w = wanted.toLowerCase();
5644
+ return tabs.find(
5645
+ (tab) => tab.name.toLowerCase() === w || tab.label.toLowerCase() === w
5646
+ );
5647
+ }
5648
+ var DONE2 = /* @__PURE__ */ new Set([
5649
+ "accepted",
5650
+ "implemented",
5651
+ "completed",
5652
+ "published"
5653
+ ]);
5654
+ var DROPPED = /* @__PURE__ */ new Set([
5655
+ "rejected",
5656
+ "superseded",
5657
+ "abandoned"
5658
+ ]);
5659
+ function statusTone(status2) {
5660
+ if (status2 === null) return "missing";
5661
+ const s = status2.toLowerCase();
5662
+ if (DONE2.has(s)) return "done";
5663
+ if (DROPPED.has(s)) return "dropped";
5664
+ return "active";
5665
+ }
5666
+ var cell = (text) => text.replaceAll("|", "\\|");
5667
+ function statusMarkdown(workspace, tabs) {
5668
+ const out = [`${workspace} \xB7 status`, ""];
5669
+ for (const tab of tabs) {
5670
+ out.push(`## ${tab.label} (${tab.rows.length})`, "");
5671
+ if (tab.rows.length === 0) {
5672
+ out.push("_Nothing yet._", "");
5673
+ continue;
5674
+ }
5675
+ out.push("| Id | Title | Status |", "| --- | --- | --- |");
5676
+ for (const row of tab.rows)
5677
+ out.push(
5678
+ `| ${cell(row.id)} | ${cell(row.title)} | ${cell(row.status ?? "?")} |`
5679
+ );
5680
+ out.push("");
5681
+ }
5682
+ return out.join("\n");
5683
+ }
5684
+
5685
+ // src/core/status-view.ts
5686
+ var FRAME = 5;
5687
+ function pageSize(height) {
5688
+ return Math.max(1, height - FRAME);
5689
+ }
5690
+ function lastTop(tab, page) {
5691
+ return Math.max(0, (tab?.rows.length ?? 0) - page);
5692
+ }
5693
+ function press(view, key, tabs, page) {
5694
+ const n = tabs.length;
5695
+ if (key === "right" || key === "tab")
5696
+ return { tab: (view.tab + 1) % n, top: 0 };
5697
+ if (key === "left" || key === "backtab")
5698
+ return { tab: (view.tab - 1 + n) % n, top: 0 };
5699
+ const moved = {
5700
+ up: view.top - 1,
5701
+ down: view.top + 1,
5702
+ pageup: view.top - page,
5703
+ pagedown: view.top + page,
5704
+ home: 0,
5705
+ end: Number.MAX_SAFE_INTEGER
5706
+ };
5707
+ const top = moved[key] ?? view.top;
5708
+ return {
5709
+ tab: view.tab,
5710
+ top: Math.min(Math.max(0, top), lastTop(tabs[view.tab], page))
5711
+ };
5712
+ }
5713
+ function clampView(view, tabs, page) {
5714
+ return {
5715
+ tab: view.tab,
5716
+ top: Math.min(view.top, lastTop(tabs[view.tab], page))
5717
+ };
5718
+ }
5719
+ var width = (text) => [...text].length;
5720
+ function fit(text, n) {
5721
+ if (n <= 0) return "";
5722
+ const chars = [...text];
5723
+ if (chars.length <= n) return text + " ".repeat(n - chars.length);
5724
+ return `${chars.slice(0, n - 1).join("").trimEnd()}\u2026`.padEnd(n);
5725
+ }
5726
+ function line2(segments, columns) {
5727
+ let left = columns;
5728
+ let out = "";
5729
+ for (const [text, style] of segments) {
5730
+ if (left <= 0) break;
5731
+ const chars = [...text];
5732
+ const kept = chars.length <= left ? text : chars.slice(0, left).join("");
5733
+ left -= width(kept);
5734
+ out += style === void 0 ? kept : style(kept);
5735
+ }
5736
+ return out;
5737
+ }
5738
+ function tabBar(tabs, active, look) {
5739
+ const segments = [[" "]];
5740
+ tabs.forEach((tab, i) => {
5741
+ if (i > 0) segments.push([" "]);
5742
+ const label = ` ${tab.label} (${tab.rows.length}) `;
5743
+ segments.push(i === active ? [label, look.inverse] : [label]);
5744
+ });
5745
+ return segments;
5746
+ }
5747
+ function rowLines(tab, top, page, columns, look) {
5748
+ if (tab === void 0 || tab.rows.length === 0)
5749
+ return [[[" Nothing here yet.", look.dim]]];
5750
+ const idWidth = Math.max(...tab.rows.map((r) => width(r.id)));
5751
+ const statusWidth = Math.max(...tab.rows.map((r) => width(r.status ?? "?")));
5752
+ const titleWidth = Math.max(
5753
+ 1,
5754
+ columns - 1 - (2 + idWidth + 2 + 2 + statusWidth)
5755
+ );
5756
+ return tab.rows.slice(top, top + page).map((row) => [
5757
+ [" "],
5758
+ [row.id.padEnd(idWidth)],
5759
+ [" "],
5760
+ [fit(row.title, titleWidth)],
5761
+ [" "],
5762
+ [
5763
+ (row.status ?? "?").padEnd(statusWidth),
5764
+ (text) => look.tone(statusTone(row.status), text)
5765
+ ]
5766
+ ]);
5767
+ }
5768
+ function footer(above, below) {
5769
+ const parts = [
5770
+ ...above > 0 ? [`\u2191 ${above} above`] : [],
5771
+ ...below > 0 ? [`\u2193 ${below} more`] : [],
5772
+ "\u2190/\u2192 tab \xB7 \u2191/\u2193 scroll \xB7 q close"
5773
+ ];
5774
+ return ` ${parts.join(" \xB7 ")}`;
5775
+ }
5776
+ function renderView(view, tabs, workspace, columns, height, look) {
5777
+ const page = pageSize(height);
5778
+ const tab = tabs[view.tab];
5779
+ const rule = [[` ${"\u2500".repeat(Math.max(0, columns - 2))}`]];
5780
+ const rows = rowLines(tab, view.top, page, columns, look);
5781
+ const shown = Math.min(page, rows.length);
5782
+ const total = tab?.rows.length ?? 0;
5783
+ const lines = [
5784
+ [[` rness \xB7 ${workspace}`, look.bold]],
5785
+ tabBar(tabs, view.tab, look),
5786
+ rule,
5787
+ ...rows,
5788
+ ...Array.from({ length: page - shown }, () => [[""]]),
5789
+ rule,
5790
+ [[footer(view.top, Math.max(0, total - view.top - page)), look.dim]]
5791
+ ];
5792
+ return lines.slice(0, Math.max(0, height)).map((l) => line2(l, columns));
5793
+ }
5794
+
5795
+ // src/commands/status.ts
5796
+ var ENTER = "\x1B[?1049h\x1B[?25l";
5797
+ var LEAVE = "\x1B[?25h\x1B[?1049l";
5798
+ function lookFor(output) {
5799
+ return {
5800
+ bold: (text) => bold(text, output),
5801
+ dim: (text) => grey(text, output),
5802
+ inverse: (text) => inverse(text, output),
5803
+ tone: (tone, text) => tone === "done" ? paint("done", text, output) : tone === "dropped" ? grey(text, output) : tone === "missing" ? paint("error", text, output) : paint("warn", text, output)
5804
+ };
5805
+ }
5806
+ var MOVES = /* @__PURE__ */ new Set([
5807
+ "left",
5808
+ "right",
5809
+ "up",
5810
+ "down",
5811
+ "pageup",
5812
+ "pagedown",
5813
+ "home",
5814
+ "end"
5815
+ ]);
5816
+ function keyOf(key) {
5817
+ const name = key?.name;
5818
+ if (name === void 0) return null;
5819
+ if (name === "q" || name === "escape" || key?.ctrl === true && name === "c")
5820
+ return "close";
5821
+ if (name === "tab") return key?.shift === true ? "backtab" : "tab";
5822
+ return MOVES.has(name) ? name : null;
5823
+ }
5824
+ function showView(tabs, workspace, start, { input, output }) {
5825
+ return new Promise((resolve8, reject) => {
5826
+ const look = lookFor(output);
5827
+ let view = { tab: start, top: 0 };
5828
+ const rows = () => output.rows || 24;
5829
+ const draw = () => {
5830
+ view = clampView(view, tabs, pageSize(rows()));
5831
+ const lines = renderView(
5832
+ view,
5833
+ tabs,
5834
+ workspace,
5835
+ output.columns || 80,
5836
+ rows(),
5837
+ look
5838
+ );
5839
+ output.write(`\x1B[H${lines.map((l) => `${l}\x1B[K`).join("\r\n")}\x1B[J`);
5840
+ };
5841
+ const restore = () => {
5842
+ output.write(LEAVE);
5843
+ };
5844
+ const close = (error) => {
5845
+ input.off("keypress", onKey);
5846
+ output.off("resize", onResize);
5847
+ process.off("exit", restore);
5848
+ input.setRawMode(false);
5849
+ input.pause();
5850
+ restore();
5851
+ if (error === void 0) resolve8();
5852
+ else reject(error);
5853
+ };
5854
+ const guarded = (step2) => {
5855
+ try {
5856
+ step2();
5857
+ } catch (e) {
5858
+ close(e);
5859
+ }
5860
+ };
5861
+ const onKey = (_, key) => {
5862
+ const k = keyOf(key);
5863
+ if (k === "close") close();
5864
+ else if (k !== null)
5865
+ guarded(() => {
5866
+ view = press(view, k, tabs, pageSize(rows()));
5867
+ draw();
5868
+ });
5869
+ };
5870
+ const onResize = () => {
5871
+ guarded(draw);
5872
+ };
5873
+ emitKeypressEvents(input);
5874
+ input.setRawMode(true);
5875
+ input.resume();
5876
+ output.write(ENTER);
5877
+ process.on("exit", restore);
5878
+ input.on("keypress", onKey);
5879
+ output.on("resize", onResize);
5880
+ guarded(draw);
5881
+ });
5882
+ }
5883
+ async function statusCommand(tab, opts, terminal = { input: process.stdin, output: process.stdout }) {
5884
+ try {
5885
+ const ws = await findWorkspace(opts.cwd ?? process.cwd());
5886
+ const manifest = await loadManifest(ws.rnessDir);
5887
+ const workspace = workspaceName(manifest, ws.root);
5888
+ const tabs = await statusTabs(ws.rnessDir);
5889
+ const wanted = tab === void 0 ? void 0 : findTab(tabs, tab);
5890
+ if (tab !== void 0 && wanted === void 0) {
5891
+ process.stderr.write(
5892
+ `unknown tab "${tab}" (tabs: ${tabs.map((t) => t.name).join(", ")})
5893
+ `
5894
+ );
5895
+ return 2;
5896
+ }
5897
+ if (terminal.input.isTTY === true && terminal.output.isTTY === true) {
5898
+ await showView(
5899
+ tabs,
5900
+ workspace,
5901
+ wanted === void 0 ? 0 : tabs.indexOf(wanted),
5902
+ terminal
5903
+ );
5904
+ return 0;
5905
+ }
5906
+ terminal.output.write(
5907
+ statusMarkdown(workspace, wanted === void 0 ? tabs : [wanted])
5908
+ );
5909
+ return 0;
5910
+ } catch (e) {
5911
+ return reportError(e);
5912
+ }
5913
+ }
5914
+
5507
5915
  // src/cli.ts
5508
5916
  function buildProgram(state) {
5509
5917
  const program = new Command().name("rness").description("Configuration-plane CLI for rness workspaces").version(VERSION, "-v, --version", "print the version").exitOverride().showHelpAfterError().configureOutput({
@@ -5531,6 +5939,11 @@ function buildProgram(state) {
5531
5939
  ).option("--json", "print JSON instead of Markdown").addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
5532
5940
  state.code = await contextCommand(opts);
5533
5941
  });
5942
+ program.command("status").description(
5943
+ "Show the status of every decision, specification and plan, a tab per directory"
5944
+ ).argument("[tab]", "open on this tab: adr, specs, plans, \u2026").addOption(new Option("--cwd <dir>").hideHelp()).action(async (tab, opts) => {
5945
+ state.code = await statusCommand(tab, opts);
5946
+ });
5534
5947
  program.command("validate").description(
5535
5948
  "Check the .rness/ tree against the contract and every generated block"
5536
5949
  ).addOption(new Option("--cwd <dir>").hideHelp()).action(async (opts) => {
@@ -5759,4 +6172,4 @@ export {
5759
6172
  OWN_COMMANDS,
5760
6173
  catchUp
5761
6174
  };
5762
- //# sourceMappingURL=chunk-R4BRAAC2.js.map
6175
+ //# sourceMappingURL=chunk-LW4S5OLI.js.map