@rness/cli 0.13.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -486,7 +486,7 @@ function parseRepoSpec(spec, org, host) {
486
486
  }
487
487
 
488
488
  // src/version.ts
489
- var VERSION = "0.13.0";
489
+ var VERSION = "0.14.0";
490
490
 
491
491
  // src/core/pm.ts
492
492
  import { execFile as execFile2 } from "child_process";
@@ -523,6 +523,12 @@ function rnessRunner(pm, version) {
523
523
  return "yarn dlx @rness/cli";
524
524
  return "npx @rness/cli";
525
525
  }
526
+ function localRunner(pm) {
527
+ if (pm === "pnpm") return "pnpm";
528
+ if (pm === "yarn") return "yarn";
529
+ if (pm === "bun") return "bunx";
530
+ return "npx";
531
+ }
526
532
  function rnessCommand(env = process.env) {
527
533
  const agent = env["npm_config_user_agent"] ?? "";
528
534
  if (agent === "") return "rness";
@@ -3211,6 +3217,54 @@ function reportError(e) {
3211
3217
  return 1;
3212
3218
  }
3213
3219
 
3220
+ // src/core/delegate.ts
3221
+ import { readFile as readFile6 } from "fs/promises";
3222
+ import { join as join10 } from "path";
3223
+ var DEFAULT_BIN = "dist/bin/rness.js";
3224
+ var PINNED_BIN = `node_modules/@rness/cli/${DEFAULT_BIN}`;
3225
+ async function readPinnedCli(rnessDir) {
3226
+ const dir = join10(rnessDir, "node_modules", "@rness", "cli");
3227
+ let pkg;
3228
+ try {
3229
+ pkg = JSON.parse(await readFile6(join10(dir, "package.json"), "utf8"));
3230
+ } catch {
3231
+ return null;
3232
+ }
3233
+ const declared2 = pkg.bin;
3234
+ let bin = DEFAULT_BIN;
3235
+ if (typeof declared2 === "string") bin = declared2;
3236
+ else if (declared2 !== null && typeof declared2 === "object") {
3237
+ const named = declared2["rness"];
3238
+ if (typeof named === "string") bin = named;
3239
+ }
3240
+ return {
3241
+ dir,
3242
+ bin,
3243
+ version: typeof pkg.version === "string" ? pkg.version : null
3244
+ };
3245
+ }
3246
+ async function pinnedBinPath(rnessDir) {
3247
+ const pinned = await readPinnedCli(rnessDir);
3248
+ if (pinned === null) return null;
3249
+ return join10(pinned.dir, ...pinned.bin.split("/"));
3250
+ }
3251
+ async function findDelegate(cwd, ownVersion) {
3252
+ let root;
3253
+ let rnessDir;
3254
+ try {
3255
+ const ws = await findWorkspace(cwd);
3256
+ root = ws.root;
3257
+ rnessDir = ws.rnessDir;
3258
+ } catch {
3259
+ return null;
3260
+ }
3261
+ const pinned = await readPinnedCli(rnessDir);
3262
+ if (pinned === null) return null;
3263
+ const { version } = pinned;
3264
+ if (version === null || version === ownVersion) return null;
3265
+ return { entry: join10(pinned.dir, "dist", "index.js"), version, root };
3266
+ }
3267
+
3214
3268
  // src/core/block.ts
3215
3269
  import { createHash as createHash2 } from "crypto";
3216
3270
  var BEGIN = "<!-- BEGIN rness -->";
@@ -3275,7 +3329,7 @@ function isCurrentBlock(blockLines, freshHash) {
3275
3329
  }
3276
3330
 
3277
3331
  // src/core/merge.ts
3278
- import { join as join10 } from "path";
3332
+ import { join as join11 } from "path";
3279
3333
  function findBlock(lines) {
3280
3334
  const begins = [];
3281
3335
  const ends = [];
@@ -3334,7 +3388,7 @@ function claudeMdAtRoot(existing, block) {
3334
3388
  return merged.ok ? { ...merged, changed: merged.text !== existing } : merged;
3335
3389
  }
3336
3390
  async function ensureClaudeMd(dir) {
3337
- const file = join10(dir, "CLAUDE.md");
3391
+ const file = join11(dir, "CLAUDE.md");
3338
3392
  if (await isSymlink(file)) return "unchanged";
3339
3393
  const existing = await readOrNull(file);
3340
3394
  if (existing === null) {
@@ -3349,14 +3403,222 @@ async function ensureClaudeMd(dir) {
3349
3403
  }
3350
3404
 
3351
3405
  // src/commands/sync.ts
3352
- import { join as join12 } from "path";
3406
+ import { join as join14 } from "path";
3353
3407
 
3354
3408
  // src/core/agent-targets.ts
3355
3409
  import { mkdir as mkdir2 } from "fs/promises";
3356
- import { dirname as dirname4, join as join11 } from "path";
3410
+ import { dirname as dirname4, join as join13 } from "path";
3411
+
3412
+ // src/core/claude-skills.ts
3413
+ var PLUGIN = ".claude/skills/rness";
3414
+ var PLUGIN_JSON = `${JSON.stringify(
3415
+ {
3416
+ name: "rness",
3417
+ description: "The rness workspace in Claude Code: /rness:status, /rness:adr, /rness:spec, /rness:plan, /rness:done."
3418
+ },
3419
+ null,
3420
+ 2
3421
+ )}
3422
+ `;
3423
+ function statusSkill(rness, pm) {
3424
+ const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status --cwd "\${CLAUDE_PROJECT_DIR}"`;
3425
+ return `---
3426
+ name: status
3427
+ description: The status of every decision, specification, plan and other tracked document of the rness workspace, one table per directory. Read-only.
3428
+ argument-hint: '[tab]'
3429
+ disable-model-invocation: true
3430
+ allowed-tools: Bash(${command})
3431
+ ---
3432
+
3433
+ !\`${command}\`
3434
+
3435
+ Show the output above to the user as it is: the heading and the tables,
3436
+ nothing added, nothing summarised, no other tool. Arguments: \`$ARGUMENTS\`.
3437
+ When they name a tab, show only that tab's section. If the output says the
3438
+ module cannot be found, say instead that ${rness} is not installed next to
3439
+ this repository.
3440
+
3441
+ End with this line: _For the view with tabs and scrolling: here, Ctrl+Z,
3442
+ then \`npx @rness/cli status\` (q to close), then \`fg\`; or in another
3443
+ terminal, from the workspace's \`.rness/\`: \`${localRunner(pm)} rness status\`._
3444
+ `;
3445
+ }
3446
+ var LIFECYCLE2 = [
3447
+ {
3448
+ name: "adr",
3449
+ description: "Record a decision of the rness workspace as an ADR in .rness/adr/ \u2014 a choice expensive to reverse or that creates a lasting constraint. Use when the developer asks for an ADR, or when the conversation reaches such a decision.",
3450
+ hint: "[subject | NNNN]",
3451
+ tab: "adr",
3452
+ body: (rness) => `You record a decision of this workspace as an ADR in \`${rness}/adr/\`. The table
3453
+ above lists the existing ADRs, newest first. Arguments: \`$ARGUMENTS\`.
3454
+
3455
+ Before writing:
3456
+
3457
+ 1. If the developer did not ask for an ADR in so many words, propose one in
3458
+ a single line \u2014 its title, and why the choice is hard to reverse \u2014 and
3459
+ wait for a yes.
3460
+ 2. Read \`${rness}/CONVENTIONS.md\` and \`${rness}/adr/0000-template.md\`: they hold the
3461
+ rules and the sections; this skill is only the procedure.
3462
+ 3. Arguments naming an ADR (\`NNNN\`): reopen that file. An \`Accepted\` ADR is
3463
+ never edited: offer a new ADR that supersedes it.
3464
+ 4. Otherwise the subject is the arguments, else the decision just
3465
+ discussed. Context, alternatives or consequences missing from the
3466
+ conversation: ask for them, one question at a time. Never invent them.
3467
+
3468
+ Writing:
3469
+
3470
+ - \`${rness}/adr/NNNN-<slug>.md\`, NNNN the highest number above plus one.
3471
+ - The template's front matter and sections, filled in, its guidance lines
3472
+ removed: \`date\` today, \`status: Proposed\`, \`repo\` the repositories under
3473
+ \`org/\` it affects.
3474
+ - An ADR it replaces is named in Context. That one becomes \`Superseded\`,
3475
+ with \`superseded_by:\`, only once the new ADR is \`Accepted\`.
3476
+ - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3477
+
3478
+ After writing: give the path and the Decision section, and say the ADR
3479
+ stays \`Proposed\` until the developer accepts it. Change a status only when
3480
+ the developer says so. Do not commit.
3481
+ `
3482
+ },
3483
+ {
3484
+ name: "spec",
3485
+ description: "Write a specification of the rness workspace in .rness/specs/ \u2014 the outcome wanted and its scope, before any plan. Use when the developer asks for a spec, or when agreed work is too large to start without one.",
3486
+ hint: "[subject | NNNN]",
3487
+ tab: "specs",
3488
+ body: (rness) => `You write a specification of this workspace in \`${rness}/specs/\`. The table above
3489
+ lists the existing ones, newest first. Arguments: \`$ARGUMENTS\`.
3490
+
3491
+ Before writing:
3492
+
3493
+ 1. If the developer did not ask for a specification in so many words,
3494
+ propose one in a single line and wait for a yes.
3495
+ 2. Read \`${rness}/CONVENTIONS.md\`, then the most recent specification above: match
3496
+ its front matter, sections and tone.
3497
+ 3. Arguments naming a specification (\`NNNN\`): reopen it. One that is
3498
+ \`Implemented\`, \`Superseded\` or \`Rejected\` is not edited: offer a new one.
3499
+ 4. Otherwise the subject is the arguments, else the work just discussed.
3500
+ The problem, what is in and out of scope, the ADRs it rests on: ask what
3501
+ the conversation does not say, one question at a time.
3502
+
3503
+ Writing:
3504
+
3505
+ - \`${rness}/specs/NNNN-<slug>.md\`, NNNN the highest number above plus one.
3506
+ - \`date\` and \`updated\` today, \`status: Draft\`, \`repo\`, and \`adr:\` linking
3507
+ the decisions it rests on \u2014 or the rationale in the body when none does.
3508
+ - No specification yet to follow: Summary, Scope, Out of scope,
3509
+ Alternatives considered, To verify, Tests.
3510
+ - What was checked in this session (a document read, a command run) is
3511
+ written as verified, with the date; everything else as a proposal or an
3512
+ open question.
3513
+ - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3514
+
3515
+ After writing: give the path and the summary. It stays \`Draft\` until the
3516
+ developer moves it (\`Proposed\` for review, \`Approved\` to plan it). No plan
3517
+ here: \`/rness:plan NNNN\` once approved. Do not commit.
3518
+ `
3519
+ },
3520
+ {
3521
+ name: "plan",
3522
+ description: "Turn an approved specification of the rness workspace into a plan in .rness/plans/ \u2014 ordered tasks, each with the command that proves it done. Use when the developer asks for a plan, or when a specification has just been approved.",
3523
+ hint: "<spec NNNN>",
3524
+ tab: "plans",
3525
+ body: (rness) => `You turn a specification into a plan in \`${rness}/plans/\`. The table above lists
3526
+ the existing plans, newest first. Arguments, the specification:
3527
+ \`$ARGUMENTS\`.
3528
+
3529
+ Before writing:
3530
+
3531
+ 1. If the developer did not ask for a plan in so many words, propose one in
3532
+ a single line and wait for a yes.
3533
+ 2. No specification named: ask which. Read it, \`${rness}/CONVENTIONS.md\`, and the
3534
+ most recent plan above: match its front matter and sections.
3535
+ 3. A specification that is not \`Approved\`: say so, and plan it only if the
3536
+ developer confirms. A plan of it that is neither \`Completed\` nor
3537
+ \`Abandoned\`: reopen it rather than write a second.
3538
+ 4. Read the code and the repositories the specification touches: every
3539
+ task names real files and real commands.
3540
+
3541
+ Writing:
3542
+
3543
+ - \`${rness}/plans/NNNN-<slug>.md\`, NNNN the highest plan number above plus one \u2014
3544
+ not the specification's.
3545
+ - \`date\` and \`updated\` today, \`status: Draft\`, \`repo\`, \`spec:\` linking the
3546
+ specification.
3547
+ - Tasks in order, as checkboxes. Each gives the files, what changes, and
3548
+ the command that proves it (a test, a build, a run). The last tasks:
3549
+ the documentation the work makes inaccurate, then the release when the
3550
+ repository has one.
3551
+ - Where the plan decides something the specification leaves open, a
3552
+ section says so.
3553
+ - The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3554
+
3555
+ After writing: give the path and the list of tasks. It becomes \`Ready\` when
3556
+ the developer agrees. Do not commit.
3557
+ `
3558
+ },
3559
+ {
3560
+ name: "done",
3561
+ description: "Close a piece of work of the rness workspace \u2014 check each task of its plan on evidence, then move the plan and its specification to their final status and correct the documents the work made inaccurate. Use when the developer says the work is done, or when the last task of a plan passes.",
3562
+ hint: "[plan NNNN]",
3563
+ tab: "plans",
3564
+ body: (rness) => `You close a piece of work: its plan in \`${rness}/plans/\`, its specification, and
3565
+ what they made inaccurate. The table above lists the plans, newest first.
3566
+ Arguments: \`$ARGUMENTS\`.
3567
+
3568
+ 1. The plan: the one named, else the one \`In progress\`. Several or none:
3569
+ ask.
3570
+ 2. If the developer did not ask to close it in so many words, propose it in
3571
+ a single line and wait for a yes.
3572
+ 3. Read the plan, its specification and \`${rness}/CONVENTIONS.md\`.
3573
+ 4. Evidence first. For each task, what shows it done: a test, a build, a
3574
+ command's output in this session. A task without evidence: run its
3575
+ check now, or report it. Never mark a task done on belief.
3576
+ 5. All tasks proven:
3577
+ - the plan: \`status: Completed\`, \`updated\` today, the checkboxes of the
3578
+ proven tasks ticked, and a section \`Verification (YYYY-MM-DD)\` \u2014 what
3579
+ ran, what it showed, what could not run and why;
3580
+ - the specification: \`Implemented\` once every plan of it is
3581
+ \`Completed\`; \`updated\` today;
3582
+ - the smallest set of documents the work makes inaccurate: \`${rness}/docs/\`,
3583
+ the READMEs of the repositories it changed.
3584
+ 6. A task not proven: the plan stays \`In progress\` \u2014 or \`Blocked\`, the
3585
+ blocker written in it. Say what remains.
3586
+ 7. The rness hook checks every edit under \`${rness}/\`: fix what it reports.
3587
+
3588
+ After: list the files changed and the statuses moved. Do not commit.
3589
+ `
3590
+ }
3591
+ ];
3592
+ function lifecycleSkill(rness, s) {
3593
+ const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status ${s.tab} --cwd "\${CLAUDE_PROJECT_DIR}"`;
3594
+ return `---
3595
+ name: ${s.name}
3596
+ description: ${s.description}
3597
+ argument-hint: '${s.hint}'
3598
+ allowed-tools: Bash(${command})
3599
+ ---
3600
+
3601
+ !\`${command}\`
3602
+
3603
+ ${s.body(rness)}`;
3604
+ }
3605
+ function pluginFiles(rness, at, ctx) {
3606
+ return [
3607
+ { file: `${PLUGIN}/.claude-plugin/plugin.json`, at, content: PLUGIN_JSON },
3608
+ {
3609
+ file: `${PLUGIN}/skills/status/SKILL.md`,
3610
+ at,
3611
+ content: statusSkill(rness, ctx.packageManager)
3612
+ },
3613
+ ...LIFECYCLE2.map((s) => ({
3614
+ file: `${PLUGIN}/skills/${s.name}/SKILL.md`,
3615
+ at,
3616
+ content: lifecycleSkill(rness, s)
3617
+ }))
3618
+ ];
3619
+ }
3357
3620
 
3358
3621
  // src/core/agents.ts
3359
- var PINNED_BIN = "node_modules/@rness/cli/dist/bin/rness.js";
3360
3622
  function hookLine(rness, event) {
3361
3623
  const run3 = `f="$CLAUDE_PROJECT_DIR/${rness}/${PINNED_BIN}"; if [ -f "$f" ]; then node "$f" hook ${event};`;
3362
3624
  if (event !== "session-start") return `${run3} fi`;
@@ -3409,53 +3671,11 @@ function hooks(rness) {
3409
3671
  }
3410
3672
  ];
3411
3673
  }
3412
- var PLUGIN = ".claude/skills/rness";
3413
- var PLUGIN_JSON = `${JSON.stringify(
3414
- {
3415
- name: "rness",
3416
- description: "The rness workspace in Claude Code: /rness:status."
3417
- },
3418
- null,
3419
- 2
3420
- )}
3421
- `;
3422
- function statusSkill(rness) {
3423
- const command = `node "\${CLAUDE_PROJECT_DIR}/${rness}/${PINNED_BIN}" status --cwd "\${CLAUDE_PROJECT_DIR}"`;
3424
- return `---
3425
- name: status
3426
- description: The status of every decision, specification, plan and other tracked document of the rness workspace, one table per directory. Read-only.
3427
- argument-hint: '[tab]'
3428
- disable-model-invocation: true
3429
- allowed-tools: Bash(${command})
3430
- ---
3431
-
3432
- !\`${command}\`
3433
-
3434
- Show the output above to the user as it is: the heading and the tables,
3435
- nothing added, nothing summarised, no other tool. Arguments: \`$ARGUMENTS\`.
3436
- When they name a tab, show only that tab's section. If the output says the
3437
- module cannot be found, say instead that ${rness} is not installed next to
3438
- this repository.
3439
-
3440
- End with this line: _For the view with tabs and scrolling: Ctrl+Z, then
3441
- \`npx @rness/cli status\` (q to close), then \`fg\`._
3442
- `;
3443
- }
3444
- function plugin(rness, at) {
3445
- return [
3446
- { file: `${PLUGIN}/.claude-plugin/plugin.json`, at, content: PLUGIN_JSON },
3447
- {
3448
- file: `${PLUGIN}/skills/status/SKILL.md`,
3449
- at,
3450
- content: statusSkill(rness)
3451
- }
3452
- ];
3453
- }
3454
3674
  var TARGETS = {
3455
3675
  claude: {
3456
3676
  name: "claude",
3457
3677
  label: "Claude Code",
3458
- files: [
3678
+ files: (ctx) => [
3459
3679
  {
3460
3680
  file: ".claude/settings.json",
3461
3681
  at: "clones",
@@ -3491,7 +3711,7 @@ var TARGETS = {
3491
3711
  }
3492
3712
  ]
3493
3713
  },
3494
- ...plugin("../../.rness", "clones"),
3714
+ ...pluginFiles("../../.rness", "clones", ctx),
3495
3715
  // A session opened at the workspace root reads this file, one opened
3496
3716
  // in a clone does not (spec 0011 §1): the hooks only, `.rness` inside.
3497
3717
  {
@@ -3499,7 +3719,7 @@ var TARGETS = {
3499
3719
  at: "root",
3500
3720
  guarantees: hooks(".rness")
3501
3721
  },
3502
- ...plugin(".rness", "root")
3722
+ ...pluginFiles(".rness", "root", ctx)
3503
3723
  ]
3504
3724
  }
3505
3725
  };
@@ -3613,30 +3833,288 @@ function describeGuarantee(g) {
3613
3833
  return `${g.path.join(".")} lacks ${what}`;
3614
3834
  }
3615
3835
 
3836
+ // src/core/pinned.ts
3837
+ import { execFile as execFile3 } from "child_process";
3838
+ import { readFile as readFile7 } from "fs/promises";
3839
+ import { basename as basename3, join as join12 } from "path";
3840
+ import { promisify as promisify3 } from "util";
3841
+ var execFileP3 = promisify3(execFile3);
3842
+ var PACKAGE = "@rness/cli";
3843
+ var EXACT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
3844
+ function compareVersions(a, b) {
3845
+ const split = (v) => {
3846
+ const dash = v.indexOf("-");
3847
+ const core = dash === -1 ? v : v.slice(0, dash);
3848
+ return [core.split(".").map(Number), dash === -1 ? null : v.slice(dash + 1)];
3849
+ };
3850
+ const [coreA, preA] = split(a);
3851
+ const [coreB, preB] = split(b);
3852
+ for (let i = 0; i < 3; i += 1) {
3853
+ const diff = (coreA[i] ?? 0) - (coreB[i] ?? 0);
3854
+ if (diff !== 0) return diff;
3855
+ }
3856
+ if (preA === preB) return 0;
3857
+ if (preA === null) return 1;
3858
+ if (preB === null) return -1;
3859
+ return preA.localeCompare(preB, "en", { numeric: true });
3860
+ }
3861
+ async function readPackageJson(rnessDir) {
3862
+ try {
3863
+ const parsed = JSON.parse(
3864
+ await readFile7(join12(rnessDir, "package.json"), "utf8")
3865
+ );
3866
+ return parsed !== null && typeof parsed === "object" ? parsed : null;
3867
+ } catch {
3868
+ return null;
3869
+ }
3870
+ }
3871
+ async function readPin(rnessDir) {
3872
+ const pkg = await readPackageJson(rnessDir);
3873
+ if (pkg === null) return null;
3874
+ for (const field of ["devDependencies", "dependencies"]) {
3875
+ const deps = pkg[field];
3876
+ if (deps === null || typeof deps !== "object") continue;
3877
+ const spec = deps[PACKAGE];
3878
+ if (typeof spec === "string") return { spec, field };
3879
+ }
3880
+ return null;
3881
+ }
3882
+ async function writePin(rnessDir, from, to) {
3883
+ const file = join12(rnessDir, "package.json");
3884
+ const text = await readFile7(file, "utf8");
3885
+ const entry = new RegExp(
3886
+ `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
3887
+ );
3888
+ if (!entry.test(text))
3889
+ throw new Error(`"${PACKAGE}": "${from}" not found in package.json`);
3890
+ await writeFileAtomic(file, text.replace(entry, `$1${to}$2`));
3891
+ }
3892
+ var LOCKFILES = [
3893
+ ["pnpm-lock.yaml", "pnpm"],
3894
+ ["yarn.lock", "yarn"],
3895
+ ["bun.lock", "bun"],
3896
+ ["bun.lockb", "bun"],
3897
+ ["package-lock.json", "npm"]
3898
+ ];
3899
+ async function workspacePackageManager(rnessDir) {
3900
+ const declared2 = (await readPackageJson(rnessDir))?.["packageManager"];
3901
+ if (typeof declared2 === "string") {
3902
+ const name = declared2.split("@")[0] ?? "";
3903
+ if (isPackageManager(name)) return name;
3904
+ }
3905
+ for (const [lockfile, pm] of LOCKFILES)
3906
+ if (await exists(join12(rnessDir, lockfile))) return pm;
3907
+ return "npm";
3908
+ }
3909
+ async function hasLockfile(rnessDir, pm) {
3910
+ for (const [lockfile, owner] of LOCKFILES)
3911
+ if (owner === pm && await exists(join12(rnessDir, lockfile))) return true;
3912
+ return false;
3913
+ }
3914
+ async function pinDrift(rnessDir) {
3915
+ const pin = await readPin(rnessDir);
3916
+ if (pin === null || !EXACT_VERSION.test(pin.spec)) return null;
3917
+ const installed = (await readPinnedCli(rnessDir))?.version ?? null;
3918
+ return installed === pin.spec ? null : { pin: pin.spec, installed };
3919
+ }
3920
+ async function driftWarning(cwd) {
3921
+ let root;
3922
+ let rnessDir;
3923
+ try {
3924
+ const ws = await findWorkspace(cwd);
3925
+ root = ws.root;
3926
+ rnessDir = ws.rnessDir;
3927
+ } catch {
3928
+ return null;
3929
+ }
3930
+ const drift = await pinDrift(rnessDir);
3931
+ if (drift === null) return null;
3932
+ const pm = await workspacePackageManager(rnessDir);
3933
+ return warn(
3934
+ `${basename3(root)}/.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${pm} install in .rness`
3935
+ );
3936
+ }
3937
+ var CHILD_DEADLINE_MS = 12e4;
3938
+ function childEnv(env = process.env) {
3939
+ return {
3940
+ ...env,
3941
+ GIT_SSH_COMMAND: `${env["GIT_SSH_COMMAND"] ?? "ssh"} -o BatchMode=yes`,
3942
+ GIT_TERMINAL_PROMPT: "0"
3943
+ };
3944
+ }
3945
+ async function ensureInstalled(rnessDir, opts = {}) {
3946
+ const drift = await pinDrift(rnessDir);
3947
+ if (drift === null) return null;
3948
+ const env = opts.env ?? process.env;
3949
+ if (env["RNESS_NO_INSTALL"] === "1")
3950
+ return { installed: false, pin: drift.pin, reason: "RNESS_NO_INSTALL=1" };
3951
+ const pm = await workspacePackageManager(rnessDir);
3952
+ const frozen = await hasLockfile(rnessDir, pm);
3953
+ const install = opts.install ?? installDependencies;
3954
+ opts.announce?.(drift.pin);
3955
+ try {
3956
+ await install(pm, rnessDir, { frozen });
3957
+ return { installed: true, pin: drift.pin, reason: null };
3958
+ } catch (e) {
3959
+ return {
3960
+ installed: false,
3961
+ pin: drift.pin,
3962
+ reason: e instanceof Error ? e.message : String(e)
3963
+ };
3964
+ }
3965
+ }
3966
+ async function runPinnedSync(root) {
3967
+ const installed = await readPinnedCli(join12(root, ".rness"));
3968
+ if (installed === null) return null;
3969
+ const pinned = join12(installed.dir, ...installed.bin.split("/"));
3970
+ if (!await exists(pinned))
3971
+ return {
3972
+ code: 1,
3973
+ stdout: "",
3974
+ stderr: `@rness/cli in .rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
3975
+ `
3976
+ };
3977
+ try {
3978
+ const { stdout, stderr } = await execFileP3(
3979
+ process.execPath,
3980
+ [pinned, "sync", "--yes"],
3981
+ {
3982
+ cwd: root,
3983
+ maxBuffer: 16 * 1024 * 1024,
3984
+ env: childEnv(),
3985
+ timeout: CHILD_DEADLINE_MS,
3986
+ killSignal: "SIGKILL"
3987
+ }
3988
+ );
3989
+ return { code: 0, stdout, stderr };
3990
+ } catch (e) {
3991
+ const err = e;
3992
+ const deadline = err.killed === true ? `rness sync in .rness did not finish within ${CHILD_DEADLINE_MS / 1e3}s and was stopped
3993
+ ` : "";
3994
+ return {
3995
+ code: typeof err.code === "number" ? err.code : 1,
3996
+ stdout: err.stdout ?? "",
3997
+ stderr: `${err.stderr ?? ""}${deadline}`
3998
+ };
3999
+ }
4000
+ }
4001
+ var SYNC_VERBS = ["updated", "unchanged", "stale"];
4002
+ function counted(noun, counts) {
4003
+ const total = [...counts.values()].reduce((a, b) => a + b, 0);
4004
+ const detail = SYNC_VERBS.filter((verb) => counts.has(verb)).map((verb) => `${counts.get(verb) ?? 0} ${verb}`).join(", ");
4005
+ return `${total} ${noun}${total === 1 ? "" : "s"}${detail === "" ? "" : `: ${detail}`}`;
4006
+ }
4007
+ function summariseSync(stdout) {
4008
+ const blocks = /* @__PURE__ */ new Map();
4009
+ const agentFiles = /* @__PURE__ */ new Map();
4010
+ const others = [];
4011
+ for (const line3 of stdout.split("\n")) {
4012
+ if (line3.trim() === "") continue;
4013
+ const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line3);
4014
+ if (m === null) {
4015
+ others.push(line3);
4016
+ continue;
4017
+ }
4018
+ const [, verb = "", path = ""] = m;
4019
+ const counts = /(^|\/)(AGENTS|CLAUDE)\.md$/.test(path) ? blocks : agentFiles;
4020
+ counts.set(verb, (counts.get(verb) ?? 0) + 1);
4021
+ }
4022
+ return {
4023
+ summary: [
4024
+ counted("block", blocks),
4025
+ ...agentFiles.size > 0 ? [counted("agent file", agentFiles)] : []
4026
+ ].join("; "),
4027
+ others
4028
+ };
4029
+ }
4030
+ function syncOutcome(result, opts = {}) {
4031
+ const { others } = summariseSync(result.stdout);
4032
+ const wrote = /^(updated|unchanged|stale)\s/m.test(result.stdout);
4033
+ const stderr = result.stderr.split("\n").map((l) => l.trimEnd()).filter((l) => l.trim() !== "");
4034
+ const hints = wrote ? [...others, ...stderr] : others;
4035
+ if (opts.sshWorks === true && /Permission denied \(publickey\)/.test(result.stderr))
4036
+ hints.push(...agentHintLines());
4037
+ return {
4038
+ code: wrote ? 0 : result.code,
4039
+ hints,
4040
+ errors: wrote ? [] : stderr
4041
+ };
4042
+ }
4043
+ async function syncPinned(root, shown, fallback) {
4044
+ const rnessDir = join12(root, ".rness");
4045
+ const installed = await readPinnedCli(rnessDir);
4046
+ if (installed === null) {
4047
+ if (fallback !== void 0) return fallback();
4048
+ process.stderr.write(
4049
+ `@rness/cli is not installed in ${shown}/.rness; install in .rness/, then ${rnessCommand()} sync
4050
+ `
4051
+ );
4052
+ return 1;
4053
+ }
4054
+ const pinned = join12(installed.dir, ...installed.bin.split("/"));
4055
+ if (!await exists(pinned)) {
4056
+ process.stderr.write(
4057
+ `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
4058
+ `
4059
+ );
4060
+ return 1;
4061
+ }
4062
+ try {
4063
+ const { stdout, stderr } = await execFileP3(
4064
+ process.execPath,
4065
+ [pinned, "sync", "--yes"],
4066
+ {
4067
+ cwd: root,
4068
+ maxBuffer: 16 * 1024 * 1024,
4069
+ env: childEnv(),
4070
+ timeout: CHILD_DEADLINE_MS,
4071
+ killSignal: "SIGKILL"
4072
+ }
4073
+ );
4074
+ process.stdout.write(stdout);
4075
+ if (stderr !== "") process.stderr.write(stderr);
4076
+ return 0;
4077
+ } catch (e) {
4078
+ const err = e;
4079
+ if (err.stdout !== void 0 && err.stdout !== "")
4080
+ process.stdout.write(err.stdout);
4081
+ if (err.stderr !== void 0 && err.stderr !== "")
4082
+ process.stderr.write(err.stderr);
4083
+ return typeof err.code === "number" ? err.code : 1;
4084
+ }
4085
+ }
4086
+
3616
4087
  // src/core/agent-targets.ts
3617
4088
  function wholeDiff(existing, content) {
3618
4089
  if (existing === content) return null;
3619
4090
  return existing === null ? "missing" : "differs from what rness writes";
3620
4091
  }
4092
+ async function contextOf(root) {
4093
+ return {
4094
+ packageManager: await workspacePackageManager(join13(root, ".rness"))
4095
+ };
4096
+ }
4097
+ var PATHS_ONLY = { packageManager: "npm" };
3621
4098
  async function filesOf(root, manifest, targets) {
3622
4099
  const files = [];
3623
- if (!await exists(join11(root, "org"))) return files;
4100
+ if (!await exists(join13(root, "org"))) return files;
4101
+ const ctx = await contextOf(root);
3624
4102
  for (const target2 of targets)
3625
- for (const owned of target2.files)
4103
+ for (const owned of target2.files(ctx))
3626
4104
  if (owned.at === "root")
3627
4105
  files.push({
3628
4106
  ...owned,
3629
- file: join11(root, ...owned.file.split("/")),
4107
+ file: join13(root, ...owned.file.split("/")),
3630
4108
  label: owned.file
3631
4109
  });
3632
4110
  for (const repo of Object.keys(manifest.repos)) {
3633
- if (!await exists(join11(root, "org", repo))) continue;
4111
+ if (!await exists(join13(root, "org", repo))) continue;
3634
4112
  for (const target2 of targets)
3635
- for (const owned of target2.files)
4113
+ for (const owned of target2.files(ctx))
3636
4114
  if (owned.at === "clones")
3637
4115
  files.push({
3638
4116
  ...owned,
3639
- file: join11(root, "org", repo, ...owned.file.split("/")),
4117
+ file: join13(root, "org", repo, ...owned.file.split("/")),
3640
4118
  label: `org/${repo}/${owned.file}`
3641
4119
  });
3642
4120
  }
@@ -3652,7 +4130,7 @@ function writtenFiles(manifest) {
3652
4130
  return [
3653
4131
  ...BLOCK_FILES,
3654
4132
  ...declared(manifest).flatMap(
3655
- (t) => t.files.filter((f) => f.at === "clones").map((f) => f.file)
4133
+ (t) => t.files(PATHS_ONLY).filter((f) => f.at === "clones").map((f) => f.file)
3656
4134
  )
3657
4135
  ];
3658
4136
  }
@@ -3960,7 +4438,7 @@ function targetsOf(manifest, root, only) {
3960
4438
  const segments = entry.path.split("/");
3961
4439
  targets.push({
3962
4440
  scope: name,
3963
- dir: join12(root, ...segments),
4441
+ dir: join14(root, ...segments),
3964
4442
  depth: segments.length,
3965
4443
  label: `${entry.path}/AGENTS.md`
3966
4444
  });
@@ -4119,7 +4597,7 @@ async function syncCommand(opts, deps = {}) {
4119
4597
  try {
4120
4598
  const notCloned = /* @__PURE__ */ new Set();
4121
4599
  for (const [name, repo] of Object.entries(manifest.repos)) {
4122
- const dir = join12(ws.root, "org", name);
4600
+ const dir = join14(ws.root, "org", name);
4123
4601
  const label = `org/${name}`;
4124
4602
  if (!await exists(dir)) {
4125
4603
  if (!toClone.has(name)) {
@@ -4171,7 +4649,7 @@ async function syncCommand(opts, deps = {}) {
4171
4649
  lines.push(
4172
4650
  `not in rness.json: ${undeclared.join(", ")} (${rnessCommand()} add <name> declares it)`
4173
4651
  );
4174
- if (!await exists(join12(ws.root, "org"))) {
4652
+ if (!await exists(join14(ws.root, "org"))) {
4175
4653
  lines.push(["skipped", "blocks (no org/ directory here)"]);
4176
4654
  } else {
4177
4655
  let differences = 0;
@@ -4183,7 +4661,7 @@ async function syncCommand(opts, deps = {}) {
4183
4661
  continue;
4184
4662
  }
4185
4663
  const [agentsLinked, claudeLinked] = await Promise.all(
4186
- BLOCK_FILES.map((f) => isSymlink(join12(t.dir, f)))
4664
+ BLOCK_FILES.map((f) => isSymlink(join14(t.dir, f)))
4187
4665
  );
4188
4666
  const atRoot = t.scope === null;
4189
4667
  if (!atRoot && (agentsLinked === true || claudeLinked === true)) {
@@ -4206,7 +4684,7 @@ async function syncCommand(opts, deps = {}) {
4206
4684
  `${t.label}: block is ${Math.round(block.bytes / 1024)} KB (over 32 KB)`
4207
4685
  );
4208
4686
  }
4209
- const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join12(t.dir, "AGENTS.md"), block, check);
4687
+ const agents = agentsLinked === true ? "skipped" : await syncBlockFile(join14(t.dir, "AGENTS.md"), block, check);
4210
4688
  if (typeof agents === "object") {
4211
4689
  problems.push(`${t.label}: ${agents.error}`);
4212
4690
  continue;
@@ -4221,7 +4699,7 @@ async function syncCommand(opts, deps = {}) {
4221
4699
  }
4222
4700
  if (claudeLinked === true) continue;
4223
4701
  const claude = await syncBlockFile(
4224
- join12(t.dir, "CLAUDE.md"),
4702
+ join14(t.dir, "CLAUDE.md"),
4225
4703
  block,
4226
4704
  check,
4227
4705
  true
@@ -4278,7 +4756,7 @@ async function syncCommand(opts, deps = {}) {
4278
4756
  }
4279
4757
 
4280
4758
  // src/commands/add.ts
4281
- import { basename as basename3, join as join13 } from "path";
4759
+ import { basename as basename4, join as join15 } from "path";
4282
4760
  function printDeclared(ui, result, already) {
4283
4761
  for (const name of result.declared) {
4284
4762
  if (already.has(name)) continue;
@@ -4313,7 +4791,7 @@ async function addCommand(spec, opts, deps = {}) {
4313
4791
  }
4314
4792
  if (parsed.name === CONTEXT_REPO) {
4315
4793
  process.stderr.write(
4316
- `${CONTEXT_REPO} is the workspace context, not a member repository \u2014 it is already cloned at ${basename3(ws.root)}/${CONTEXT_REPO}
4794
+ `${CONTEXT_REPO} is the workspace context, not a member repository \u2014 it is already cloned at ${basename4(ws.root)}/${CONTEXT_REPO}
4317
4795
  `
4318
4796
  );
4319
4797
  return 2;
@@ -4371,7 +4849,7 @@ async function addCommand(spec, opts, deps = {}) {
4371
4849
  }
4372
4850
  parsed = parseRepoSpec(spec, org, host);
4373
4851
  const scopes = (opts.scopes ?? "").split(",").map((s) => s.trim()).filter((s) => s !== "");
4374
- const dir = join13(ws.root, "org", parsed.name);
4852
+ const dir = join15(ws.root, "org", parsed.name);
4375
4853
  const present = await exists(dir);
4376
4854
  if (interactive) {
4377
4855
  const { confirm, isCancel } = await terminal.prompts();
@@ -4487,66 +4965,19 @@ async function contextCommand(opts) {
4487
4965
  manifest,
4488
4966
  scope
4489
4967
  });
4490
- process.stdout.write(
4491
- opts.json ? `${JSON.stringify(ctx, null, 2)}
4492
- ` : renderMarkdown(ctx)
4493
- );
4494
- return 0;
4495
- } catch (e) {
4496
- return reportError(e);
4497
- }
4498
- }
4499
-
4500
- // src/core/delegate.ts
4501
- import { readFile as readFile6 } from "fs/promises";
4502
- import { join as join14 } from "path";
4503
- var DEFAULT_BIN = "dist/bin/rness.js";
4504
- async function readPinnedCli(rnessDir) {
4505
- const dir = join14(rnessDir, "node_modules", "@rness", "cli");
4506
- let pkg;
4507
- try {
4508
- pkg = JSON.parse(await readFile6(join14(dir, "package.json"), "utf8"));
4509
- } catch {
4510
- return null;
4511
- }
4512
- const declared2 = pkg.bin;
4513
- let bin = DEFAULT_BIN;
4514
- if (typeof declared2 === "string") bin = declared2;
4515
- else if (declared2 !== null && typeof declared2 === "object") {
4516
- const named = declared2["rness"];
4517
- if (typeof named === "string") bin = named;
4518
- }
4519
- return {
4520
- dir,
4521
- bin,
4522
- version: typeof pkg.version === "string" ? pkg.version : null
4523
- };
4524
- }
4525
- async function pinnedBinPath(rnessDir) {
4526
- const pinned = await readPinnedCli(rnessDir);
4527
- if (pinned === null) return null;
4528
- return join14(pinned.dir, ...pinned.bin.split("/"));
4529
- }
4530
- async function findDelegate(cwd, ownVersion) {
4531
- let root;
4532
- let rnessDir;
4533
- try {
4534
- const ws = await findWorkspace(cwd);
4535
- root = ws.root;
4536
- rnessDir = ws.rnessDir;
4537
- } catch {
4538
- return null;
4968
+ process.stdout.write(
4969
+ opts.json ? `${JSON.stringify(ctx, null, 2)}
4970
+ ` : renderMarkdown(ctx)
4971
+ );
4972
+ return 0;
4973
+ } catch (e) {
4974
+ return reportError(e);
4539
4975
  }
4540
- const pinned = await readPinnedCli(rnessDir);
4541
- if (pinned === null) return null;
4542
- const { version } = pinned;
4543
- if (version === null || version === ownVersion) return null;
4544
- return { entry: join14(pinned.dir, "dist", "index.js"), version, root };
4545
4976
  }
4546
4977
 
4547
4978
  // src/core/scaffold.ts
4548
4979
  import { statSync } from "fs";
4549
- import { dirname as dirname5, join as join15 } from "path";
4980
+ import { dirname as dirname5, join as join16 } from "path";
4550
4981
  import { fileURLToPath } from "url";
4551
4982
  var SCAFFOLD_FILES = [
4552
4983
  "AGENTS.md",
@@ -4570,9 +5001,9 @@ var SCAFFOLD_FILES = [
4570
5001
  function scaffoldDir() {
4571
5002
  let dir = dirname5(fileURLToPath(import.meta.url));
4572
5003
  for (let i = 0; i < 4; i += 1) {
4573
- const candidate = join15(dir, "scaffold");
5004
+ const candidate = join16(dir, "scaffold");
4574
5005
  try {
4575
- if (statSync(join15(candidate, "rness.json")).isFile()) return candidate;
5006
+ if (statSync(join16(candidate, "rness.json")).isFile()) return candidate;
4576
5007
  } catch {
4577
5008
  }
4578
5009
  dir = dirname5(dir);
@@ -4581,15 +5012,15 @@ function scaffoldDir() {
4581
5012
  }
4582
5013
 
4583
5014
  // src/core/scaffold-copy.ts
4584
- import { chmod as chmod2, mkdir as mkdir3, readFile as readFile7, stat as stat2, writeFile as writeFile2 } from "fs/promises";
4585
- import { dirname as dirname6, join as join16 } from "path";
5015
+ import { chmod as chmod2, mkdir as mkdir3, readFile as readFile8, stat as stat2, writeFile as writeFile2 } from "fs/promises";
5016
+ import { dirname as dirname6, join as join17 } from "path";
4586
5017
  var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
4587
5018
  async function renderScaffold(source, tokens) {
4588
5019
  const files = [];
4589
5020
  for (const rel of SCAFFOLD_FILES) {
4590
5021
  if (rel === "rness.json") continue;
4591
- const from = join16(source, ...rel.split("/"));
4592
- let content = await readFile7(from, "utf8");
5022
+ const from = join17(source, ...rel.split("/"));
5023
+ let content = await readFile8(from, "utf8");
4593
5024
  if (TOKENISED.has(rel)) {
4594
5025
  content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
4595
5026
  }
@@ -4603,7 +5034,7 @@ async function renderScaffold(source, tokens) {
4603
5034
  }
4604
5035
  async function copyScaffold(dest, tokens) {
4605
5036
  for (const file of await renderScaffold(scaffoldDir(), tokens)) {
4606
- const to = join16(dest, ...file.path.split("/"));
5037
+ const to = join17(dest, ...file.path.split("/"));
4607
5038
  await mkdir3(dirname6(to), { recursive: true });
4608
5039
  await writeFile2(to, file.content, "utf8");
4609
5040
  if (file.executable) await chmod2(to, 493);
@@ -4615,16 +5046,16 @@ import { spawn as spawn3 } from "child_process";
4615
5046
  import { realpath } from "fs/promises";
4616
5047
 
4617
5048
  // src/core/setup-git.ts
4618
- import { execFile as execFile3 } from "child_process";
4619
- import { promisify as promisify3 } from "util";
4620
- var execFileP3 = promisify3(execFile3);
5049
+ import { execFile as execFile4 } from "child_process";
5050
+ import { promisify as promisify4 } from "util";
5051
+ var execFileP4 = promisify4(execFile4);
4621
5052
  var KEY = "credential.https://github.com.helper";
4622
5053
  function helperValue(node, bin) {
4623
5054
  return `!'${node}' '${bin}' git-credential`;
4624
5055
  }
4625
5056
  var isOurs = (value) => /^!'[^']+' '[^']*rness[^']*' git-credential$/.test(value);
4626
5057
  async function config(args) {
4627
- const { stdout } = await execFileP3("git", ["config", "--global", ...args]);
5058
+ const { stdout } = await execFileP4("git", ["config", "--global", ...args]);
4628
5059
  return stdout;
4629
5060
  }
4630
5061
  async function values() {
@@ -4802,257 +5233,6 @@ async function askProvider(p) {
4802
5233
  import { mkdir as mkdir4, readdir as readdir3, rm as rm5, stat as stat3 } from "fs/promises";
4803
5234
  import { dirname as dirname8, join as join19, relative as relative2 } from "path";
4804
5235
 
4805
- // src/core/pinned.ts
4806
- import { execFile as execFile4 } from "child_process";
4807
- import { readFile as readFile8 } from "fs/promises";
4808
- import { basename as basename4, join as join17 } from "path";
4809
- import { promisify as promisify4 } from "util";
4810
- var execFileP4 = promisify4(execFile4);
4811
- var PACKAGE = "@rness/cli";
4812
- var EXACT_VERSION = /^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/;
4813
- function compareVersions(a, b) {
4814
- const split = (v) => {
4815
- const dash = v.indexOf("-");
4816
- const core = dash === -1 ? v : v.slice(0, dash);
4817
- return [core.split(".").map(Number), dash === -1 ? null : v.slice(dash + 1)];
4818
- };
4819
- const [coreA, preA] = split(a);
4820
- const [coreB, preB] = split(b);
4821
- for (let i = 0; i < 3; i += 1) {
4822
- const diff = (coreA[i] ?? 0) - (coreB[i] ?? 0);
4823
- if (diff !== 0) return diff;
4824
- }
4825
- if (preA === preB) return 0;
4826
- if (preA === null) return 1;
4827
- if (preB === null) return -1;
4828
- return preA.localeCompare(preB, "en", { numeric: true });
4829
- }
4830
- async function readPackageJson(rnessDir) {
4831
- try {
4832
- const parsed = JSON.parse(
4833
- await readFile8(join17(rnessDir, "package.json"), "utf8")
4834
- );
4835
- return parsed !== null && typeof parsed === "object" ? parsed : null;
4836
- } catch {
4837
- return null;
4838
- }
4839
- }
4840
- async function readPin(rnessDir) {
4841
- const pkg = await readPackageJson(rnessDir);
4842
- if (pkg === null) return null;
4843
- for (const field of ["devDependencies", "dependencies"]) {
4844
- const deps = pkg[field];
4845
- if (deps === null || typeof deps !== "object") continue;
4846
- const spec = deps[PACKAGE];
4847
- if (typeof spec === "string") return { spec, field };
4848
- }
4849
- return null;
4850
- }
4851
- async function writePin(rnessDir, from, to) {
4852
- const file = join17(rnessDir, "package.json");
4853
- const text = await readFile8(file, "utf8");
4854
- const entry = new RegExp(
4855
- `("${PACKAGE}"\\s*:\\s*")${from.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(")`
4856
- );
4857
- if (!entry.test(text))
4858
- throw new Error(`"${PACKAGE}": "${from}" not found in package.json`);
4859
- await writeFileAtomic(file, text.replace(entry, `$1${to}$2`));
4860
- }
4861
- var LOCKFILES = [
4862
- ["pnpm-lock.yaml", "pnpm"],
4863
- ["yarn.lock", "yarn"],
4864
- ["bun.lock", "bun"],
4865
- ["bun.lockb", "bun"],
4866
- ["package-lock.json", "npm"]
4867
- ];
4868
- async function workspacePackageManager(rnessDir) {
4869
- const declared2 = (await readPackageJson(rnessDir))?.["packageManager"];
4870
- if (typeof declared2 === "string") {
4871
- const name = declared2.split("@")[0] ?? "";
4872
- if (isPackageManager(name)) return name;
4873
- }
4874
- for (const [lockfile, pm] of LOCKFILES)
4875
- if (await exists(join17(rnessDir, lockfile))) return pm;
4876
- return "npm";
4877
- }
4878
- async function hasLockfile(rnessDir, pm) {
4879
- for (const [lockfile, owner] of LOCKFILES)
4880
- if (owner === pm && await exists(join17(rnessDir, lockfile))) return true;
4881
- return false;
4882
- }
4883
- async function pinDrift(rnessDir) {
4884
- const pin = await readPin(rnessDir);
4885
- if (pin === null || !EXACT_VERSION.test(pin.spec)) return null;
4886
- const installed = (await readPinnedCli(rnessDir))?.version ?? null;
4887
- return installed === pin.spec ? null : { pin: pin.spec, installed };
4888
- }
4889
- async function driftWarning(cwd) {
4890
- let root;
4891
- let rnessDir;
4892
- try {
4893
- const ws = await findWorkspace(cwd);
4894
- root = ws.root;
4895
- rnessDir = ws.rnessDir;
4896
- } catch {
4897
- return null;
4898
- }
4899
- const drift = await pinDrift(rnessDir);
4900
- if (drift === null) return null;
4901
- const pm = await workspacePackageManager(rnessDir);
4902
- return warn(
4903
- `${basename4(root)}/.rness pins @rness/cli ${drift.pin} but ${drift.installed ?? "nothing"} is installed \u2014 run ${pm} install in .rness`
4904
- );
4905
- }
4906
- var CHILD_DEADLINE_MS = 12e4;
4907
- function childEnv(env = process.env) {
4908
- return {
4909
- ...env,
4910
- GIT_SSH_COMMAND: `${env["GIT_SSH_COMMAND"] ?? "ssh"} -o BatchMode=yes`,
4911
- GIT_TERMINAL_PROMPT: "0"
4912
- };
4913
- }
4914
- async function ensureInstalled(rnessDir, opts = {}) {
4915
- const drift = await pinDrift(rnessDir);
4916
- if (drift === null) return null;
4917
- const env = opts.env ?? process.env;
4918
- if (env["RNESS_NO_INSTALL"] === "1")
4919
- return { installed: false, pin: drift.pin, reason: "RNESS_NO_INSTALL=1" };
4920
- const pm = await workspacePackageManager(rnessDir);
4921
- const frozen = await hasLockfile(rnessDir, pm);
4922
- const install = opts.install ?? installDependencies;
4923
- opts.announce?.(drift.pin);
4924
- try {
4925
- await install(pm, rnessDir, { frozen });
4926
- return { installed: true, pin: drift.pin, reason: null };
4927
- } catch (e) {
4928
- return {
4929
- installed: false,
4930
- pin: drift.pin,
4931
- reason: e instanceof Error ? e.message : String(e)
4932
- };
4933
- }
4934
- }
4935
- async function runPinnedSync(root) {
4936
- const installed = await readPinnedCli(join17(root, ".rness"));
4937
- if (installed === null) return null;
4938
- const pinned = join17(installed.dir, ...installed.bin.split("/"));
4939
- if (!await exists(pinned))
4940
- return {
4941
- code: 1,
4942
- stdout: "",
4943
- stderr: `@rness/cli in .rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
4944
- `
4945
- };
4946
- try {
4947
- const { stdout, stderr } = await execFileP4(
4948
- process.execPath,
4949
- [pinned, "sync", "--yes"],
4950
- {
4951
- cwd: root,
4952
- maxBuffer: 16 * 1024 * 1024,
4953
- env: childEnv(),
4954
- timeout: CHILD_DEADLINE_MS,
4955
- killSignal: "SIGKILL"
4956
- }
4957
- );
4958
- return { code: 0, stdout, stderr };
4959
- } catch (e) {
4960
- const err = e;
4961
- const deadline = err.killed === true ? `rness sync in .rness did not finish within ${CHILD_DEADLINE_MS / 1e3}s and was stopped
4962
- ` : "";
4963
- return {
4964
- code: typeof err.code === "number" ? err.code : 1,
4965
- stdout: err.stdout ?? "",
4966
- stderr: `${err.stderr ?? ""}${deadline}`
4967
- };
4968
- }
4969
- }
4970
- var SYNC_VERBS = ["updated", "unchanged", "stale"];
4971
- function counted(noun, counts) {
4972
- const total = [...counts.values()].reduce((a, b) => a + b, 0);
4973
- const detail = SYNC_VERBS.filter((verb) => counts.has(verb)).map((verb) => `${counts.get(verb) ?? 0} ${verb}`).join(", ");
4974
- return `${total} ${noun}${total === 1 ? "" : "s"}${detail === "" ? "" : `: ${detail}`}`;
4975
- }
4976
- function summariseSync(stdout) {
4977
- const blocks = /* @__PURE__ */ new Map();
4978
- const agentFiles = /* @__PURE__ */ new Map();
4979
- const others = [];
4980
- for (const line3 of stdout.split("\n")) {
4981
- if (line3.trim() === "") continue;
4982
- const m = /^(updated|unchanged|stale)\s+(\S+)/.exec(line3);
4983
- if (m === null) {
4984
- others.push(line3);
4985
- continue;
4986
- }
4987
- const [, verb = "", path = ""] = m;
4988
- const counts = /(^|\/)(AGENTS|CLAUDE)\.md$/.test(path) ? blocks : agentFiles;
4989
- counts.set(verb, (counts.get(verb) ?? 0) + 1);
4990
- }
4991
- return {
4992
- summary: [
4993
- counted("block", blocks),
4994
- ...agentFiles.size > 0 ? [counted("agent file", agentFiles)] : []
4995
- ].join("; "),
4996
- others
4997
- };
4998
- }
4999
- function syncOutcome(result, opts = {}) {
5000
- const { others } = summariseSync(result.stdout);
5001
- const wrote = /^(updated|unchanged|stale)\s/m.test(result.stdout);
5002
- const stderr = result.stderr.split("\n").map((l) => l.trimEnd()).filter((l) => l.trim() !== "");
5003
- const hints = wrote ? [...others, ...stderr] : others;
5004
- if (opts.sshWorks === true && /Permission denied \(publickey\)/.test(result.stderr))
5005
- hints.push(...agentHintLines());
5006
- return {
5007
- code: wrote ? 0 : result.code,
5008
- hints,
5009
- errors: wrote ? [] : stderr
5010
- };
5011
- }
5012
- async function syncPinned(root, shown, fallback) {
5013
- const rnessDir = join17(root, ".rness");
5014
- const installed = await readPinnedCli(rnessDir);
5015
- if (installed === null) {
5016
- if (fallback !== void 0) return fallback();
5017
- process.stderr.write(
5018
- `@rness/cli is not installed in ${shown}/.rness; install in .rness/, then ${rnessCommand()} sync
5019
- `
5020
- );
5021
- return 1;
5022
- }
5023
- const pinned = join17(installed.dir, ...installed.bin.split("/"));
5024
- if (!await exists(pinned)) {
5025
- process.stderr.write(
5026
- `@rness/cli in ${shown}/.rness is not installed correctly (missing ${installed.bin}); reinstall in .rness/
5027
- `
5028
- );
5029
- return 1;
5030
- }
5031
- try {
5032
- const { stdout, stderr } = await execFileP4(
5033
- process.execPath,
5034
- [pinned, "sync", "--yes"],
5035
- {
5036
- cwd: root,
5037
- maxBuffer: 16 * 1024 * 1024,
5038
- env: childEnv(),
5039
- timeout: CHILD_DEADLINE_MS,
5040
- killSignal: "SIGKILL"
5041
- }
5042
- );
5043
- process.stdout.write(stdout);
5044
- if (stderr !== "") process.stderr.write(stderr);
5045
- return 0;
5046
- } catch (e) {
5047
- const err = e;
5048
- if (err.stdout !== void 0 && err.stdout !== "")
5049
- process.stdout.write(err.stdout);
5050
- if (err.stderr !== void 0 && err.stderr !== "")
5051
- process.stderr.write(err.stderr);
5052
- return typeof err.code === "number" ? err.code : 1;
5053
- }
5054
- }
5055
-
5056
5236
  // src/core/scaffold-git.ts
5057
5237
  import { execFile as execFile5 } from "child_process";
5058
5238
  import { mkdtemp, realpath as realpath2, rm as rm4, writeFile as writeFile3 } from "fs/promises";
@@ -8353,6 +8533,7 @@ export {
8353
8533
  defaultTransport,
8354
8534
  findWorkspace,
8355
8535
  reportError,
8536
+ findDelegate,
8356
8537
  BEGIN,
8357
8538
  END,
8358
8539
  BLOCK_SIZE_WARNING,
@@ -8367,7 +8548,6 @@ export {
8367
8548
  addCommand,
8368
8549
  renderMarkdown,
8369
8550
  contextCommand,
8370
- findDelegate,
8371
8551
  SCAFFOLD_FILES,
8372
8552
  scaffoldDir,
8373
8553
  copyScaffold,
@@ -8381,4 +8561,4 @@ export {
8381
8561
  OWN_COMMANDS,
8382
8562
  catchUp
8383
8563
  };
8384
- //# sourceMappingURL=chunk-4VTYSPZW.js.map
8564
+ //# sourceMappingURL=chunk-AXTFDXN2.js.map