@rness/cli 0.7.0 → 0.8.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.
@@ -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.0";
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,166 @@ 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
+
3127
3295
  // src/core/new-workspace.ts
3128
3296
  async function isEmptyDir(dir) {
3129
3297
  return (await readdir2(dir)).length === 0;
@@ -3138,11 +3306,11 @@ async function insideWorkspace(dir) {
3138
3306
  }
3139
3307
  async function targetProblem(cwd, root, pm) {
3140
3308
  const shown = relative2(cwd, root) || ".";
3141
- if (await insideWorkspace(cwd) || await insideWorkspace(dirname7(root)))
3309
+ if (await insideWorkspace(cwd) || await insideWorkspace(dirname8(root)))
3142
3310
  return "already inside an rness workspace; create runs from outside one";
3143
3311
  if (!await exists(root)) return null;
3144
- if (await exists(join16(root, ".rness", "rness.json"))) {
3145
- if (!await exists(join16(root, ".rness", "node_modules")))
3312
+ if (await exists(join17(root, ".rness", "rness.json"))) {
3313
+ if (!await exists(join17(root, ".rness", "node_modules")))
3146
3314
  return `${shown} holds an rness workspace whose dependencies are not installed; cd ${shown}/.rness && ${pm} install, then ${rnessCommand()} sync`;
3147
3315
  return `${shown} already holds an rness workspace (.rness/); run ${rnessCommand()} add inside it to add repositories`;
3148
3316
  }
@@ -3165,7 +3333,7 @@ async function installContext(input) {
3165
3333
  }
3166
3334
  async function buildContext(input) {
3167
3335
  const { ui, root, shown, pm } = input;
3168
- const rnessDir = join16(root, ".rness");
3336
+ const rnessDir = join17(root, ".rness");
3169
3337
  const pmVersion = await packageManagerVersion(pm);
3170
3338
  await mkdir4(root, { recursive: true });
3171
3339
  try {
@@ -3179,9 +3347,9 @@ async function buildContext(input) {
3179
3347
  return true;
3180
3348
  } catch (e) {
3181
3349
  reportError(e);
3182
- await rm4(rnessDir, { recursive: true, force: true }).catch(() => void 0);
3350
+ await rm5(rnessDir, { recursive: true, force: true }).catch(() => void 0);
3183
3351
  if (await isEmptyDir(root).catch(() => false))
3184
- await rm4(root, { recursive: true, force: true }).catch(() => void 0);
3352
+ await rm5(root, { recursive: true, force: true }).catch(() => void 0);
3185
3353
  process.stderr.write(
3186
3354
  `removed ${shown}/.rness after the failure; fix it and retry
3187
3355
  `
@@ -3193,7 +3361,7 @@ async function commitContext(input) {
3193
3361
  const { ui, rnessDir, shown } = input;
3194
3362
  await init(rnessDir);
3195
3363
  try {
3196
- await commitAll(rnessDir, "chore: rness workspace context");
3364
+ await commitAll(rnessDir, scaffoldMessage(CREATE_SUBJECT, VERSION));
3197
3365
  ui.line("committed", `${shown}/.rness`);
3198
3366
  } catch (e) {
3199
3367
  ui.warn(
@@ -3234,7 +3402,7 @@ async function syncBlocks(ui, root, shown, inPlace, sshWorks) {
3234
3402
 
3235
3403
  // src/commands/create-blank.ts
3236
3404
  import { mkdir as mkdir5 } from "fs/promises";
3237
- import { join as join17, relative as relative3, resolve as resolve3 } from "path";
3405
+ import { join as join18, relative as relative3, resolve as resolve4 } from "path";
3238
3406
  var WORKSPACE_NAME = /^(?!\.{1,2}$)[A-Za-z0-9._][A-Za-z0-9._-]*$/;
3239
3407
  function workspaceNameError(name) {
3240
3408
  return `workspace name "${name}" may only contain letters, digits, '.', '_' and '-', not starting with '-'`;
@@ -3266,7 +3434,7 @@ async function createBlank(input) {
3266
3434
  name = answer.trim();
3267
3435
  prompted = true;
3268
3436
  }
3269
- const root = resolve3(cwd, name);
3437
+ const root = resolve4(cwd, name);
3270
3438
  const shown = relative3(cwd, root) || ".";
3271
3439
  const problem = await targetProblem(cwd, root, pm);
3272
3440
  if (problem !== null) {
@@ -3308,8 +3476,8 @@ async function createBlank(input) {
3308
3476
  kind: "blank workspace"
3309
3477
  });
3310
3478
  if (!built) return 1;
3311
- await mkdir5(join17(root, "org"), { recursive: true });
3312
- await commitContext({ ui, rnessDir: join17(root, ".rness"), shown });
3479
+ await mkdir5(join18(root, "org"), { recursive: true });
3480
+ await commitContext({ ui, rnessDir: join18(root, ".rness"), shown });
3313
3481
  const code = await syncBlocks(
3314
3482
  ui,
3315
3483
  root,
@@ -3469,7 +3637,7 @@ async function cloneSelection(input) {
3469
3637
  { doing: `cloning org/${repo}`, git: true },
3470
3638
  () => clone(
3471
3639
  entry.url,
3472
- join18(input.root, "org", repo),
3640
+ join19(input.root, "org", repo),
3473
3641
  input.provider.credentialsFor(entry.url)
3474
3642
  ),
3475
3643
  () => ["cloned", `org/${repo}`]
@@ -3738,7 +3906,7 @@ async function createCommand(opts, deps = {}) {
3738
3906
  org = answer.trim();
3739
3907
  prompted = true;
3740
3908
  }
3741
- const root = resolve4(cwd, org);
3909
+ const root = resolve5(cwd, org);
3742
3910
  const shown = relative4(cwd, root) || ".";
3743
3911
  const problem = await targetProblem(cwd, root, pm);
3744
3912
  if (problem !== null) {
@@ -3793,8 +3961,8 @@ async function createCommand(opts, deps = {}) {
3793
3961
  scopes: {}
3794
3962
  };
3795
3963
  const staged = async () => {
3796
- staging ??= await mkdtemp(join18(tmpdir(), "rness-join-"));
3797
- return join18(staging, ".rness");
3964
+ staging ??= await mkdtemp2(join19(tmpdir2(), "rness-join-"));
3965
+ return join19(staging, ".rness");
3798
3966
  };
3799
3967
  if (joining) {
3800
3968
  const stagedDir = await staged();
@@ -3852,7 +4020,7 @@ async function createCommand(opts, deps = {}) {
3852
4020
  });
3853
4021
  if (p.isCancel(ok) || ok !== true) return cancelled(ui);
3854
4022
  }
3855
- const rnessDir = join18(root, ".rness");
4023
+ const rnessDir = join19(root, ".rness");
3856
4024
  const install = () => installContext({ ui, pm, rnessDir, skip: opts.skipInstall === true });
3857
4025
  if (joining) {
3858
4026
  await mkdir6(root, { recursive: true });
@@ -3865,7 +4033,7 @@ async function createCommand(opts, deps = {}) {
3865
4033
  ui.line("cloned", `${shown}/.rness (joined ${org})`);
3866
4034
  if (opts.pm === void 0) pm = await workspacePackageManager(rnessDir);
3867
4035
  await install();
3868
- await mkdir6(join18(root, "org"), { recursive: true });
4036
+ await mkdir6(join19(root, "org"), { recursive: true });
3869
4037
  const failures2 = await cloneSelection({
3870
4038
  root,
3871
4039
  rnessDir,
@@ -3918,7 +4086,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
3918
4086
  kind: "new workspace"
3919
4087
  });
3920
4088
  if (!built) return 1;
3921
- await mkdir6(join18(root, "org"), { recursive: true });
4089
+ await mkdir6(join19(root, "org"), { recursive: true });
3922
4090
  const failures = await cloneSelection({
3923
4091
  root,
3924
4092
  rnessDir,
@@ -3985,7 +4153,7 @@ ${next2.map((l) => ` ${l}`).join("\n")}
3985
4153
  return reportError(e);
3986
4154
  } finally {
3987
4155
  if (staging !== void 0)
3988
- await rm5(staging, { recursive: true, force: true }).catch(() => void 0);
4156
+ await rm6(staging, { recursive: true, force: true }).catch(() => void 0);
3989
4157
  }
3990
4158
  }
3991
4159
 
@@ -4018,17 +4186,16 @@ async function logoutCommand() {
4018
4186
  }
4019
4187
 
4020
4188
  // 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);
4189
+ import { execFile as execFile6 } from "child_process";
4190
+ import { mkdtemp as mkdtemp3, readFile as readFile9, rm as rm7 } from "fs/promises";
4191
+ import { tmpdir as tmpdir3 } from "os";
4192
+ import { basename as basename5, join as join20, relative as relative5 } from "path";
4193
+ import { promisify as promisify6 } from "util";
4194
+ var execFileP6 = promisify6(execFile6);
4026
4195
  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
4196
  async function latestVersion() {
4030
4197
  try {
4031
- const { stdout } = await execFileP5(
4198
+ const { stdout } = await execFileP6(
4032
4199
  "npm",
4033
4200
  ["view", "@rness/cli@latest", "version"],
4034
4201
  { timeout: 3e4 }
@@ -4039,7 +4206,70 @@ async function latestVersion() {
4039
4206
  return null;
4040
4207
  }
4041
4208
  }
4042
- async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4209
+ async function scaffoldFor(version, rnessDir) {
4210
+ if (version === VERSION) return { dir: scaffoldDir() };
4211
+ const installed = await readPinnedCli(rnessDir);
4212
+ const inside = installed === null ? null : join20(installed.dir, "scaffold");
4213
+ if (installed?.version === version && inside !== null && await exists(join20(inside, "package.json")))
4214
+ return { dir: inside };
4215
+ const scratch = await mkdtemp3(join20(tmpdir3(), "rness-pack-"));
4216
+ const cleanup = () => rm7(scratch, { recursive: true, force: true });
4217
+ try {
4218
+ const { stdout } = await execFileP6(
4219
+ "npm",
4220
+ [
4221
+ "pack",
4222
+ `@rness/cli@${version}`,
4223
+ "--pack-destination",
4224
+ scratch,
4225
+ "--silent"
4226
+ ],
4227
+ { timeout: 6e4 }
4228
+ );
4229
+ const tarball = stdout.trim().split("\n").at(-1) ?? "";
4230
+ await execFileP6("tar", ["-xzf", join20(scratch, tarball), "-C", scratch]);
4231
+ return { dir: join20(scratch, "package", "scaffold"), cleanup };
4232
+ } catch (e) {
4233
+ await cleanup();
4234
+ throw new Error(
4235
+ `cannot get the @rness/cli ${version} scaffold (npm pack): ${e instanceof Error ? e.message.split("\n")[0] : String(e)}`,
4236
+ { cause: e }
4237
+ );
4238
+ }
4239
+ }
4240
+ async function declaredManager(rnessDir, pm) {
4241
+ try {
4242
+ const pkg = JSON.parse(
4243
+ await readFile9(join20(rnessDir, "package.json"), "utf8")
4244
+ );
4245
+ if (typeof pkg.packageManager === "string") return pkg.packageManager;
4246
+ } catch {
4247
+ }
4248
+ return `${pm}@${await packageManagerVersion(pm)}`;
4249
+ }
4250
+ function finish(ui, cwd, rnessDir, label, target) {
4251
+ const rel = relative5(cwd, rnessDir) || ".";
4252
+ const next = [
4253
+ `git -C ${rel} add -A && git -C ${rel} commit -m "chore: rness ${target}"`,
4254
+ "# teammates: git pull \u2014 the next rness command installs it"
4255
+ ];
4256
+ if (ui.session) {
4257
+ ui.note("Next steps", next);
4258
+ ui.outro(`${label} runs @rness/cli ${target}`);
4259
+ } else
4260
+ process.stdout.write(
4261
+ [
4262
+ "",
4263
+ status("upgraded", `${label} to @rness/cli ${target}`),
4264
+ "",
4265
+ "Next:",
4266
+ ...next.map((line) => ` ${line}`),
4267
+ ""
4268
+ ].join("\n")
4269
+ );
4270
+ return 0;
4271
+ }
4272
+ async function upgradeCommand(version, opts, terminal = defaultTerminal, deps = {}) {
4043
4273
  const cwd = opts.cwd ?? process.cwd();
4044
4274
  if (version !== void 0 && !EXACT_VERSION.test(version)) {
4045
4275
  process.stderr.write(
@@ -4081,7 +4311,21 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4081
4311
  return 1;
4082
4312
  }
4083
4313
  const installed = (await readPinnedCli(ws.rnessDir))?.version ?? null;
4084
- if (pin.spec === target && installed === target) {
4314
+ if (!await isRepository2(ws.rnessDir)) {
4315
+ process.stderr.write(`${label} is not a git repository
4316
+ `);
4317
+ return 1;
4318
+ }
4319
+ if (!await isClean(ws.rnessDir)) {
4320
+ process.stderr.write(
4321
+ `${label} has uncommitted changes; commit or stash them, then ${rnessCommand()} upgrade
4322
+ `
4323
+ );
4324
+ return 1;
4325
+ }
4326
+ const base = await findScaffoldBase(ws.rnessDir);
4327
+ const scaffoldCurrent = base !== null && base.version === target;
4328
+ if (pin.spec === target && installed === target && scaffoldCurrent) {
4085
4329
  if (ui.session) ui.outro(`Already at ${target}`);
4086
4330
  else process.stdout.write(`already at ${target}
4087
4331
  `);
@@ -4096,7 +4340,9 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4096
4340
  return 1;
4097
4341
  }
4098
4342
  const pm = await workspacePackageManager(ws.rnessDir);
4099
- if (pin.spec === target)
4343
+ if (pin.spec === target && installed === target)
4344
+ ui.line("scaffold", `@rness/cli ${target} \u2014 merging it into ${label}`);
4345
+ else if (pin.spec === target)
4100
4346
  ui.line(
4101
4347
  "install",
4102
4348
  `@rness/cli ${target} in ${label} (${pm}) \u2014 ${installed ?? "nothing"} is installed`
@@ -4118,46 +4364,58 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4118
4364
  return 1;
4119
4365
  }
4120
4366
  }
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)
4367
+ let merged = false;
4368
+ if (!scaffoldCurrent) {
4369
+ const source = await (deps.scaffoldFor ?? scaffoldFor)(
4370
+ target,
4371
+ ws.rnessDir
4372
+ );
4373
+ try {
4374
+ const files = await renderScaffold(source.dir, {
4375
+ version: target,
4376
+ packageManager: await declaredManager(ws.rnessDir, pm)
4377
+ });
4378
+ if (base === null)
4379
+ ui.line(
4380
+ "adopting",
4381
+ `the @rness/cli ${target} scaffold (no scaffold commit in ${label} yet)`
4382
+ );
4383
+ else ui.line("merging", `the @rness/cli ${target} scaffold`);
4384
+ const commit = await buildScaffoldCommit(
4385
+ ws.rnessDir,
4386
+ base?.commit ?? null,
4387
+ files,
4388
+ target
4138
4389
  );
4139
- ui.line(
4140
- "updated",
4141
- `${shown} (it now reads the version from package.json)`
4390
+ const result = await mergeScaffold(
4391
+ ws.rnessDir,
4392
+ commit,
4393
+ base?.commit ?? null
4142
4394
  );
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`);
4395
+ for (const [change, path] of result.changes) ui.line(change, path);
4396
+ if (result.conflicts.length > 0) {
4397
+ for (const path of result.conflicts) ui.error(`conflict ${path}`);
4398
+ const rel = relative5(cwd, ws.rnessDir) || ".";
4399
+ ui.hint(
4400
+ `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`,
4401
+ "stderr"
4402
+ );
4403
+ return 1;
4404
+ }
4405
+ merged = true;
4406
+ } finally {
4407
+ await source.cleanup?.();
4145
4408
  }
4146
4409
  }
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
- }
4410
+ const packageFile = join20(ws.rnessDir, "package.json");
4411
+ const restore = /* @__PURE__ */ new Map([
4412
+ [packageFile, await readFile9(packageFile, "utf8")]
4413
+ ]);
4414
+ const moved = await readPin(ws.rnessDir);
4415
+ if (moved !== null && moved.spec !== target)
4416
+ await writePin(ws.rnessDir, moved.spec, target);
4417
+ if (pin.spec === target && installed === target)
4418
+ return finish(ui, cwd, ws.rnessDir, label, target);
4161
4419
  try {
4162
4420
  await ui.step(
4163
4421
  { doing: `installing dependencies with ${pm}` },
@@ -4165,10 +4423,20 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4165
4423
  () => ["installed", `dependencies with ${pm}`]
4166
4424
  );
4167
4425
  } catch (e) {
4168
- for (const [file, text] of restore) await writeFileAtomic(file, text);
4169
4426
  const message = e instanceof Error ? e.message : String(e);
4170
4427
  ui.error(message);
4171
- ui.hint(`restored ${relative5(cwd, packageFile) || packageFile}`, "stderr");
4428
+ if (!merged) {
4429
+ for (const [file, text] of restore) await writeFileAtomic(file, text);
4430
+ ui.hint(
4431
+ `restored ${relative5(cwd, packageFile) || packageFile}`,
4432
+ "stderr"
4433
+ );
4434
+ }
4435
+ if (merged)
4436
+ ui.hint(
4437
+ `the scaffold merge is still staged; undo it with: git -C ${relative5(cwd, ws.rnessDir) || "."} merge --abort`,
4438
+ "stderr"
4439
+ );
4172
4440
  if (/minimum.?release.?age|NO_MATURE/i.test(message))
4173
4441
  ui.hint(
4174
4442
  "pnpm refuses versions published less than 24 h ago: add '@rness/cli' under minimumReleaseAgeExclude in .rness/pnpm-workspace.yaml",
@@ -4187,33 +4455,14 @@ async function upgradeCommand(version, opts, terminal = defaultTerminal) {
4187
4455
  ui.gitIsSilent = true;
4188
4456
  const code = await syncBlocks(ui, ws.root, basename5(ws.root));
4189
4457
  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;
4458
+ return finish(ui, cwd, ws.rnessDir, label, target);
4210
4459
  } catch (e) {
4211
4460
  return reportError(e);
4212
4461
  }
4213
4462
  }
4214
4463
 
4215
4464
  // src/core/blocks.ts
4216
- import { join as join20 } from "path";
4465
+ import { join as join21 } from "path";
4217
4466
  var PROBLEM = /* @__PURE__ */ new Set(["stale", "malformed"]);
4218
4467
  async function checkBlocks({
4219
4468
  root,
@@ -4230,7 +4479,7 @@ async function checkBlocks({
4230
4479
  warnings: checks.filter((c) => !PROBLEM.has(c.status) && c.message !== null).map((c) => c.message),
4231
4480
  checks
4232
4481
  });
4233
- if (!await exists(join20(root, "org"))) return derive();
4482
+ if (!await exists(join21(root, "org"))) return derive();
4234
4483
  const targets = [
4235
4484
  { scope: null, dir: root, depth: 0, file: "AGENTS.md", label: "AGENTS.md" },
4236
4485
  { scope: null, dir: root, depth: 0, file: "CLAUDE.md", label: "CLAUDE.md" }
@@ -4239,7 +4488,7 @@ async function checkBlocks({
4239
4488
  const segments = entry.path.split("/");
4240
4489
  targets.push({
4241
4490
  scope: name,
4242
- dir: join20(root, ...segments),
4491
+ dir: join21(root, ...segments),
4243
4492
  depth: segments.length,
4244
4493
  file: "AGENTS.md",
4245
4494
  label: `${entry.path}/AGENTS.md`
@@ -4254,7 +4503,7 @@ async function checkBlocks({
4254
4503
  );
4255
4504
  continue;
4256
4505
  }
4257
- const text = await readOrNull(join20(t.dir, t.file));
4506
+ const text = await readOrNull(join21(t.dir, t.file));
4258
4507
  const lines = text === null ? [] : text.split(/\r?\n/);
4259
4508
  const found = findBlock(lines);
4260
4509
  if (found.kind === "none") {
@@ -4294,7 +4543,7 @@ async function checkBlocks({
4294
4543
  }
4295
4544
 
4296
4545
  // src/core/contract.ts
4297
- import { join as join21 } from "path";
4546
+ import { join as join22 } from "path";
4298
4547
  var STATUSES = {
4299
4548
  adr: ["Proposed", "Accepted", "Rejected", "Superseded"],
4300
4549
  specs: [
@@ -4313,7 +4562,7 @@ async function checkContract(rnessDir) {
4313
4562
  const problems = [];
4314
4563
  for (const name of COLLECTIONS) {
4315
4564
  const allowed = STATUSES[name];
4316
- const items = await collectMarkdown(join21(rnessDir, name));
4565
+ const items = await collectMarkdown(join22(rnessDir, name));
4317
4566
  for (const item of items) {
4318
4567
  const where = `${name}/${item.rel}`;
4319
4568
  if (SKIP.has(where)) continue;
@@ -4375,11 +4624,25 @@ async function validateCommand(opts, deps = {}) {
4375
4624
  );
4376
4625
  else if (o.status === "invalid")
4377
4626
  problems.push(`${o.label}: ${o.detail ?? "unreadable"}`);
4627
+ const scaffoldWarnings = [];
4628
+ if (await isRepository2(ws.rnessDir) && !await isShallow(ws.rnessDir)) {
4629
+ const pin = await readPin(ws.rnessDir);
4630
+ const base = await findScaffoldBase(ws.rnessDir);
4631
+ if (base === null || base.version === null)
4632
+ scaffoldWarnings.push(
4633
+ `the scaffold is not tracked in .rness yet: run ${rnessCommand()} upgrade`
4634
+ );
4635
+ else if (pin !== null && EXACT_VERSION.test(pin.spec) && pin.spec !== base.version)
4636
+ scaffoldWarnings.push(
4637
+ `the @rness/cli ${pin.spec} scaffold is not merged here: run ${rnessCommand()} upgrade`
4638
+ );
4639
+ }
4378
4640
  const ui = await makeUi(deps.terminal ?? defaultTerminal);
4379
4641
  if (!ui.session) {
4380
4642
  for (const p of problems) process.stderr.write(`${p}
4381
4643
  `);
4382
- for (const w of blocks.warnings) process.stderr.write(`${warn(w)}
4644
+ for (const w of [...blocks.warnings, ...scaffoldWarnings])
4645
+ process.stderr.write(`${warn(w)}
4383
4646
  `);
4384
4647
  if (problems.length > 0) return 1;
4385
4648
  process.stdout.write("context ok\n");
@@ -4400,6 +4663,7 @@ async function validateCommand(opts, deps = {}) {
4400
4663
  for (const o of targets)
4401
4664
  if (o.status === "unchanged") ui.line("current", o.label);
4402
4665
  for (const p of problems) if (!blocks.problems.includes(p)) ui.error(p);
4666
+ for (const w of scaffoldWarnings) ui.warn(w);
4403
4667
  ui.outro(
4404
4668
  problems.length > 0 ? "Context mismatch" : "Context OK",
4405
4669
  problems.length > 0 ? "error" : "done"
@@ -4551,7 +4815,7 @@ function buildProgram(state) {
4551
4815
  });
4552
4816
  return program;
4553
4817
  }
4554
- async function run(argv) {
4818
+ async function run2(argv) {
4555
4819
  const state = { code: 0 };
4556
4820
  const program = buildProgram(state);
4557
4821
  if (argv.length === 0) {
@@ -4678,8 +4942,8 @@ export {
4678
4942
  STATUSES,
4679
4943
  checkContract,
4680
4944
  validateCommand,
4681
- run,
4945
+ run2 as run,
4682
4946
  OWN_COMMANDS,
4683
4947
  catchUp
4684
4948
  };
4685
- //# sourceMappingURL=chunk-VYP4L7BQ.js.map
4949
+ //# sourceMappingURL=chunk-VQMX52HT.js.map