@rness/cli 0.5.0 → 0.5.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.
@@ -774,6 +774,18 @@ async function probeRemote(url, timeoutMs = 6e4, credentials) {
774
774
  import { readFile as readFile3 } from "fs/promises";
775
775
  import { basename as basename2, join as join3 } from "path";
776
776
  var NAME = /^(?!\.{1,2}$)[a-z0-9._][a-z0-9._-]*$/;
777
+ var CONTEXT_REPO = ".rness";
778
+ var EXCLUDED_NAMES = /* @__PURE__ */ new Set([
779
+ CONTEXT_REPO,
780
+ ".git",
781
+ "node_modules"
782
+ ]);
783
+ function isMemberRepo(repo) {
784
+ return !repo.archived && !EXCLUDED_NAMES.has(repo.name.toLowerCase());
785
+ }
786
+ function isWorkspaceClone(name) {
787
+ return NAME.test(name) && !EXCLUDED_NAMES.has(name);
788
+ }
777
789
  var NAME_RULE = "lowercase letters, digits, '.', '_' and '-', not starting with '-'";
778
790
  var ORG_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9]|-(?=[A-Za-z0-9])){0,38}$/;
779
791
  var KEYS = ["contract", "org", "repos", "scopes"];
@@ -1035,7 +1047,7 @@ function paint(tone, text, stream = process.stdout) {
1035
1047
  if (noColor()) return text;
1036
1048
  return styleText(FORMAT[tone], text, { stream, validateStream: true });
1037
1049
  }
1038
- var IDLE = /* @__PURE__ */ new Set(["unchanged", "skipped", "not cloned"]);
1050
+ var IDLE = /* @__PURE__ */ new Set(["unchanged", "skipped", "not cloned", "current"]);
1039
1051
  var DONE = /* @__PURE__ */ new Set([
1040
1052
  "created",
1041
1053
  "cloned",
@@ -1067,11 +1079,20 @@ function bold(text, stream = process.stdout) {
1067
1079
  if (noColor()) return text;
1068
1080
  return styleText("bold", text, { stream, validateStream: true });
1069
1081
  }
1070
- function badge(text, stream = process.stdout) {
1082
+ var BACKGROUND = {
1083
+ intro: ["bgCyan", "black", "bold"],
1084
+ done: ["bgGreen", "black", "bold"],
1085
+ idle: ["bgCyan", "black", "bold"],
1086
+ info: ["bgCyan", "black", "bold"],
1087
+ warn: ["bgYellow", "black", "bold"],
1088
+ error: ["bgRed", "white", "bold"]
1089
+ };
1090
+ function badge(text, tone = "intro", stream = process.stdout) {
1091
+ const [t, s] = typeof tone === "string" ? [tone, stream] : ["intro", tone];
1071
1092
  if (noColor()) return text;
1072
1093
  const padded = ` ${text} `;
1073
- const painted = styleText(["bgCyan", "black", "bold"], padded, {
1074
- stream,
1094
+ const painted = styleText(BACKGROUND[t], padded, {
1095
+ stream: s,
1075
1096
  validateStream: true
1076
1097
  });
1077
1098
  return painted === padded ? text : painted;
@@ -1214,6 +1235,14 @@ var CHECKING = [
1214
1235
  function usingRest(access2, login) {
1215
1236
  return access2.ok ? `ssh (github.com as ${access2.login})` : `https (ssh to github.com unavailable: ${access2.reason}${loggedInAs(login)})`;
1216
1237
  }
1238
+ function agentHintLines(platform = process.platform) {
1239
+ const mac = platform === "darwin";
1240
+ return [
1241
+ "your key works but is not loaded; ssh asks again in every child:",
1242
+ ` ssh-add ${mac ? "--apple-use-keychain " : ""}~/.ssh/id_ed25519`,
1243
+ ` ~/.ssh/config: AddKeysToAgent yes${mac ? ", UseKeychain yes" : ""}`
1244
+ ];
1245
+ }
1217
1246
  function sshWorkspaceLines(reason3) {
1218
1247
  return [
1219
1248
  `this workspace clones over SSH (rness.json) but ssh to github.com fails: ${reason3}`,
@@ -1229,6 +1258,7 @@ function httpsFlagLine(spec) {
1229
1258
  }
1230
1259
 
1231
1260
  // src/core/workspace.ts
1261
+ import { readdir } from "fs/promises";
1232
1262
  import { dirname as dirname3, join as join5, parse, resolve as resolve2 } from "path";
1233
1263
  async function findWorkspace(startDir) {
1234
1264
  const start = resolve2(startDir);
@@ -1248,6 +1278,16 @@ async function findWorkspace(startDir) {
1248
1278
  }
1249
1279
  throw new Error(`no rness workspace found above ${startDir}`);
1250
1280
  }
1281
+ async function clonesIn(root) {
1282
+ try {
1283
+ const entries = await readdir(join5(root, "org"), { withFileTypes: true });
1284
+ return new Set(
1285
+ entries.filter((e) => e.isDirectory() && isWorkspaceClone(e.name)).map((e) => e.name)
1286
+ );
1287
+ } catch {
1288
+ return /* @__PURE__ */ new Set();
1289
+ }
1290
+ }
1251
1291
 
1252
1292
  // src/report.ts
1253
1293
  function reportError(e) {
@@ -1544,7 +1584,6 @@ async function ensureClaudeMd(dir) {
1544
1584
  }
1545
1585
 
1546
1586
  // src/commands/sync.ts
1547
- import { readdir } from "fs/promises";
1548
1587
  import { join as join9 } from "path";
1549
1588
 
1550
1589
  // src/core/progress.ts
@@ -1642,12 +1681,17 @@ var plainUi = {
1642
1681
  return result;
1643
1682
  }
1644
1683
  };
1684
+ var IDLE_SYMBOL = "\xB7";
1645
1685
  function sessionUi(p) {
1646
1686
  const say = (verb, text) => {
1647
1687
  const tone = toneOf(verb);
1648
- if (tone === "done") p.log.step(text);
1688
+ if (tone === "done") p.log.success(text);
1649
1689
  else if (tone === "warn") p.log.warn(text);
1650
- else if (tone === "idle") p.log.message(paint("idle", text));
1690
+ else if (tone === "error") p.log.error(text);
1691
+ else if (tone === "idle")
1692
+ p.log.message(paint("idle", text), {
1693
+ symbol: paint("idle", IDLE_SYMBOL)
1694
+ });
1651
1695
  else p.log.info(text);
1652
1696
  };
1653
1697
  return {
@@ -1657,10 +1701,10 @@ function sessionUi(p) {
1657
1701
  p.intro(badge(title));
1658
1702
  },
1659
1703
  outro: (message, tone = "done") => {
1660
- p.outro(paint(tone, message));
1704
+ p.outro(badge(message, tone));
1661
1705
  },
1662
1706
  line: (verb, rest, sentence) => {
1663
- say(verb, sentence ?? tidy(`${verb} ${rest}`));
1707
+ say(verb, sentence ?? `${rest} ${verb}`);
1664
1708
  },
1665
1709
  warn: (text) => {
1666
1710
  p.log.warn(text);
@@ -1679,22 +1723,21 @@ function sessionUi(p) {
1679
1723
  },
1680
1724
  async step(options, work, done) {
1681
1725
  const label = options.sentence ?? tidy(options.doing);
1682
- const finish = (result) => {
1683
- const line = done?.(result) ?? null;
1684
- return line === null ? null : tidy(`${line[0]} ${line[1]}`);
1685
- };
1726
+ const finish = (result) => done?.(result) ?? null;
1727
+ const label_ = (line) => `${line[1]} ${line[0]}`;
1686
1728
  if (options.git === true && !this.gitIsSilent) {
1687
1729
  p.log.message(paint("idle", `${label}\u2026`));
1688
1730
  const result = await work();
1689
- const text = finish(result);
1690
- if (text !== null) p.log.step(text);
1731
+ const line = finish(result);
1732
+ if (line !== null) say(line[0], label_(line));
1691
1733
  return result;
1692
1734
  }
1693
1735
  const spin = p.spinner();
1694
1736
  spin.start(label);
1695
1737
  try {
1696
1738
  const result = await work();
1697
- spin.stop(finish(result) ?? label);
1739
+ const line = finish(result);
1740
+ spin.stop(line === null ? label : label_(line));
1698
1741
  return result;
1699
1742
  } catch (e) {
1700
1743
  spin.error(`${label} \u2014 failed`);
@@ -1738,16 +1781,6 @@ function targetsOf(manifest, root, only) {
1738
1781
  return targets;
1739
1782
  }
1740
1783
  var SELECT_ALL = "*";
1741
- async function clonesIn(root) {
1742
- try {
1743
- const entries = await readdir(join9(root, "org"), { withFileTypes: true });
1744
- return new Set(
1745
- entries.filter((e) => e.isDirectory() && !e.name.startsWith(".")).map((e) => e.name)
1746
- );
1747
- } catch {
1748
- return /* @__PURE__ */ new Set();
1749
- }
1750
- }
1751
1784
  async function askWhatToSync(input) {
1752
1785
  if (!input.terminal.isTty()) {
1753
1786
  process.stderr.write(
@@ -1978,7 +2011,7 @@ async function syncCommand(opts, deps = {}) {
1978
2011
  }
1979
2012
 
1980
2013
  // src/commands/add.ts
1981
- import { join as join10 } from "path";
2014
+ import { basename as basename3, join as join10 } from "path";
1982
2015
  function printDeclared(ui, result, already) {
1983
2016
  for (const name of result.declared) {
1984
2017
  if (already.has(name)) continue;
@@ -2013,6 +2046,13 @@ async function addCommand(spec, opts, deps = {}) {
2013
2046
  `);
2014
2047
  return 2;
2015
2048
  }
2049
+ if (parsed.name === CONTEXT_REPO) {
2050
+ process.stderr.write(
2051
+ `${CONTEXT_REPO} is the workspace context, not a member repository \u2014 it is already cloned at ${basename3(ws.root)}/${CONTEXT_REPO}
2052
+ `
2053
+ );
2054
+ return 2;
2055
+ }
2016
2056
  const interactive = opts.yes !== true;
2017
2057
  const ownUi = deps.ui === void 0;
2018
2058
  if (interactive && !terminal.isTty()) {
@@ -2214,6 +2254,11 @@ async function readPinnedCli(rnessDir) {
2214
2254
  version: typeof pkg.version === "string" ? pkg.version : null
2215
2255
  };
2216
2256
  }
2257
+ async function pinnedBinPath(rnessDir) {
2258
+ const pinned = await readPinnedCli(rnessDir);
2259
+ if (pinned === null) return null;
2260
+ return join11(pinned.dir, ...pinned.bin.split("/"));
2261
+ }
2217
2262
  async function findDelegate(cwd, ownVersion) {
2218
2263
  let root;
2219
2264
  let rnessDir;
@@ -2264,13 +2309,23 @@ function installErrorLine(stderr) {
2264
2309
  const lines = stderr.split("\n").map((l) => l.trim()).filter((l) => l !== "");
2265
2310
  return lines.find((l) => !BARE_NPM_CODE.test(l)) ?? firstLine3(stderr);
2266
2311
  }
2267
- async function installDependencies(pm, dir) {
2312
+ var FROZEN = {
2313
+ npm: ["ci"],
2314
+ pnpm: ["install", "--frozen-lockfile"],
2315
+ yarn: ["install", "--immutable"],
2316
+ bun: ["install", "--frozen-lockfile"]
2317
+ };
2318
+ function frozenArgs(pm) {
2319
+ return [...FROZEN[pm]];
2320
+ }
2321
+ async function installDependencies(pm, dir, opts = {}) {
2322
+ const args = opts.frozen === true ? frozenArgs(pm) : ["install"];
2268
2323
  try {
2269
- await execFileP2(pm, ["install"], { cwd: dir, maxBuffer: 16 * 1024 * 1024 });
2324
+ await execFileP2(pm, args, { cwd: dir, maxBuffer: 16 * 1024 * 1024 });
2270
2325
  } catch (e) {
2271
2326
  const err = e;
2272
2327
  throw new Error(
2273
- `${pm} install failed in ${dir}: ${installErrorLine(err.stderr ?? err.message)}`,
2328
+ `${pm} ${args.join(" ")} failed in ${dir}: ${installErrorLine(err.stderr ?? err.message)}`,
2274
2329
  { cause: e }
2275
2330
  );
2276
2331
  }
@@ -2279,7 +2334,7 @@ async function installDependencies(pm, dir) {
2279
2334
  // src/core/pinned.ts
2280
2335
  import { execFile as execFile3 } from "child_process";
2281
2336
  import { readFile as readFile7 } from "fs/promises";
2282
- import { basename as basename3, join as join12 } from "path";
2337
+ import { basename as basename4, join as join12 } from "path";
2283
2338
  import { promisify as promisify3 } from "util";
2284
2339
  var execFileP3 = promisify3(execFile3);
2285
2340
  var PACKAGE = "@rness/cli";
@@ -2349,6 +2404,11 @@ async function workspacePackageManager(rnessDir) {
2349
2404
  if (await exists(join12(rnessDir, lockfile))) return pm;
2350
2405
  return "npm";
2351
2406
  }
2407
+ async function hasLockfile(rnessDir, pm) {
2408
+ for (const [lockfile, owner] of LOCKFILES)
2409
+ if (owner === pm && await exists(join12(rnessDir, lockfile))) return true;
2410
+ return false;
2411
+ }
2352
2412
  async function pinDrift(rnessDir) {
2353
2413
  const pin = await readPin(rnessDir);
2354
2414
  if (pin === null || !EXACT_VERSION.test(pin.spec)) return null;
@@ -2369,9 +2429,38 @@ async function driftWarning(cwd) {
2369
2429
  if (drift === null) return null;
2370
2430
  const pm = await workspacePackageManager(rnessDir);
2371
2431
  return warn(
2372
- `${basename3(root)}/.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${pm} install in .rness`
2432
+ `${basename4(root)}/.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${pm} install in .rness`
2373
2433
  );
2374
2434
  }
2435
+ var CHILD_DEADLINE_MS = 12e4;
2436
+ function childEnv(env = process.env) {
2437
+ return {
2438
+ ...env,
2439
+ GIT_SSH_COMMAND: `${env["GIT_SSH_COMMAND"] ?? "ssh"} -o BatchMode=yes`,
2440
+ GIT_TERMINAL_PROMPT: "0"
2441
+ };
2442
+ }
2443
+ async function ensureInstalled(rnessDir, opts = {}) {
2444
+ const drift = await pinDrift(rnessDir);
2445
+ if (drift === null) return null;
2446
+ const env = opts.env ?? process.env;
2447
+ if (env["RNESS_NO_INSTALL"] === "1")
2448
+ return { installed: false, pin: drift.pin, reason: "RNESS_NO_INSTALL=1" };
2449
+ const pm = await workspacePackageManager(rnessDir);
2450
+ const frozen = await hasLockfile(rnessDir, pm);
2451
+ const install = opts.install ?? installDependencies;
2452
+ opts.announce?.(drift.pin);
2453
+ try {
2454
+ await install(pm, rnessDir, { frozen });
2455
+ return { installed: true, pin: drift.pin, reason: null };
2456
+ } catch (e) {
2457
+ return {
2458
+ installed: false,
2459
+ pin: drift.pin,
2460
+ reason: e instanceof Error ? e.message : String(e)
2461
+ };
2462
+ }
2463
+ }
2375
2464
  async function runPinnedSync(root) {
2376
2465
  const installed = await readPinnedCli(join12(root, ".rness"));
2377
2466
  if (installed === null) return null;
@@ -2387,15 +2476,23 @@ async function runPinnedSync(root) {
2387
2476
  const { stdout, stderr } = await execFileP3(
2388
2477
  process.execPath,
2389
2478
  [pinned, "sync", "--yes"],
2390
- { cwd: root, maxBuffer: 16 * 1024 * 1024 }
2479
+ {
2480
+ cwd: root,
2481
+ maxBuffer: 16 * 1024 * 1024,
2482
+ env: childEnv(),
2483
+ timeout: CHILD_DEADLINE_MS,
2484
+ killSignal: "SIGKILL"
2485
+ }
2391
2486
  );
2392
2487
  return { code: 0, stdout, stderr };
2393
2488
  } catch (e) {
2394
2489
  const err = e;
2490
+ const deadline = err.killed === true ? `rness sync in .rness did not finish within ${CHILD_DEADLINE_MS / 1e3}s and was stopped
2491
+ ` : "";
2395
2492
  return {
2396
2493
  code: typeof err.code === "number" ? err.code : 1,
2397
2494
  stdout: err.stdout ?? "",
2398
- stderr: err.stderr ?? ""
2495
+ stderr: `${err.stderr ?? ""}${deadline}`
2399
2496
  };
2400
2497
  }
2401
2498
  }
@@ -2415,6 +2512,19 @@ function summariseSync(stdout) {
2415
2512
  others
2416
2513
  };
2417
2514
  }
2515
+ function syncOutcome(result, opts = {}) {
2516
+ const { others } = summariseSync(result.stdout);
2517
+ const wrote = /^(updated|unchanged|stale)\s/m.test(result.stdout);
2518
+ const stderr = result.stderr.split("\n").map((l) => l.trimEnd()).filter((l) => l.trim() !== "");
2519
+ const hints = wrote ? [...others, ...stderr] : others;
2520
+ if (opts.sshWorks === true && /Permission denied \(publickey\)/.test(result.stderr))
2521
+ hints.push(...agentHintLines());
2522
+ return {
2523
+ code: wrote ? 0 : result.code,
2524
+ hints,
2525
+ errors: wrote ? [] : stderr
2526
+ };
2527
+ }
2418
2528
  async function syncPinned(root, shown, fallback) {
2419
2529
  const rnessDir = join12(root, ".rness");
2420
2530
  const installed = await readPinnedCli(rnessDir);
@@ -2438,7 +2548,13 @@ async function syncPinned(root, shown, fallback) {
2438
2548
  const { stdout, stderr } = await execFileP3(
2439
2549
  process.execPath,
2440
2550
  [pinned, "sync", "--yes"],
2441
- { cwd: root, maxBuffer: 16 * 1024 * 1024 }
2551
+ {
2552
+ cwd: root,
2553
+ maxBuffer: 16 * 1024 * 1024,
2554
+ env: childEnv(),
2555
+ timeout: CHILD_DEADLINE_MS,
2556
+ killSignal: "SIGKILL"
2557
+ }
2442
2558
  );
2443
2559
  process.stdout.write(stdout);
2444
2560
  if (stderr !== "") process.stderr.write(stderr);
@@ -2467,6 +2583,7 @@ var SCAFFOLD_FILES = [
2467
2583
  "_gitignore",
2468
2584
  ".githooks/pre-commit",
2469
2585
  ".github/workflows/validate.yml",
2586
+ ".github/dependabot.yml",
2470
2587
  "pnpm-workspace.yaml",
2471
2588
  "adr/0000-template.md",
2472
2589
  "standards/.gitkeep",
@@ -2511,7 +2628,7 @@ async function copyScaffold(dest, tokens) {
2511
2628
  }
2512
2629
 
2513
2630
  // src/version.ts
2514
- var VERSION = "0.5.0";
2631
+ var VERSION = "0.5.1";
2515
2632
 
2516
2633
  // src/commands/login.ts
2517
2634
  import { spawn as spawn3 } from "child_process";
@@ -2668,7 +2785,7 @@ import { tmpdir } from "os";
2668
2785
  import { dirname as dirname6, join as join15, relative as relative2, resolve as resolve3 } from "path";
2669
2786
 
2670
2787
  // src/core/sync-blocks.ts
2671
- async function syncBlocks(ui, root, shown, inPlace) {
2788
+ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
2672
2789
  if (!ui.session)
2673
2790
  return ui.step(
2674
2791
  { doing: "syncing the blocks", git: true },
@@ -2689,10 +2806,10 @@ async function syncBlocks(ui, root, shown, inPlace) {
2689
2806
  );
2690
2807
  return 1;
2691
2808
  }
2692
- for (const line of summariseSync(result.stdout).others) ui.hint(line);
2693
- for (const line of result.stderr.split("\n"))
2694
- if (line.trim() !== "") ui.error(line);
2695
- return result.code;
2809
+ const outcome = syncOutcome(result, { sshWorks: sshWorks === true });
2810
+ for (const line of outcome.hints) ui.hint(line);
2811
+ for (const line of outcome.errors) ui.error(line);
2812
+ return outcome.code;
2696
2813
  }
2697
2814
 
2698
2815
  // src/commands/create.ts
@@ -2760,7 +2877,12 @@ async function pickRepositories(input) {
2760
2877
  quiet: true
2761
2878
  },
2762
2879
  () => provider.listRepositories(org),
2763
- (l) => ["listed", `${l.repositories.length} repositories of ${org}`]
2880
+ // What the listing offers, not what GitHub returned: the count and the
2881
+ // list below it must agree (spec 0009).
2882
+ (l) => [
2883
+ "listed",
2884
+ `${l.repositories.filter(isMemberRepo).length} repositories of ${org}`
2885
+ ]
2764
2886
  );
2765
2887
  listed = listing.repositories;
2766
2888
  if (listing.owner === "user")
@@ -2779,9 +2901,7 @@ async function pickRepositories(input) {
2779
2901
  }
2780
2902
  const inCatalogue = (name) => Object.hasOwn(input.catalogue, name);
2781
2903
  const byLabel = (a, b) => a.label.localeCompare(b.label, "en", { sensitivity: "base" });
2782
- const candidates = listed.filter(
2783
- (r) => !r.archived && !r.name.startsWith(".")
2784
- );
2904
+ const candidates = listed.filter(isMemberRepo);
2785
2905
  const padlock = unicode();
2786
2906
  const hint = (isPrivate, catalogued) => [isPrivate && !padlock ? "private" : null, catalogued ? "in .rness" : null].filter((h) => h !== null).join(" \xB7 ") || null;
2787
2907
  const declarable = candidates.filter((r) => NAME.test(r.name.toLowerCase())).map((r) => {
@@ -2969,7 +3089,7 @@ async function createCommand(opts, deps = {}) {
2969
3089
  );
2970
3090
  return 2;
2971
3091
  }
2972
- const pm = opts.pm !== void 0 && isPackageManager(opts.pm) ? opts.pm : detectPackageManager();
3092
+ let pm = opts.pm !== void 0 && isPackageManager(opts.pm) ? opts.pm : detectPackageManager();
2973
3093
  const interactive = opts.yes !== true && terminal.isTty();
2974
3094
  const ui = interactive ? await makeUi(terminal) : { ...plainUi };
2975
3095
  let sshTest;
@@ -2984,6 +3104,7 @@ async function createCommand(opts, deps = {}) {
2984
3104
  }
2985
3105
  return sshTest.access;
2986
3106
  };
3107
+ const sshWorks = () => sshTest?.access.ok === true;
2987
3108
  const chooseHost = async () => {
2988
3109
  if (opts.host !== void 0 || opts.https === true) ui.gitIsSilent = true;
2989
3110
  if (opts.host !== void 0) return opts.host;
@@ -3151,12 +3272,13 @@ async function createCommand(opts, deps = {}) {
3151
3272
  const rnessDir = join15(root, ".rness");
3152
3273
  const install = async () => {
3153
3274
  if (opts.skipInstall === true) {
3154
- ui.line("skipped", "install (--skip-install)");
3275
+ ui.line("skipped", `install with ${pm} (--skip-install)`);
3155
3276
  return;
3156
3277
  }
3278
+ const frozen = await hasLockfile(rnessDir, pm);
3157
3279
  await ui.step(
3158
3280
  { doing: `installing dependencies with ${pm}` },
3159
- () => installDependencies(pm, rnessDir),
3281
+ () => installDependencies(pm, rnessDir, { frozen }),
3160
3282
  () => ["installed", `dependencies with ${pm}`]
3161
3283
  );
3162
3284
  };
@@ -3169,11 +3291,7 @@ async function createCommand(opts, deps = {}) {
3169
3291
  gitProvider.credentialsFor(contextUrl)
3170
3292
  );
3171
3293
  ui.line("cloned", `${shown}/.rness (joined ${org})`);
3172
- const pin = (await readPin(rnessDir))?.spec;
3173
- if (pin !== void 0 && EXACT_VERSION.test(pin) && compareVersions(pin, VERSION) < 0)
3174
- ui.hint(
3175
- `${org}/.rness pins @rness/cli ${pin} \u2014 you run ${VERSION}; rness upgrade moves the workspace`
3176
- );
3294
+ if (opts.pm === void 0) pm = await workspacePackageManager(rnessDir);
3177
3295
  await install();
3178
3296
  await mkdir3(join15(root, "org"), { recursive: true });
3179
3297
  const failures2 = await cloneSelection({
@@ -3190,14 +3308,28 @@ async function createCommand(opts, deps = {}) {
3190
3308
  ui,
3191
3309
  root,
3192
3310
  shown,
3193
- () => syncCommand({ yes: true, cwd: root })
3311
+ () => syncCommand({ yes: true, cwd: root }),
3312
+ sshWorks()
3194
3313
  );
3195
3314
  if (code2 !== 0) return code2;
3196
- if (failures2.length > 0)
3197
- ui.note(
3198
- "Next steps",
3199
- failures2.map((f) => `rness add ${f.spec} # failed: ${f.message}`)
3200
- );
3315
+ const cloned = await clonesIn(root);
3316
+ const remaining = Object.keys(catalogue.repos).filter(
3317
+ (name2) => !cloned.has(name2)
3318
+ );
3319
+ const next2 = [
3320
+ `cd ${shown}`,
3321
+ // The comment above its command, as the new-workspace note does: a
3322
+ // line that wraps in a narrow terminal is a line nobody reads.
3323
+ ...remaining.length > 0 ? [
3324
+ `# clone the catalogue repositories you did not pick: ${remaining.join(", ")}`,
3325
+ "rness sync --all"
3326
+ ] : [],
3327
+ ...failures2.map((f) => `rness add ${f.spec} # failed: ${f.message}`)
3328
+ ];
3329
+ if (ui.session) ui.note("Next steps", next2);
3330
+ else process.stdout.write(`
3331
+ ${next2.map((l) => ` ${l}`).join("\n")}
3332
+ `);
3201
3333
  ui.outro(`You joined ${org} \u2014 your workspace is in ${shown}/`);
3202
3334
  return failures2.length > 0 ? 1 : 0;
3203
3335
  }
@@ -3248,7 +3380,8 @@ async function createCommand(opts, deps = {}) {
3248
3380
  ui,
3249
3381
  root,
3250
3382
  shown,
3251
- () => syncCommand({ yes: true, cwd: root })
3383
+ () => syncCommand({ yes: true, cwd: root }),
3384
+ sshWorks()
3252
3385
  );
3253
3386
  if (code !== 0) return code;
3254
3387
  const name = org;
@@ -3329,8 +3462,8 @@ async function logoutCommand() {
3329
3462
 
3330
3463
  // src/commands/upgrade.ts
3331
3464
  import { execFile as execFile5 } from "child_process";
3332
- import { readFile as readFile9, readdir as readdir3 } from "fs/promises";
3333
- import { basename as basename4, join as join16, relative as relative3 } from "path";
3465
+ import { mkdir as mkdir4, readFile as readFile9, readdir as readdir3 } from "fs/promises";
3466
+ import { basename as basename5, dirname as dirname7, join as join16, relative as relative3 } from "path";
3334
3467
  import { promisify as promisify5 } from "util";
3335
3468
  var execFileP5 = promisify5(execFile5);
3336
3469
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
@@ -3360,7 +3493,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3360
3493
  }
3361
3494
  try {
3362
3495
  const ws = await findWorkspace(cwd);
3363
- const label = `${basename4(ws.root)}/.rness`;
3496
+ const label = `${basename5(ws.root)}/.rness`;
3364
3497
  const pin = await readPin(ws.rnessDir);
3365
3498
  if (pin === null) {
3366
3499
  process.stderr.write(
@@ -3377,7 +3510,7 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3377
3510
  return 2;
3378
3511
  }
3379
3512
  const ui = interactive ? await makeUi(terminal) : { ...plainUi };
3380
- ui.intro(`Upgrade workspace ${basename4(ws.root)}`);
3513
+ ui.intro(`Upgrade workspace ${basename5(ws.root)}`);
3381
3514
  const target = version ?? await ui.step(
3382
3515
  { doing: "asking the registry for the latest release", quiet: true },
3383
3516
  () => latestVersion(),
@@ -3453,6 +3586,20 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3453
3586
  ui.warn(`${shown} names @rness/cli@${pin.spec}; update it by hand`);
3454
3587
  }
3455
3588
  }
3589
+ const dependabot = join16(ws.rnessDir, ".github", "dependabot.yml");
3590
+ if (await exists(dependabot)) {
3591
+ ui.hint(".github/dependabot.yml is already there \u2014 left as it is");
3592
+ } else {
3593
+ await mkdir4(dirname7(dependabot), { recursive: true });
3594
+ await writeFileAtomic(
3595
+ dependabot,
3596
+ await readFile9(join16(scaffoldDir(), ".github", "dependabot.yml"), "utf8")
3597
+ );
3598
+ ui.line(
3599
+ "wrote",
3600
+ ".rness/.github/dependabot.yml (@rness/cli now moves by pull request)"
3601
+ );
3602
+ }
3456
3603
  try {
3457
3604
  await ui.step(
3458
3605
  { doing: `installing dependencies with ${pm}` },
@@ -3480,12 +3627,12 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3480
3627
  return 1;
3481
3628
  }
3482
3629
  ui.gitIsSilent = true;
3483
- const code = await syncBlocks(ui, ws.root, basename4(ws.root));
3630
+ const code = await syncBlocks(ui, ws.root, basename5(ws.root));
3484
3631
  if (code !== 0) return code;
3485
3632
  const rel = relative3(cwd, ws.rnessDir) || ".";
3486
3633
  const next = [
3487
3634
  `git -C ${rel} add -A && git -C ${rel} commit -m "chore: rness ${target}"`,
3488
- `# teammates: git pull, then ${pm} install in .rness`
3635
+ "# teammates: git pull \u2014 the next rness command installs it"
3489
3636
  ];
3490
3637
  if (ui.session) {
3491
3638
  ui.note("Next steps", next);
@@ -3509,15 +3656,23 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
3509
3656
 
3510
3657
  // src/core/blocks.ts
3511
3658
  import { join as join17 } from "path";
3659
+ var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
3512
3660
  async function checkBlocks({
3513
3661
  root,
3514
3662
  rnessDir,
3515
3663
  manifest,
3516
3664
  org
3517
3665
  }) {
3518
- const problems = [];
3519
- const warnings = [];
3520
- if (!await exists(join17(root, "org"))) return { problems, warnings };
3666
+ const checks = [];
3667
+ const note = (label, status2, message = null) => {
3668
+ checks.push({ label, status: status2, message });
3669
+ };
3670
+ const derive = () => ({
3671
+ problems: checks.filter((c) => PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
3672
+ warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
3673
+ checks
3674
+ });
3675
+ if (!await exists(join17(root, "org"))) return derive();
3521
3676
  const targets = [{ scope: null, dir: root, depth: 0, label: "AGENTS.md" }];
3522
3677
  for (const [name, entry] of Object.entries(manifest.scopes)) {
3523
3678
  const segments = entry.path.split("/");
@@ -3530,18 +3685,26 @@ async function checkBlocks({
3530
3685
  }
3531
3686
  for (const t of targets) {
3532
3687
  if (!await exists(t.dir)) {
3533
- warnings.push(`${t.label}: directory not present, block not checked`);
3688
+ note(
3689
+ t.label,
3690
+ "missing-dir",
3691
+ `${t.label}: directory not present, block not checked`
3692
+ );
3534
3693
  continue;
3535
3694
  }
3536
3695
  const text = await readOrNull(join17(t.dir, "AGENTS.md"));
3537
3696
  const lines = text === null ? [] : text.split(/\r?\n/);
3538
3697
  const found = findBlock(lines);
3539
3698
  if (found.kind === "none") {
3540
- warnings.push(`${t.label}: no rness block yet (run rness sync)`);
3699
+ note(
3700
+ t.label,
3701
+ "no-block",
3702
+ `${t.label}: no rness block yet (run rness sync)`
3703
+ );
3541
3704
  continue;
3542
3705
  }
3543
3706
  if (found.kind === "error") {
3544
- problems.push(`${t.label}: ${found.message}`);
3707
+ note(t.label, "malformed", `${t.label}: ${found.message}`);
3545
3708
  continue;
3546
3709
  }
3547
3710
  const context = await assembleContext({
@@ -3555,11 +3718,12 @@ async function checkBlocks({
3555
3718
  depth: t.depth,
3556
3719
  context
3557
3720
  });
3558
- if (!isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash)) {
3559
- problems.push(`${t.label}: stale rness block (run rness sync)`);
3560
- }
3721
+ if (isCurrentBlock(lines.slice(found.begin, found.end + 1), fresh2.hash))
3722
+ note(t.label, "current");
3723
+ else
3724
+ note(t.label, "stale", `${t.label}: stale rness block (run rness sync)`);
3561
3725
  }
3562
- return { problems, warnings };
3726
+ return derive();
3563
3727
  }
3564
3728
 
3565
3729
  // src/core/contract.ts
@@ -3619,7 +3783,7 @@ async function checkContract(rnessDir) {
3619
3783
  }
3620
3784
 
3621
3785
  // src/commands/validate.ts
3622
- async function validateCommand(opts) {
3786
+ async function validateCommand(opts, deps = {}) {
3623
3787
  const cwd = opts.cwd ?? process.cwd();
3624
3788
  try {
3625
3789
  const ws = await findWorkspace(cwd);
@@ -3636,13 +3800,29 @@ async function validateCommand(opts) {
3636
3800
  });
3637
3801
  problems.push(...blocks.problems);
3638
3802
  warnings.push(...blocks.warnings);
3639
- for (const p of problems) process.stderr.write(`${p}
3803
+ const ui = await makeUi(deps.terminal ?? defaultTerminal);
3804
+ if (!ui.session) {
3805
+ for (const p of problems) process.stderr.write(`${p}
3640
3806
  `);
3641
- for (const w of warnings) process.stderr.write(`${warn(w)}
3807
+ for (const w of warnings) process.stderr.write(`${warn(w)}
3642
3808
  `);
3643
- if (problems.length > 0) return 1;
3644
- process.stdout.write("context ok\n");
3645
- return 0;
3809
+ if (problems.length > 0) return 1;
3810
+ process.stdout.write("context ok\n");
3811
+ return 0;
3812
+ }
3813
+ ui.intro(`Validate ${org}`);
3814
+ for (const c of blocks.checks)
3815
+ if (c.message === null) ui.line("current", c.label);
3816
+ else if (c.status === "stale" || c.status === "malformed")
3817
+ ui.error(c.message);
3818
+ else ui.warn(c.message);
3819
+ for (const p of problems) if (!blocks.problems.includes(p)) ui.error(p);
3820
+ for (const w of warnings) if (!blocks.warnings.includes(w)) ui.warn(w);
3821
+ ui.outro(
3822
+ problems.length > 0 ? "Context mismatch" : "Context OK",
3823
+ problems.length > 0 ? "error" : "done"
3824
+ );
3825
+ return problems.length > 0 ? 1 : 0;
3646
3826
  } catch (e) {
3647
3827
  return reportError(e);
3648
3828
  }
@@ -3833,13 +4013,16 @@ export {
3833
4013
  addCommand,
3834
4014
  renderMarkdown,
3835
4015
  contextCommand,
4016
+ pinnedBinPath,
3836
4017
  findDelegate,
3837
4018
  PACKAGE_MANAGERS,
3838
4019
  isPackageManager,
3839
4020
  detectPackageManager,
3840
4021
  packageManagerVersion,
3841
4022
  installDependencies,
4023
+ pinDrift,
3842
4024
  driftWarning,
4025
+ ensureInstalled,
3843
4026
  SCAFFOLD_FILES,
3844
4027
  scaffoldDir,
3845
4028
  copyScaffold,
@@ -3854,4 +4037,4 @@ export {
3854
4037
  validateCommand,
3855
4038
  run
3856
4039
  };
3857
- //# sourceMappingURL=chunk-WSGPDTPQ.js.map
4040
+ //# sourceMappingURL=chunk-C6HBGV5V.js.map