@rness/cli 0.7.0 → 0.8.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.
@@ -797,7 +797,7 @@ function classifyProbe(url, exitCode, stderr) {
797
797
  var STDERR_FLUSH_MS = 200;
798
798
  async function probeRemote(url, timeoutMs = 6e4, credentials) {
799
799
  positional(url, "repository url");
800
- return new Promise((resolve5) => {
800
+ return new Promise((resolve6) => {
801
801
  const auth = withCredentials(credentials);
802
802
  const child = spawn(
803
803
  "git",
@@ -816,7 +816,7 @@ async function probeRemote(url, timeoutMs = 6e4, credentials) {
816
816
  settled = true;
817
817
  clearTimeout(timer);
818
818
  child.stderr?.destroy();
819
- resolve5(probe);
819
+ resolve6(probe);
820
820
  };
821
821
  const timer = setTimeout(() => {
822
822
  child.kill("SIGKILL");
@@ -827,14 +827,14 @@ async function probeRemote(url, timeoutMs = 6e4, credentials) {
827
827
  }, timeoutMs);
828
828
  child.on("error", (e) => settle(classifyProbe(url, 1, e.message)));
829
829
  child.on("exit", (code) => {
830
- const finish = () => settle(classifyProbe(url, code ?? 1, chunks.join("")));
830
+ const finish2 = () => settle(classifyProbe(url, code ?? 1, chunks.join("")));
831
831
  const stderr = child.stderr;
832
832
  if (stderr === null || stderr.readableEnded || stderr.destroyed)
833
- return finish();
834
- const flush = setTimeout(finish, STDERR_FLUSH_MS);
833
+ return finish2();
834
+ const flush = setTimeout(finish2, STDERR_FLUSH_MS);
835
835
  const done = () => {
836
836
  clearTimeout(flush);
837
- finish();
837
+ finish2();
838
838
  };
839
839
  stderr.once("end", done);
840
840
  stderr.once("error", done);
@@ -1248,7 +1248,7 @@ async function detectGithubSsh(opts) {
1248
1248
  ...opts.interactive ? [] : ["-o", "BatchMode=yes"],
1249
1249
  "git@github.com"
1250
1250
  ];
1251
- return new Promise((resolve5) => {
1251
+ return new Promise((resolve6) => {
1252
1252
  const child = spawn2(opts.command ?? "ssh", args, {
1253
1253
  stdio: [opts.interactive ? "inherit" : "ignore", "ignore", "pipe"]
1254
1254
  });
@@ -1261,7 +1261,7 @@ async function detectGithubSsh(opts) {
1261
1261
  settled = true;
1262
1262
  clearTimeout(timer);
1263
1263
  child.stderr?.destroy();
1264
- resolve5(access2);
1264
+ resolve6(access2);
1265
1265
  };
1266
1266
  const timer = setTimeout(() => {
1267
1267
  child.kill("SIGKILL");
@@ -1269,7 +1269,7 @@ async function detectGithubSsh(opts) {
1269
1269
  }, timeoutMs);
1270
1270
  child.on("error", (e) => settle({ ok: false, reason: e.message }));
1271
1271
  child.on("exit", (code) => {
1272
- const finish = () => {
1272
+ const finish2 = () => {
1273
1273
  const stderr2 = chunks.join("");
1274
1274
  const login = parseSshGreeting(stderr2);
1275
1275
  settle(
@@ -1281,11 +1281,11 @@ async function detectGithubSsh(opts) {
1281
1281
  };
1282
1282
  const stderr = child.stderr;
1283
1283
  if (stderr === null || stderr.readableEnded || stderr.destroyed)
1284
- return finish();
1285
- const flush = setTimeout(finish, STDERR_FLUSH_MS2);
1284
+ return finish2();
1285
+ const flush = setTimeout(finish2, STDERR_FLUSH_MS2);
1286
1286
  const done = () => {
1287
1287
  clearTimeout(flush);
1288
- finish();
1288
+ finish2();
1289
1289
  };
1290
1290
  stderr.once("end", done);
1291
1291
  stderr.once("error", done);
@@ -1390,7 +1390,7 @@ function reportError(e) {
1390
1390
  }
1391
1391
 
1392
1392
  // src/version.ts
1393
- var VERSION = "0.7.0";
1393
+ var VERSION = "0.8.1";
1394
1394
 
1395
1395
  // src/core/block.ts
1396
1396
  import { createHash } from "crypto";
@@ -2030,12 +2030,12 @@ function sessionUi(p) {
2030
2030
  },
2031
2031
  async step(options, work, done) {
2032
2032
  const label = options.sentence ?? tidy(options.doing);
2033
- const finish = (result) => done?.(result) ?? null;
2033
+ const finish2 = (result) => done?.(result) ?? null;
2034
2034
  const label_ = (line) => `${line[1]} ${line[0]}`;
2035
2035
  if (options.git === true && !this.gitIsSilent) {
2036
2036
  p.log.message(paint("idle", `${label}\u2026`));
2037
2037
  const result = await work();
2038
- const line = finish(result);
2038
+ const line = finish2(result);
2039
2039
  if (line !== null) say(line[0], label_(line));
2040
2040
  return result;
2041
2041
  }
@@ -2043,7 +2043,7 @@ function sessionUi(p) {
2043
2043
  spin.start(label);
2044
2044
  try {
2045
2045
  const result = await work();
2046
- const line = finish(result);
2046
+ const line = finish2(result);
2047
2047
  spin.stop(line === null ? label : label_(line));
2048
2048
  return result;
2049
2049
  } catch (e) {
@@ -2710,21 +2710,29 @@ function scaffoldDir() {
2710
2710
  import { chmod as chmod2, mkdir as mkdir3, readFile as readFile7, stat as stat2, writeFile as writeFile2 } from "fs/promises";
2711
2711
  import { dirname as dirname6, join as join14 } from "path";
2712
2712
  var TOKENISED = /* @__PURE__ */ new Set(["package.json"]);
2713
- async function copyScaffold(dest, tokens) {
2714
- const source = scaffoldDir();
2713
+ async function renderScaffold(source, tokens) {
2714
+ const files = [];
2715
2715
  for (const rel of SCAFFOLD_FILES) {
2716
2716
  if (rel === "rness.json") continue;
2717
2717
  const from = join14(source, ...rel.split("/"));
2718
- const targetRel = rel === "_gitignore" ? ".gitignore" : rel;
2719
- const to = join14(dest, ...targetRel.split("/"));
2720
- await mkdir3(dirname6(to), { recursive: true });
2721
2718
  let content = await readFile7(from, "utf8");
2722
2719
  if (TOKENISED.has(rel)) {
2723
2720
  content = content.replaceAll("__RNESS_VERSION__", tokens.version).replaceAll("__RNESS_PM__", tokens.packageManager);
2724
2721
  }
2725
- await writeFile2(to, content, "utf8");
2726
- const mode = (await stat2(from)).mode & 511;
2727
- if (mode & 73) await chmod2(to, mode);
2722
+ files.push({
2723
+ path: rel === "_gitignore" ? ".gitignore" : rel,
2724
+ content,
2725
+ executable: ((await stat2(from)).mode & 73) !== 0
2726
+ });
2727
+ }
2728
+ return files;
2729
+ }
2730
+ async function copyScaffold(dest, tokens) {
2731
+ for (const file of await renderScaffold(scaffoldDir(), tokens)) {
2732
+ const to = join14(dest, ...file.path.split("/"));
2733
+ await mkdir3(dirname6(to), { recursive: true });
2734
+ await writeFile2(to, file.content, "utf8");
2735
+ if (file.executable) await chmod2(to, 493);
2728
2736
  }
2729
2737
  }
2730
2738
 
@@ -2878,13 +2886,13 @@ function revokeUrl() {
2878
2886
  }
2879
2887
 
2880
2888
  // src/commands/create.ts
2881
- import { mkdir as mkdir6, mkdtemp, rename as rename2, rm as rm5 } from "fs/promises";
2882
- import { tmpdir } from "os";
2883
- import { join as join18, relative as relative4, resolve as resolve4 } from "path";
2889
+ import { mkdir as mkdir6, mkdtemp as mkdtemp2, rename as rename2, rm as rm6 } from "fs/promises";
2890
+ import { tmpdir as tmpdir2 } from "os";
2891
+ import { join as join19, relative as relative4, resolve as resolve5 } from "path";
2884
2892
 
2885
2893
  // src/core/new-workspace.ts
2886
- import { mkdir as mkdir4, readdir as readdir2, rm as rm4, stat as stat3 } from "fs/promises";
2887
- import { dirname as dirname7, join as join16, relative as relative2 } from "path";
2894
+ import { mkdir as mkdir4, readdir as readdir2, rm as rm5, stat as stat3 } from "fs/promises";
2895
+ import { dirname as dirname8, join as join17, relative as relative2 } from "path";
2888
2896
 
2889
2897
  // src/core/pinned.ts
2890
2898
  import { execFile as execFile4 } from "child_process";
@@ -3124,6 +3132,184 @@ async function syncPinned(root, shown, fallback) {
3124
3132
  }
3125
3133
  }
3126
3134
 
3135
+ // src/core/scaffold-git.ts
3136
+ import { execFile as execFile5 } from "child_process";
3137
+ import { mkdtemp, realpath as realpath2, rm as rm4, writeFile as writeFile3 } from "fs/promises";
3138
+ import { tmpdir } from "os";
3139
+ import { dirname as dirname7, join as join16, resolve as resolve3 } from "path";
3140
+ import { promisify as promisify5 } from "util";
3141
+ var execFileP5 = promisify5(execFile5);
3142
+ var CREATE_SUBJECT = "chore: rness workspace context";
3143
+ var SCAFFOLD_SUBJECT = /^chore: rness scaffold (\S+)$/;
3144
+ var SCAFFOLD_TRAILER = "Rness-Scaffold";
3145
+ async function run(dir, args, env = {}) {
3146
+ try {
3147
+ const { stdout, stderr } = await execFileP5("git", [...args], {
3148
+ cwd: dir,
3149
+ env: {
3150
+ ...process.env,
3151
+ ...env,
3152
+ GIT_TERMINAL_PROMPT: "0",
3153
+ GIT_CEILING_DIRECTORIES: dirname7(resolve3(dir))
3154
+ },
3155
+ maxBuffer: 16 * 1024 * 1024
3156
+ });
3157
+ return { code: 0, stdout, stderr };
3158
+ } catch (e) {
3159
+ const err = e;
3160
+ return {
3161
+ code: typeof err.code === "number" ? err.code : 1,
3162
+ stdout: err.stdout ?? "",
3163
+ stderr: err.stderr ?? String(e)
3164
+ };
3165
+ }
3166
+ }
3167
+ async function must(dir, args, env = {}) {
3168
+ const r = await run(dir, args, env);
3169
+ if (r.code !== 0) {
3170
+ const first = r.stderr.split("\n").find((l) => l.trim() !== "") ?? "unknown error";
3171
+ throw new Error(`git ${args[0] ?? ""} failed: ${first.trim()}`);
3172
+ }
3173
+ return r.stdout.trim();
3174
+ }
3175
+ function scaffoldMessage(subject, version) {
3176
+ return `${subject}
3177
+
3178
+ ${SCAFFOLD_TRAILER}: ${version}`;
3179
+ }
3180
+ async function findScaffoldBase(rnessDir) {
3181
+ const log = await run(rnessDir, [
3182
+ "log",
3183
+ "--topo-order",
3184
+ `--format=%H%x1f%P%x1f%s%x1f%(trailers:key=${SCAFFOLD_TRAILER},valueonly,separator=%x2C)%x1e`,
3185
+ "HEAD"
3186
+ ]);
3187
+ if (log.code !== 0) return null;
3188
+ const commits = log.stdout.split("").map((r) => r.trim()).filter((r) => r !== "").map((r) => {
3189
+ const [hash = "", parents = "", subject = "", trailer = ""] = r.split("");
3190
+ return { hash, parents: parents.trim(), subject, trailer: trailer.trim() };
3191
+ });
3192
+ for (const c of commits) {
3193
+ const m = SCAFFOLD_SUBJECT.exec(c.subject);
3194
+ if (m !== null) return { commit: c.hash, version: m[1] ?? null };
3195
+ }
3196
+ const root = commits.find(
3197
+ (c) => c.parents === "" && c.subject === CREATE_SUBJECT
3198
+ );
3199
+ if (root === void 0) return null;
3200
+ return {
3201
+ commit: root.hash,
3202
+ version: root.trailer === "" ? null : root.trailer
3203
+ };
3204
+ }
3205
+ async function buildScaffoldCommit(rnessDir, base, files, version) {
3206
+ const scratch = await mkdtemp(join16(tmpdir(), "rness-scaffold-"));
3207
+ const env = { GIT_INDEX_FILE: join16(scratch, "index") };
3208
+ try {
3209
+ await must(
3210
+ rnessDir,
3211
+ ["read-tree", ...base === null ? ["--empty"] : [base]],
3212
+ env
3213
+ );
3214
+ for (const [i, file] of files.entries()) {
3215
+ const tmp = join16(scratch, `blob-${i}`);
3216
+ await writeFile3(tmp, file.content, "utf8");
3217
+ const blob = await must(rnessDir, [
3218
+ "hash-object",
3219
+ "-w",
3220
+ "--no-filters",
3221
+ "--",
3222
+ tmp
3223
+ ]);
3224
+ await must(
3225
+ rnessDir,
3226
+ [
3227
+ "update-index",
3228
+ "--add",
3229
+ "--cacheinfo",
3230
+ `${file.executable ? "100755" : "100644"},${blob},${file.path}`
3231
+ ],
3232
+ env
3233
+ );
3234
+ }
3235
+ const tree = await must(rnessDir, ["write-tree"], env);
3236
+ return await must(rnessDir, [
3237
+ "commit-tree",
3238
+ tree,
3239
+ ...base === null ? [] : ["-p", base],
3240
+ "-m",
3241
+ scaffoldMessage(`chore: rness scaffold ${version}`, version)
3242
+ ]);
3243
+ } finally {
3244
+ await rm4(scratch, { recursive: true, force: true });
3245
+ }
3246
+ }
3247
+ async function blobAt(rnessDir, rev, path) {
3248
+ const r = await run(rnessDir, [
3249
+ "rev-parse",
3250
+ "--verify",
3251
+ "--quiet",
3252
+ `${rev}:${path}`
3253
+ ]);
3254
+ return r.code === 0 ? r.stdout.trim() : null;
3255
+ }
3256
+ async function mergeScaffold(rnessDir, commit, base) {
3257
+ await run(rnessDir, [
3258
+ "merge",
3259
+ "--no-commit",
3260
+ "--no-ff",
3261
+ ...base === null ? ["--allow-unrelated-histories"] : [],
3262
+ commit
3263
+ ]);
3264
+ const conflicts = (await must(rnessDir, ["diff", "--name-only", "--diff-filter=U"])).split("\n").filter((l) => l !== "");
3265
+ const changes = [];
3266
+ const status2 = await must(rnessDir, [
3267
+ "diff",
3268
+ "--cached",
3269
+ "--name-status",
3270
+ "HEAD"
3271
+ ]);
3272
+ for (const line of status2.split("\n")) {
3273
+ const [letter = "", path = ""] = line.split(" ");
3274
+ if (path === "" || conflicts.includes(path)) continue;
3275
+ if (letter === "A") changes.push(["added", path]);
3276
+ else if (letter === "D") changes.push(["removed", path]);
3277
+ else if (letter === "M") {
3278
+ const untouched = base !== null && await blobAt(rnessDir, "HEAD", path) === await blobAt(rnessDir, base, path);
3279
+ changes.push([untouched ? "updated" : "merged", path]);
3280
+ }
3281
+ }
3282
+ return { conflicts, changes };
3283
+ }
3284
+ async function isShallow(rnessDir) {
3285
+ const r = await run(rnessDir, ["rev-parse", "--is-shallow-repository"]);
3286
+ return r.code === 0 && r.stdout.trim() === "true";
3287
+ }
3288
+ async function isRepository2(rnessDir) {
3289
+ const r = await run(rnessDir, ["rev-parse", "--show-toplevel"]);
3290
+ if (r.code !== 0) return false;
3291
+ const real = (p) => realpath2(p).catch(() => resolve3(p));
3292
+ return await real(r.stdout.trim()) === await real(rnessDir);
3293
+ }
3294
+ async function withoutRedundantKeeps(rnessDir, files) {
3295
+ const kept = [];
3296
+ for (const file of files) {
3297
+ if (file.path.endsWith("/.gitkeep")) {
3298
+ const dir = file.path.slice(0, -"/.gitkeep".length);
3299
+ const listed = await run(rnessDir, [
3300
+ "ls-tree",
3301
+ "--name-only",
3302
+ "HEAD",
3303
+ "--",
3304
+ `${dir}/`
3305
+ ]);
3306
+ if (listed.code === 0 && listed.stdout.trim() !== "") continue;
3307
+ }
3308
+ kept.push(file);
3309
+ }
3310
+ return kept;
3311
+ }
3312
+
3127
3313
  // src/core/new-workspace.ts
3128
3314
  async function isEmptyDir(dir) {
3129
3315
  return (await readdir2(dir)).length === 0;
@@ -3138,11 +3324,11 @@ async function insideWorkspace(dir) {
3138
3324
  }
3139
3325
  async function targetProblem(cwd, root, pm) {
3140
3326
  const shown = relative2(cwd, root) || ".";
3141
- if (await insideWorkspace(cwd) || await insideWorkspace(dirname7(root)))
3327
+ if (await insideWorkspace(cwd) || await insideWorkspace(dirname8(root)))
3142
3328
  return "already inside an rness workspace; create runs from outside one";
3143
3329
  if (!await exists(root)) return null;
3144
- if (await exists(join16(root, ".rness", "rness.json"))) {
3145
- if (!await exists(join16(root, ".rness", "node_modules")))
3330
+ if (await exists(join17(root, ".rness", "rness.json"))) {
3331
+ if (!await exists(join17(root, ".rness", "node_modules")))
3146
3332
  return `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then ${rnessCommand()} sync`;
3147
3333
  return `${shown} already holds an rness workspace (.rness/); run ${rnessCommand()} add inside it to add repositories`;
3148
3334
  }
@@ -3165,7 +3351,7 @@ async function installContext(input) {
3165
3351
  }
3166
3352
  async function buildContext(input) {
3167
3353
  const { ui, root, shown, pm } = input;
3168
- const rnessDir = join16(root, ".rness");
3354
+ const rnessDir = join17(root, ".rness");
3169
3355
  const pmVersion = await packageManagerVersion(pm);
3170
3356
  await mkdir4(root, { recursive: true });
3171
3357
  try {
@@ -3179,9 +3365,9 @@ async function buildContext(input) {
3179
3365
  return true;
3180
3366
  } catch (e) {
3181
3367
  reportError(e);
3182
- await rm4(rnessDir, { recursive: true, force: true }).catch(() => void 0);
3368
+ await rm5(rnessDir, { recursive: true, force: true }).catch(() => void 0);
3183
3369
  if (await isEmptyDir(root).catch(() => false))
3184
- await rm4(root, { recursive: true, force: true }).catch(() => void 0);
3370
+ await rm5(root, { recursive: true, force: true }).catch(() => void 0);
3185
3371
  process.stderr.write(
3186
3372
  `removed ${shown}/.rness after the failure; fix it and retry
3187
3373
  `
@@ -3193,7 +3379,7 @@ async function commitContext(input) {
3193
3379
  const { ui, rnessDir, shown } = input;
3194
3380
  await init(rnessDir);
3195
3381
  try {
3196
- await commitAll(rnessDir, "chore: rness workspace context");
3382
+ await commitAll(rnessDir, scaffoldMessage(CREATE_SUBJECT, VERSION));
3197
3383
  ui.line("committed", `${shown}/.rness`);
3198
3384
  } catch (e) {
3199
3385
  ui.warn(
@@ -3234,7 +3420,7 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
3234
3420
 
3235
3421
  // src/commands/create-blank.ts
3236
3422
  import { mkdir as mkdir5 } from "fs/promises";
3237
- import { join as join17, relative as relative3, resolve as resolve3 } from "path";
3423
+ import { join as join18, relative as relative3, resolve as resolve4 } from "path";
3238
3424
  var WORKSPACE_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._][A-Za-z0-9._-]*$/;
3239
3425
  function workspaceNameError(name) {
3240
3426
  return `workspace name "${name}" may only contain letters, digits, '.', '_' and '-', not starting with '-'`;
@@ -3266,7 +3452,7 @@ async function createBlank(input) {
3266
3452
  name = answer.trim();
3267
3453
  prompted = true;
3268
3454
  }
3269
- const root = resolve3(cwd, name);
3455
+ const root = resolve4(cwd, name);
3270
3456
  const shown = relative3(cwd, root) || ".";
3271
3457
  const problem = await targetProblem(cwd, root, pm);
3272
3458
  if (problem !== null) {
@@ -3308,8 +3494,8 @@ async function createBlank(input) {
3308
3494
  kind: "blank workspace"
3309
3495
  });
3310
3496
  if (!built) return 1;
3311
- await mkdir5(join17(root, "org"), { recursive: true });
3312
- await commitContext({ ui, rnessDir: join17(root, ".rness"), shown });
3497
+ await mkdir5(join18(root, "org"), { recursive: true });
3498
+ await commitContext({ ui, rnessDir: join18(root, ".rness"), shown });
3313
3499
  const code = await syncBlocks(
3314
3500
  ui,
3315
3501
  root,
@@ -3469,7 +3655,7 @@ async function cloneSelection(input) {
3469
3655
  { doing: `cloning org/${repo}`, git: true },
3470
3656
  () => clone(
3471
3657
  entry.url,
3472
- join18(input.root, "org", repo),
3658
+ join19(input.root, "org", repo),
3473
3659
  input.provider.credentialsFor(entry.url)
3474
3660
  ),
3475
3661
  () => ["cloned", `org/${repo}`]
@@ -3738,7 +3924,7 @@ async function createCommand(opts, deps = {}) {
3738
3924
  org = answer.trim();
3739
3925
  prompted = true;
3740
3926
  }
3741
- const root = resolve4(cwd, org);
3927
+ const root = resolve5(cwd, org);
3742
3928
  const shown = relative4(cwd, root) || ".";
3743
3929
  const problem = await targetProblem(cwd, root, pm);
3744
3930
  if (problem !== null) {
@@ -3793,8 +3979,8 @@ async function createCommand(opts, deps = {}) {
3793
3979
  scopes: {}
3794
3980
  };
3795
3981
  const staged = async () => {
3796
- staging ??= await mkdtemp(join18(tmpdir(), "rness-join-"));
3797
- return join18(staging, ".rness");
3982
+ staging ??= await mkdtemp2(join19(tmpdir2(), "rness-join-"));
3983
+ return join19(staging, ".rness");
3798
3984
  };
3799
3985
  if (joining) {
3800
3986
  const stagedDir = await staged();
@@ -3852,7 +4038,7 @@ async function createCommand(opts, deps = {}) {
3852
4038
  });
3853
4039
  if (p.isCancel(ok) || ok !== true) return cancelled(ui);
3854
4040
  }
3855
- const rnessDir = join18(root, ".rness");
4041
+ const rnessDir = join19(root, ".rness");
3856
4042
  const install = () => installContext({ ui, pm, rnessDir, skip: opts.skipInstall === true });
3857
4043
  if (joining) {
3858
4044
  await mkdir6(root, { recursive: true });
@@ -3865,7 +4051,7 @@ async function createCommand(opts, deps = {}) {
3865
4051
  ui.line("cloned", `${shown}/.rness (joined ${org})`);
3866
4052
  if (opts.pm === void 0) pm = await workspacePackageManager(rnessDir);
3867
4053
  await install();
3868
- await mkdir6(join18(root, "org"), { recursive: true });
4054
+ await mkdir6(join19(root, "org"), { recursive: true });
3869
4055
  const failures2 = await cloneSelection({
3870
4056
  root,
3871
4057
  rnessDir,
@@ -3918,7 +4104,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
3918
4104
  kind: "new workspace"
3919
4105
  });
3920
4106
  if (!built) return 1;
3921
- await mkdir6(join18(root, "org"), { recursive: true });
4107
+ await mkdir6(join19(root, "org"), { recursive: true });
3922
4108
  const failures = await cloneSelection({
3923
4109
  root,
3924
4110
  rnessDir,
@@ -3985,7 +4171,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
3985
4171
  return reportError(e);
3986
4172
  } finally {
3987
4173
  if (staging !== void 0)
3988
- await rm5(staging, { recursive: true, force: true }).catch(() => void 0);
4174
+ await rm6(staging, { recursive: true, force: true }).catch(() => void 0);
3989
4175
  }
3990
4176
  }
3991
4177
 
@@ -4018,17 +4204,16 @@ async function logoutCommand() {
4018
4204
  }
4019
4205
 
4020
4206
  // src/commands/upgrade.ts
4021
- import { execFile as execFile5 } from "child_process";
4022
- import { mkdir as mkdir7, readFile as readFile9, readdir as readdir3 } from "fs/promises";
4023
- import { basename as basename5, dirname as dirname8, join as join19, relative as relative5 } from "path";
4024
- import { promisify as promisify5 } from "util";
4025
- var execFileP5 = promisify5(execFile5);
4207
+ import { execFile as execFile6 } from "child_process";
4208
+ import { mkdtemp as mkdtemp3, readFile as readFile9, rm as rm7 } from "fs/promises";
4209
+ import { tmpdir as tmpdir3 } from "os";
4210
+ import { basename as basename5, join as join20, relative as relative5 } from "path";
4211
+ import { promisify as promisify6 } from "util";
4212
+ var execFileP6 = promisify6(execFile6);
4026
4213
  var NOTES = "https://github.com/rness-dev/rness/tree/main/packages/cli#readme";
4027
- var OLD_WORKFLOW_RUN = /npx --yes @rness\/cli@(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?) validate/;
4028
- var WORKFLOW_RUN = `npx --yes "@rness/cli@$(node -p "require('./package.json').devDependencies['@rness/cli']")" validate`;
4029
4214
  async function latestVersion() {
4030
4215
  try {
4031
- const { stdout } = await execFileP5(
4216
+ const { stdout } = await execFileP6(
4032
4217
  "npm",
4033
4218
  ["view", "@rness/cli@latest", "version"],
4034
4219
  { timeout: 3e4 }
@@ -4039,7 +4224,70 @@ async function latestVersion() {
4039
4224
  return null;
4040
4225
  }
4041
4226
  }
4042
- async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4227
+ async function scaffoldFor(version, rnessDir) {
4228
+ if (version === VERSION) return { dir: scaffoldDir() };
4229
+ const installed = await readPinnedCli(rnessDir);
4230
+ const inside = installed === null ? null : join20(installed.dir, "scaffold");
4231
+ if (installed?.version === version && inside !== null && await exists(join20(inside, "package.json")))
4232
+ return { dir: inside };
4233
+ const scratch = await mkdtemp3(join20(tmpdir3(), "rness-pack-"));
4234
+ const cleanup = () => rm7(scratch, { recursive: true, force: true });
4235
+ try {
4236
+ const { stdout } = await execFileP6(
4237
+ "npm",
4238
+ [
4239
+ "pack",
4240
+ `@rness/cli@${version}`,
4241
+ "--pack-destination",
4242
+ scratch,
4243
+ "--silent"
4244
+ ],
4245
+ { timeout: 6e4 }
4246
+ );
4247
+ const tarball = stdout.trim().split("\n").at(-1) ?? "";
4248
+ await execFileP6("tar", ["-xzf", join20(scratch, tarball), "-C", scratch]);
4249
+ return { dir: join20(scratch, "package", "scaffold"), cleanup };
4250
+ } catch (e) {
4251
+ await cleanup();
4252
+ throw new Error(
4253
+ `cannot get the @rness/cli ${version} scaffold (npm pack): ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`,
4254
+ { cause: e }
4255
+ );
4256
+ }
4257
+ }
4258
+ async function declaredManager(rnessDir, pm) {
4259
+ try {
4260
+ const pkg = JSON.parse(
4261
+ await readFile9(join20(rnessDir, "package.json"), "utf8")
4262
+ );
4263
+ if (typeof pkg.packageManager === "string") return pkg.packageManager;
4264
+ } catch {
4265
+ }
4266
+ return `${pm}@${await packageManagerVersion(pm)}`;
4267
+ }
4268
+ function finish(ui, cwd, rnessDir, label, target) {
4269
+ const rel = relative5(cwd, rnessDir) || ".";
4270
+ const next = [
4271
+ `git -C ${rel} add -A && git -C ${rel} commit -m "chore: rness ${target}"`,
4272
+ "# teammates: git pull \u2014 the next rness command installs it"
4273
+ ];
4274
+ if (ui.session) {
4275
+ ui.note("Next steps", next);
4276
+ ui.outro(`${label} runs @rness/cli ${target}`);
4277
+ } else
4278
+ process.stdout.write(
4279
+ [
4280
+ "",
4281
+ status("upgraded", `${label} to @rness/cli ${target}`),
4282
+ "",
4283
+ "Next:",
4284
+ ...next.map((line) => ` ${line}`),
4285
+ ""
4286
+ ].join("\n")
4287
+ );
4288
+ return 0;
4289
+ }
4290
+ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps = {}) {
4043
4291
  const cwd = opts.cwd ?? process.cwd();
4044
4292
  if (version !== void 0 && !EXACT_VERSION.test(version)) {
4045
4293
  process.stderr.write(
@@ -4081,7 +4329,21 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4081
4329
  return 1;
4082
4330
  }
4083
4331
  const installed = (await readPinnedCli(ws.rnessDir))?.version ?? null;
4084
- if (pin.spec === target && installed === target) {
4332
+ if (!await isRepository2(ws.rnessDir)) {
4333
+ process.stderr.write(`${label} is not a git repository
4334
+ `);
4335
+ return 1;
4336
+ }
4337
+ if (!await isClean(ws.rnessDir)) {
4338
+ process.stderr.write(
4339
+ `${label} has uncommitted changes; commit or stash them, then ${rnessCommand()} upgrade
4340
+ `
4341
+ );
4342
+ return 1;
4343
+ }
4344
+ const base = await findScaffoldBase(ws.rnessDir);
4345
+ const scaffoldCurrent = base !== null && base.version === target;
4346
+ if (pin.spec === target && installed === target && scaffoldCurrent) {
4085
4347
  if (ui.session) ui.outro(`Already at ${target}`);
4086
4348
  else process.stdout.write(`already at ${target}
4087
4349
  `);
@@ -4096,7 +4358,13 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4096
4358
  return 1;
4097
4359
  }
4098
4360
  const pm = await workspacePackageManager(ws.rnessDir);
4099
- if (pin.spec === target)
4361
+ if (pin.spec === target && installed === target)
4362
+ ui.line(
4363
+ "scaffold",
4364
+ `@rness/cli ${target} \u2014 merging it into ${label}`,
4365
+ `Merging the @rness/cli ${target} scaffold into ${label}`
4366
+ );
4367
+ else if (pin.spec === target)
4100
4368
  ui.line(
4101
4369
  "install",
4102
4370
  `@rness/cli ${target} in ${label} (${pm}) \u2014 ${installed ?? "nothing"} is installed`
@@ -4118,46 +4386,64 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4118
4386
  return 1;
4119
4387
  }
4120
4388
  }
4121
- const packageFile = join19(ws.rnessDir, "package.json");
4122
- const restore = /* @__PURE__ */ new Map([
4123
- [packageFile, await readFile9(packageFile, "utf8")]
4124
- ]);
4125
- if (pin.spec !== target) await writePin(ws.rnessDir, pin.spec, target);
4126
- const workflows = join19(ws.rnessDir, ".github", "workflows");
4127
- for (const name of await readdir3(workflows).catch(() => [])) {
4128
- if (!/\.ya?ml$/.test(name)) continue;
4129
- const file = join19(workflows, name);
4130
- const text = await readOrNull(file);
4131
- if (text === null) continue;
4132
- const shown = `.github/workflows/${name}`;
4133
- if (name === "validate.yml" && OLD_WORKFLOW_RUN.test(text)) {
4134
- restore.set(file, text);
4135
- await writeFileAtomic(
4136
- file,
4137
- text.replace(OLD_WORKFLOW_RUN, () => WORKFLOW_RUN)
4389
+ let merged = false;
4390
+ if (!scaffoldCurrent) {
4391
+ const source = await (deps.scaffoldFor ?? scaffoldFor)(
4392
+ target,
4393
+ ws.rnessDir
4394
+ );
4395
+ try {
4396
+ const files = await renderScaffold(source.dir, {
4397
+ version: target,
4398
+ packageManager: await declaredManager(ws.rnessDir, pm)
4399
+ });
4400
+ if (base === null)
4401
+ ui.line(
4402
+ "adopting",
4403
+ `the @rness/cli ${target} scaffold (no scaffold commit in ${label} yet)`,
4404
+ `Adopting the @rness/cli ${target} scaffold \u2014 ${label} has no scaffold commit yet`
4405
+ );
4406
+ else
4407
+ ui.line(
4408
+ "merging",
4409
+ `the @rness/cli ${target} scaffold`,
4410
+ `Merging the @rness/cli ${target} scaffold`
4411
+ );
4412
+ const commit = await buildScaffoldCommit(
4413
+ ws.rnessDir,
4414
+ base?.commit ?? null,
4415
+ base === null ? await withoutRedundantKeeps(ws.rnessDir, files) : files,
4416
+ target
4138
4417
  );
4139
- ui.line(
4140
- "updated",
4141
- `${shown} (it now reads the version from package.json)`
4418
+ const result = await mergeScaffold(
4419
+ ws.rnessDir,
4420
+ commit,
4421
+ base?.commit ?? null
4142
4422
  );
4143
- } else if (pin.spec !== target && text.includes(`@rness/cli@${pin.spec}`)) {
4144
- ui.warn(`${shown} names @rness/cli@${pin.spec}; update it by hand`);
4423
+ for (const [change, path] of result.changes) ui.line(change, path);
4424
+ if (result.conflicts.length > 0) {
4425
+ for (const path of result.conflicts) ui.error(`conflict ${path}`);
4426
+ const rel = relative5(cwd, ws.rnessDir) || ".";
4427
+ ui.hint(
4428
+ `resolve them in ${rel} (git checkout --ours <file> keeps yours, --theirs takes the scaffold's, or edit), git add, git commit, then ${rnessCommand()} upgrade again`,
4429
+ "stderr"
4430
+ );
4431
+ return 1;
4432
+ }
4433
+ merged = true;
4434
+ } finally {
4435
+ await source.cleanup?.();
4145
4436
  }
4146
4437
  }
4147
- const dependabot = join19(ws.rnessDir, ".github", "dependabot.yml");
4148
- if (await exists(dependabot)) {
4149
- ui.hint(".github/dependabot.yml is already there \u2014 left as it is");
4150
- } else {
4151
- await mkdir7(dirname8(dependabot), { recursive: true });
4152
- await writeFileAtomic(
4153
- dependabot,
4154
- await readFile9(join19(scaffoldDir(), ".github", "dependabot.yml"), "utf8")
4155
- );
4156
- ui.line(
4157
- "wrote",
4158
- ".rness/.github/dependabot.yml (@rness/cli now moves by pull request)"
4159
- );
4160
- }
4438
+ const packageFile = join20(ws.rnessDir, "package.json");
4439
+ const restore = /* @__PURE__ */ new Map([
4440
+ [packageFile, await readFile9(packageFile, "utf8")]
4441
+ ]);
4442
+ const moved = await readPin(ws.rnessDir);
4443
+ if (moved !== null && moved.spec !== target)
4444
+ await writePin(ws.rnessDir, moved.spec, target);
4445
+ if (pin.spec === target && installed === target)
4446
+ return finish(ui, cwd, ws.rnessDir, label, target);
4161
4447
  try {
4162
4448
  await ui.step(
4163
4449
  { doing: `installing dependencies with ${pm}` },
@@ -4165,10 +4451,20 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4165
4451
  () => ["installed", `dependencies with ${pm}`]
4166
4452
  );
4167
4453
  } catch (e) {
4168
- for (const [file, text] of restore) await writeFileAtomic(file, text);
4169
4454
  const message = e instanceof Error ? e.message : String(e);
4170
4455
  ui.error(message);
4171
- ui.hint(`restored ${relative5(cwd, packageFile) || packageFile}`, "stderr");
4456
+ if (!merged) {
4457
+ for (const [file, text] of restore) await writeFileAtomic(file, text);
4458
+ ui.hint(
4459
+ `restored ${relative5(cwd, packageFile) || packageFile}`,
4460
+ "stderr"
4461
+ );
4462
+ }
4463
+ if (merged)
4464
+ ui.hint(
4465
+ `the scaffold merge is still staged; undo it with: git -C ${relative5(cwd, ws.rnessDir) || "."} merge --abort`,
4466
+ "stderr"
4467
+ );
4172
4468
  if (/minimum.?release.?age|NO_MATURE/i.test(message))
4173
4469
  ui.hint(
4174
4470
  "pnpm refuses versions published less than 24 h ago: add '@rness/cli' under minimumReleaseAgeExclude in .rness/pnpm-workspace.yaml",
@@ -4187,33 +4483,14 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4187
4483
  ui.gitIsSilent = true;
4188
4484
  const code = await syncBlocks(ui, ws.root, basename5(ws.root));
4189
4485
  if (code !== 0) return code;
4190
- const rel = relative5(cwd, ws.rnessDir) || ".";
4191
- const next = [
4192
- `git -C ${rel} add -A && git -C ${rel} commit -m "chore: rness ${target}"`,
4193
- "# teammates: git pull \u2014 the next rness command installs it"
4194
- ];
4195
- if (ui.session) {
4196
- ui.note("Next steps", next);
4197
- ui.outro(`${label} runs @rness/cli ${target}`);
4198
- } else
4199
- process.stdout.write(
4200
- [
4201
- "",
4202
- status("upgraded", `${label} to @rness/cli ${target}`),
4203
- "",
4204
- "Next:",
4205
- ...next.map((line) => ` ${line}`),
4206
- ""
4207
- ].join("\n")
4208
- );
4209
- return 0;
4486
+ return finish(ui, cwd, ws.rnessDir, label, target);
4210
4487
  } catch (e) {
4211
4488
  return reportError(e);
4212
4489
  }
4213
4490
  }
4214
4491
 
4215
4492
  // src/core/blocks.ts
4216
- import { join as join20 } from "path";
4493
+ import { join as join21 } from "path";
4217
4494
  var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
4218
4495
  async function checkBlocks({
4219
4496
  root,
@@ -4230,7 +4507,7 @@ async function checkBlocks({
4230
4507
  warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
4231
4508
  checks
4232
4509
  });
4233
- if (!await exists(join20(root, "org"))) return derive();
4510
+ if (!await exists(join21(root, "org"))) return derive();
4234
4511
  const targets = [
4235
4512
  { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
4236
4513
  { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
@@ -4239,7 +4516,7 @@ async function checkBlocks({
4239
4516
  const segments = entry.path.split("/");
4240
4517
  targets.push({
4241
4518
  scope: name,
4242
- dir: join20(root, ...segments),
4519
+ dir: join21(root, ...segments),
4243
4520
  depth: segments.length,
4244
4521
  file: "AGENTS.md",
4245
4522
  label: `${entry.path}/AGENTS.md`
@@ -4254,7 +4531,7 @@ async function checkBlocks({
4254
4531
  );
4255
4532
  continue;
4256
4533
  }
4257
- const text = await readOrNull(join20(t.dir, t.file));
4534
+ const text = await readOrNull(join21(t.dir, t.file));
4258
4535
  const lines = text === null ? [] : text.split(/\r?\n/);
4259
4536
  const found = findBlock(lines);
4260
4537
  if (found.kind === "none") {
@@ -4294,7 +4571,7 @@ async function checkBlocks({
4294
4571
  }
4295
4572
 
4296
4573
  // src/core/contract.ts
4297
- import { join as join21 } from "path";
4574
+ import { join as join22 } from "path";
4298
4575
  var STATUSES = {
4299
4576
  adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
4300
4577
  specs: [
@@ -4313,7 +4590,7 @@ async function checkContract(rnessDir) {
4313
4590
  const problems = [];
4314
4591
  for (const name of COLLECTIONS) {
4315
4592
  const allowed = STATUSES[name];
4316
- const items = await collectMarkdown(join21(rnessDir, name));
4593
+ const items = await collectMarkdown(join22(rnessDir, name));
4317
4594
  for (const item of items) {
4318
4595
  const where = `${name}/${item.rel}`;
4319
4596
  if (SKIP.has(where)) continue;
@@ -4375,11 +4652,25 @@ async function validateCommand(opts, deps = {}) {
4375
4652
  );
4376
4653
  else if (o.status === "invalid")
4377
4654
  problems.push(`${o.label}: ${o.detail ?? "unreadable"}`);
4655
+ const scaffoldWarnings = [];
4656
+ if (await isRepository2(ws.rnessDir) && !await isShallow(ws.rnessDir)) {
4657
+ const pin = await readPin(ws.rnessDir);
4658
+ const base = await findScaffoldBase(ws.rnessDir);
4659
+ if (base === null || base.version === null)
4660
+ scaffoldWarnings.push(
4661
+ `the scaffold is not tracked in .rness yet: run ${rnessCommand()} upgrade`
4662
+ );
4663
+ else if (pin !== null && EXACT_VERSION.test(pin.spec) && pin.spec !== base.version)
4664
+ scaffoldWarnings.push(
4665
+ `the @rness/cli ${pin.spec} scaffold is not merged here: run ${rnessCommand()} upgrade`
4666
+ );
4667
+ }
4378
4668
  const ui = await makeUi(deps.terminal ?? defaultTerminal);
4379
4669
  if (!ui.session) {
4380
4670
  for (const p of problems) process.stderr.write(`${p}
4381
4671
  `);
4382
- for (const w of blocks.warnings) process.stderr.write(`${warn(w)}
4672
+ for (const w of [...blocks.warnings, ...scaffoldWarnings])
4673
+ process.stderr.write(`${warn(w)}
4383
4674
  `);
4384
4675
  if (problems.length > 0) return 1;
4385
4676
  process.stdout.write("context ok\n");
@@ -4400,6 +4691,7 @@ async function validateCommand(opts, deps = {}) {
4400
4691
  for (const o of targets)
4401
4692
  if (o.status === "unchanged") ui.line("current", o.label);
4402
4693
  for (const p of problems) if (!blocks.problems.includes(p)) ui.error(p);
4694
+ for (const w of scaffoldWarnings) ui.warn(w);
4403
4695
  ui.outro(
4404
4696
  problems.length > 0 ? "Context mismatch" : "Context OK",
4405
4697
  problems.length > 0 ? "error" : "done"
@@ -4551,7 +4843,7 @@ function buildProgram(state) {
4551
4843
  });
4552
4844
  return program;
4553
4845
  }
4554
- async function run(argv) {
4846
+ async function run2(argv) {
4555
4847
  const state = { code: 0 };
4556
4848
  const program = buildProgram(state);
4557
4849
  if (argv.length === 0) {
@@ -4678,8 +4970,8 @@ export {
4678
4970
  STATUSES,
4679
4971
  checkContract,
4680
4972
  validateCommand,
4681
- run,
4973
+ run2 as run,
4682
4974
  OWN_COMMANDS,
4683
4975
  catchUp
4684
4976
  };
4685
- //# sourceMappingURL=chunk-VYP4L7BQ.js.map
4977
+ //# sourceMappingURL=chunk-ONLF4VFA.js.map