@plexus-ms/cli 0.100.0 → 0.102.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.
Files changed (2) hide show
  1. package/dist/plx.js +176 -103
  2. package/package.json +3 -3
package/dist/plx.js CHANGED
@@ -7589,6 +7589,7 @@ var PLX_PIN = "npm:@plexus-ms/cli";
7589
7589
  var bumpPlxPin = (text, train) => text.replace(/("npm:@plexus-ms\/cli"\s*=\s*(?:\{\s*version\s*=\s*)?")[^"]*(")/g, `$1${train}$2`);
7590
7590
  var CONFIG_PIN = "@plexus-ms/config";
7591
7591
  var bumpConfigPin = (text, train) => text.replace(/("@plexus-ms\/config":\s*")(?!workspace:|catalog:)[^"]*(")/g, `$1${train}$2`);
7592
+ var bumpCatalogPin = (text, train) => text.replace(/^(\s+"@plexus-ms\/config":\s*)\S+$/m, `$1${train}`);
7592
7593
  var stampStandard = (text, train, today2) => setFrontmatterField(setFrontmatterField(text, "version", train), "timestamp", today2);
7593
7594
  var stampChanged = (text, train) => setFrontmatterField(text, "changed", train, { after: "binds" });
7594
7595
  var hasConflictMarkers = (text) => /^<<<<<<< /m.test(text);
@@ -7760,7 +7761,7 @@ function realIo(prompt) {
7760
7761
  stdout: process.stdout,
7761
7762
  stderr: process.stderr,
7762
7763
  prompt: prompt && isInteractive() ? prompt : void 0,
7763
- exec: (command, args, options = {}) => new Promise((resolve10, reject) => {
7764
+ exec: (command, args, options = {}) => new Promise((resolve11, reject) => {
7764
7765
  const child = spawn(command, args, {
7765
7766
  cwd: options.cwd,
7766
7767
  env: options.env,
@@ -7783,7 +7784,7 @@ function realIo(prompt) {
7783
7784
  child.on("error", reject);
7784
7785
  child.on("close", (code) => {
7785
7786
  if (code === 0)
7786
- resolve10(stdout);
7787
+ resolve11(stdout);
7787
7788
  else
7788
7789
  reject(new ExecError(render(command, args), code, stderr));
7789
7790
  });
@@ -8682,22 +8683,22 @@ async function templateFacet(ctx, cwd) {
8682
8683
  // ../lib/dist/propagate.js
8683
8684
  import { join as join8, relative as relative3 } from "node:path";
8684
8685
  var isClean = (s) => s.conflicted.length === 0 && s.failed.length === 0 && s.skipped.length === 0;
8685
- var BASE_TAG = "ship-base";
8686
- var trainBranch = (train) => `train/${train}`;
8687
- var copierIn = (clone, args) => [
8686
+ var emptySummary = (train, preview) => ({
8687
+ train,
8688
+ preview,
8689
+ clean: [],
8690
+ conflicted: [],
8691
+ failed: [],
8692
+ skipped: [],
8693
+ pulls: []
8694
+ });
8695
+ var trainBranch = (train) => `plexus/${train}`;
8696
+ var copierIn = (dir, args) => [
8688
8697
  "env",
8689
- [`MISE_IGNORED_CONFIG_PATHS=${clone}`, "mise", ...copierVia(args)]
8698
+ [`MISE_IGNORED_CONFIG_PATHS=${dir}`, "mise", ...copierVia(args)]
8690
8699
  ];
8691
8700
  async function propagate(ctx, workspace, train) {
8692
- const summary = {
8693
- train,
8694
- preview: ctx.dryRun,
8695
- clean: [],
8696
- conflicted: [],
8697
- failed: [],
8698
- skipped: [],
8699
- pulls: []
8700
- };
8701
+ const summary = emptySummary(train, ctx.dryRun);
8701
8702
  const upstream = join8(workspace, UPSTREAM);
8702
8703
  const tree = await overview(ctx, workspace);
8703
8704
  const upstreamDir = tree.children.find((c) => c.path === upstream);
@@ -8712,6 +8713,7 @@ async function propagate(ctx, workspace, train) {
8712
8713
  for (const preset of presets)
8713
8714
  if (!consumers.some((c) => c.sites.some((s) => s.preset === preset)))
8714
8715
  ctx.say(`\u2022 no consumers of ${presetSource(preset)} found under ${workspace}`);
8716
+ const take = { train, opener: "`plx ship`", ...ctx.dryRun ? { previewFrom: upstream } : {} };
8715
8717
  const scratch = (await ctx.stage(run("mktemp", ["-d"])))?.trim() ?? "";
8716
8718
  try {
8717
8719
  for (const consumer of consumers) {
@@ -8721,17 +8723,12 @@ async function propagate(ctx, workspace, train) {
8721
8723
  ctx.say(`\u2192 ${consumer.rel}: clone ${consumer.origin} at ${consumer.branch}`);
8722
8724
  try {
8723
8725
  await ctx.stage(run("git", ["clone", "--quiet", "--branch", consumer.branch, consumer.origin, clone]));
8724
- await ctx.stage(run("git", ["tag", BASE_TAG], { cwd: clone }));
8725
8726
  } catch (error) {
8726
8727
  ctx.say(` \u2717 FAILED \u2014 clone${detail(error)}`);
8727
8728
  summary.failed.push(consumer.rel);
8728
8729
  continue;
8729
8730
  }
8730
- for (const site of consumer.sites)
8731
- await updateSite(ctx, consumer, site, clone, upstream, summary);
8732
- await refreshLockfile(ctx, consumer, clone, summary);
8733
- await bumpConsumerRefs(ctx, consumer, clone, train);
8734
- await openPull(ctx, consumer, clone, train, summary);
8731
+ await takeTrain(ctx, consumer, clone, take, summary);
8735
8732
  }
8736
8733
  } finally {
8737
8734
  if (scratch)
@@ -8739,7 +8736,15 @@ async function propagate(ctx, workspace, train) {
8739
8736
  }
8740
8737
  return summary;
8741
8738
  }
8742
- var previewCommit = (ctx, clone, message) => ctx.stage(run("git", ["add", "-A"], { cwd: clone })).then(() => ctx.stage(run("git", [
8739
+ async function takeTrain(ctx, consumer, dir, take, summary) {
8740
+ const base = (await ctx.read("git", ["rev-parse", "HEAD"], { cwd: dir })).trim();
8741
+ for (const site of consumer.sites)
8742
+ await updateSite(ctx, consumer, site, dir, take, summary);
8743
+ await refreshLockfile(ctx, consumer, dir, base, summary);
8744
+ await bumpConsumerRefs(ctx, consumer, dir, take.train);
8745
+ await openPull(ctx, consumer, dir, take, summary);
8746
+ }
8747
+ var previewCommit = (ctx, dir, message) => ctx.stage(run("git", ["add", "-A"], { cwd: dir })).then(() => ctx.stage(run("git", [
8743
8748
  "-c",
8744
8749
  "commit.gpgsign=false",
8745
8750
  "-c",
@@ -8752,31 +8757,32 @@ var previewCommit = (ctx, clone, message) => ctx.stage(run("git", ["add", "-A"],
8752
8757
  "--allow-empty",
8753
8758
  "-m",
8754
8759
  message
8755
- ], { cwd: clone })));
8756
- async function updateSite(ctx, consumer, site, clone, upstream, summary) {
8757
- const target = join8(clone, relative3(consumer.path, site.path));
8758
- const localPreset = join8(upstream, site.preset);
8760
+ ], { cwd: dir })));
8761
+ async function updateSite(ctx, consumer, site, dir, take, summary) {
8762
+ const target = join8(dir, relative3(consumer.path, site.path));
8759
8763
  try {
8760
- if (ctx.dryRun) {
8764
+ if (take.previewFrom) {
8765
+ const localPreset = join8(take.previewFrom, site.preset);
8761
8766
  ctx.say(`\u2192 ${site.rel}: copier update (preview from ${localPreset}@main)`);
8762
8767
  const answers = await ctx.readFile(join8(target, ANSWERS_FILE));
8763
8768
  await ctx.stage(write(join8(target, ANSWERS_FILE), answers.replace(/^_src_path: .*$/m, `_src_path: ${localPreset}`)));
8764
- await previewCommit(ctx, clone, `plx preview: ${site.rel} reads ${localPreset} from the local checkout`);
8765
- await ctx.stage(run(...copierIn(clone, [
8769
+ await previewCommit(ctx, dir, `plx preview: ${site.rel} reads ${localPreset} from the local checkout`);
8770
+ await ctx.stage(run(...copierIn(dir, ["update", "--skip-answered", "--trust", "--defaults", "--skip-tasks", "--vcs-ref", "main"]), { cwd: target }));
8771
+ } else {
8772
+ ctx.say(`\u2192 ${site.rel}: copier update${take.ref ? ` to ${take.ref}` : ""}`);
8773
+ const args = [
8766
8774
  "update",
8767
8775
  "--skip-answered",
8768
8776
  "--trust",
8769
- "--defaults",
8770
8777
  "--skip-tasks",
8771
- "--vcs-ref",
8772
- "main"
8773
- ]), {
8774
- cwd: target
8775
- }));
8776
- } else {
8777
- ctx.say(`\u2192 ${site.rel}: copier update`);
8778
- const args = ["update", "--skip-answered", "--trust", "--skip-tasks", ...ctx.yes ? ["--defaults"] : []];
8779
- await ctx.apply([run(...copierIn(clone, args), { cwd: target, interactive: true })]);
8778
+ ...take.ref ? ["--vcs-ref", take.ref] : [],
8779
+ ...ctx.yes || ctx.dryRun ? ["--defaults"] : []
8780
+ ];
8781
+ const step = run(...copierIn(dir, args), { cwd: target, interactive: !ctx.dryRun });
8782
+ if (ctx.dryRun)
8783
+ await ctx.stage(step);
8784
+ else
8785
+ await ctx.apply([step]);
8780
8786
  }
8781
8787
  } catch (error) {
8782
8788
  ctx.say(` \u2717 FAILED \u2014 copier update exited nonzero${detail(error)}`);
@@ -8801,8 +8807,10 @@ async function updateSite(ctx, consumer, site, clone, upstream, summary) {
8801
8807
  }
8802
8808
  for (const line of status.split("\n").filter(Boolean))
8803
8809
  ctx.say(` ${line}`);
8804
- if (ctx.dryRun)
8805
- await previewCommit(ctx, clone, `plx preview: ${site.rel} updated`);
8810
+ if (take.previewFrom)
8811
+ await previewCommit(ctx, dir, `plx preview: ${site.rel} updated`);
8812
+ else
8813
+ await ctx.stage(run("git", ["add", "-A"], { cwd: dir }));
8806
8814
  }
8807
8815
  async function syncSiteMarker(ctx, site, target) {
8808
8816
  const marker = await ctx.readFile(join8(target, MARKER_FILE)).catch(() => void 0);
@@ -8817,77 +8825,71 @@ async function syncSiteMarker(ctx, site, target) {
8817
8825
  ctx.say(` marker: plexus \u2192 ${commit} in ${site.rel}/${MARKER_FILE}`);
8818
8826
  await ctx.stage(write(join8(target, MARKER_FILE), synced));
8819
8827
  }
8820
- async function refreshLockfile(ctx, consumer, clone, summary) {
8821
- if (await ctx.readFile(join8(clone, "pnpm-workspace.yaml")).catch(() => void 0) === void 0)
8828
+ async function refreshLockfile(ctx, consumer, dir, base, summary) {
8829
+ if (await ctx.readFile(join8(dir, "pnpm-workspace.yaml")).catch(() => void 0) === void 0)
8822
8830
  return;
8823
8831
  const manifests = ["*package.json", "pnpm-workspace.yaml"];
8824
- const unchanged = await ctx.read("git", ["diff", "--quiet", BASE_TAG, "--", ...manifests], { cwd: clone }).then(() => true).catch(() => false);
8832
+ const unchanged = await ctx.read("git", ["diff", "--quiet", base, "--", ...manifests], { cwd: dir }).then(() => true).catch(() => false);
8825
8833
  if (unchanged)
8826
8834
  return;
8827
8835
  ctx.say(`\u2192 ${consumer.rel}: pnpm install (manifests changed)`);
8828
8836
  try {
8829
- await ctx.apply([run("mise", ["exec", "--", "pnpm", "install", "--reporter=silent"], { cwd: clone })]);
8837
+ await ctx.apply([run("mise", ["exec", "--", "pnpm", "install", "--reporter=silent"], { cwd: dir })]);
8830
8838
  } catch (error) {
8831
8839
  ctx.say(` \u2717 pnpm install failed${detail(error)}`);
8832
8840
  summary.failed.push(`${consumer.rel}/pnpm-lock.yaml`);
8833
8841
  }
8834
8842
  }
8835
- async function bumpConsumerRefs(ctx, consumer, clone, train) {
8836
- const dir = join8(clone, ".github", "workflows");
8837
- const entries = await ctx.readDir(dir).catch(() => []);
8843
+ async function bumpConsumerRefs(ctx, consumer, dir, train) {
8844
+ const workflows = join8(dir, ".github", "workflows");
8845
+ const entries = await ctx.readDir(workflows).catch(() => []);
8838
8846
  const files = entries.filter((e) => !e.directory && /\.ya?ml$/.test(e.name)).map((e) => e.name).sort();
8839
8847
  for (const f of files) {
8840
- const text = await ctx.readFile(join8(dir, f));
8848
+ const text = await ctx.readFile(join8(workflows, f));
8841
8849
  if (!text.includes(WORKFLOW_REF))
8842
8850
  continue;
8843
8851
  const bumped = bumpWorkflowRefs(text, train);
8844
8852
  if (bumped === text)
8845
8853
  continue;
8846
8854
  ctx.say(`\u2192 ${consumer.rel}: ${f} workflow refs \u2192 @${train}`);
8847
- await ctx.stage(write(join8(dir, f), bumped));
8855
+ await ctx.stage(write(join8(workflows, f), bumped));
8848
8856
  }
8849
8857
  }
8850
- async function openPull(ctx, consumer, clone, train, summary) {
8858
+ async function openPullUrl(ctx, consumer, dir, train) {
8859
+ const args = ["pr", "list", "--head", trainBranch(train), "--base", consumer.branch, "--state", "open"];
8860
+ return (await ctx.read("gh", [...args, "--json", "url", "--jq", ".[0].url"], { cwd: dir }).catch(() => "")).trim();
8861
+ }
8862
+ async function openPull(ctx, consumer, dir, take, summary) {
8863
+ const { train } = take;
8851
8864
  const branch = trainBranch(train);
8852
8865
  if (ctx.dryRun) {
8853
8866
  ctx.say(` (preview) ${consumer.rel}: would commit, push ${branch} and open a pull request against ${consumer.branch}`);
8854
8867
  return;
8855
8868
  }
8856
- if ((await ctx.read("git", ["status", "--porcelain"], { cwd: clone })).trim() === "") {
8869
+ if ((await ctx.read("git", ["status", "--porcelain"], { cwd: dir })).trim() === "") {
8857
8870
  ctx.say(`\u2022 ${consumer.rel}: nothing changed \u2014 no pull request`);
8858
8871
  return;
8859
8872
  }
8860
- const head = `${consumer.origin} ${branch}`;
8873
+ const head = `${consumer.origin ?? "origin"} ${branch}`;
8861
8874
  try {
8862
8875
  await ctx.apply([
8863
- run("git", ["add", "-A"], { cwd: clone }),
8864
- run("git", ["commit", "--quiet", "--no-verify", "-m", `chore: train ${train}`], { cwd: clone })
8876
+ run("git", ["add", "-A"], { cwd: dir }),
8877
+ run("git", ["commit", "--quiet", "--no-verify", "-m", `chore: train ${train}`], { cwd: dir })
8865
8878
  ]);
8866
8879
  await ctx.read("git", ["fetch", "--quiet", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`], {
8867
- cwd: clone
8880
+ cwd: dir
8868
8881
  }).catch(() => "");
8869
8882
  await ctx.apply([
8870
- run("git", ["push", "--quiet", "--force-with-lease", "origin", `HEAD:refs/heads/${branch}`], { cwd: clone })
8883
+ run("git", ["push", "--quiet", "--no-verify", "--force-with-lease", "origin", `HEAD:refs/heads/${branch}`], {
8884
+ cwd: dir
8885
+ })
8871
8886
  ]);
8872
8887
  } catch (error) {
8873
8888
  ctx.say(` \u2717 FAILED \u2014 push to ${head}${detail(error)}`);
8874
8889
  summary.failed.push(consumer.rel);
8875
8890
  return;
8876
8891
  }
8877
- const existing = (await ctx.read("gh", [
8878
- "pr",
8879
- "list",
8880
- "--head",
8881
- branch,
8882
- "--base",
8883
- consumer.branch,
8884
- "--state",
8885
- "open",
8886
- "--json",
8887
- "url",
8888
- "--jq",
8889
- ".[0].url"
8890
- ], { cwd: clone }).catch(() => "")).trim();
8892
+ const existing = await openPullUrl(ctx, consumer, dir, train);
8891
8893
  if (existing) {
8892
8894
  ctx.say(`\u2022 ${consumer.rel}: pull request updated: ${existing}`);
8893
8895
  summary.pulls.push(existing);
@@ -8898,7 +8900,7 @@ async function openPull(ctx, consumer, clone, train, summary) {
8898
8900
  const body2 = [
8899
8901
  `Train ${train} (\xA7 1.5 PLX): \`copier update\` at ${sites.join(", ")}, the marker, the lockfile where manifests moved, and the workflow refs.`,
8900
8902
  conflicted.length > 0 ? `Conflicts to resolve, committed with their markers: ${conflicted.join(", ")}.` : "Merged clean at every site.",
8901
- "Opened by `plx ship`."
8903
+ `Opened by ${take.opener}.`
8902
8904
  ].join("\n\n");
8903
8905
  try {
8904
8906
  await ctx.apply([
@@ -8913,29 +8915,14 @@ async function openPull(ctx, consumer, clone, train, summary) {
8913
8915
  `chore: train ${train}`,
8914
8916
  "--body",
8915
8917
  body2
8916
- ], { cwd: clone })
8918
+ ], { cwd: dir })
8917
8919
  ]);
8918
8920
  } catch (error) {
8919
8921
  ctx.say(` \u2717 FAILED \u2014 pull request${detail(error)}`);
8920
8922
  summary.failed.push(consumer.rel);
8921
8923
  return;
8922
8924
  }
8923
- const url = (await ctx.read("gh", [
8924
- "pr",
8925
- "list",
8926
- "--head",
8927
- branch,
8928
- "--base",
8929
- consumer.branch,
8930
- "--state",
8931
- "open",
8932
- "--json",
8933
- "url",
8934
- "--jq",
8935
- ".[0].url"
8936
- ], {
8937
- cwd: clone
8938
- }).catch(() => "")).trim();
8925
+ const url = await openPullUrl(ctx, consumer, dir, train);
8939
8926
  ctx.say(`\u2022 ${consumer.rel}: pull request opened${url ? `: ${url}` : ""}`);
8940
8927
  summary.pulls.push(url || `${consumer.rel} ${branch}`);
8941
8928
  }
@@ -9215,7 +9202,7 @@ async function awaitPublished(ctx, next, wait) {
9215
9202
  break;
9216
9203
  if (Date.now() - started >= wait.timeout)
9217
9204
  throw new Error(`${missing.join(", ")} not on npm at ${next} after ${Math.round(wait.timeout / 6e4)} min \u2014 check the release workflow (gh run list -R plexus-ms/${PRIMITIVES})`);
9218
- await new Promise((resolve10) => setTimeout(resolve10, wait.interval));
9205
+ await new Promise((resolve11) => setTimeout(resolve11, wait.interval));
9219
9206
  }
9220
9207
  ctx.say(` \u2713 ${list2} on npm`);
9221
9208
  }
@@ -9252,6 +9239,8 @@ async function planBookkeeping(ctx, train, repos2, next, today2) {
9252
9239
  await edit(r, f, `plx pin \u2192 ${next}`, (t) => bumpPlxPin(t, next));
9253
9240
  for (const f of await grep(r, `"${CONFIG_PIN}"`, ["*package.json", "*package.json.jinja"]))
9254
9241
  await edit(r, f, `${CONFIG_PIN} pin \u2192 ${next}`, (t) => bumpConfigPin(t, next));
9242
+ for (const f of await grep(r, `"${CONFIG_PIN}"`, ["*pnpm-workspace.yaml", "*pnpm-workspace.yaml.jinja"]))
9243
+ await edit(r, f, `${CONFIG_PIN} catalog entry \u2192 ${next}`, (t) => bumpCatalogPin(t, next));
9255
9244
  }
9256
9245
  const comp = repo("compendium");
9257
9246
  if (comp) {
@@ -9275,6 +9264,76 @@ async function planBookkeeping(ctx, train, repos2, next, today2) {
9275
9264
  return plan;
9276
9265
  }
9277
9266
 
9267
+ // ../lib/dist/update.js
9268
+ import { basename, join as join11 } from "node:path";
9269
+ var PRESETS = TRAIN_REPOS.filter((r) => r.startsWith("preset-"));
9270
+ async function update(ctx, options) {
9271
+ const { repo } = options;
9272
+ const name = basename(repo);
9273
+ if (options.train !== void 0 && !isTrainVersion(options.train))
9274
+ throw new UsageError(`'${options.train}' is not plain semver (X.Y.Z, no v)`);
9275
+ const tree = await overview(ctx, repo);
9276
+ const consumer = (await findConsumers(ctx, tree, PRESETS)).find((c) => c.path === tree.path);
9277
+ if (!consumer)
9278
+ throw new Error(`${repo} is not a repo generated from the presets: no ${ANSWERS_FILE} at its root naming one of ${PRESETS.map(presetSource).join(", ")}`);
9279
+ if (consumer.skipped)
9280
+ throw new Error(`${name}: ${consumer.skipped}`);
9281
+ consumer.rel = name;
9282
+ for (const s of consumer.sites)
9283
+ s.rel = s.rel === "." ? name : join11(name, s.rel);
9284
+ const root = consumer.sites.find((s) => s.path === consumer.path);
9285
+ if (!root)
9286
+ throw new Error(`${name}: its root is not a site of a repo preset`);
9287
+ await preflightForge(ctx);
9288
+ const train = options.train ?? await newestTrain(ctx, root.preset);
9289
+ const summary = emptySummary(train, ctx.dryRun);
9290
+ const recorded = await Promise.all(consumer.sites.map(async (s) => parseAnswers(await ctx.readFile(join11(s.path, ANSWERS_FILE))).commit));
9291
+ if (recorded.every((c) => c === train)) {
9292
+ ctx.say(`\u2022 ${name}: already at ${train}`);
9293
+ return summary;
9294
+ }
9295
+ const past = recorded.find((c) => c && isTrainVersion(c) && compareTrains(c, train) > 0);
9296
+ if (past)
9297
+ throw new Error(`${name} is at ${past}, past ${train}; a repo is never taken back to an older train`);
9298
+ const branch = trainBranch(train);
9299
+ const onOrigin = await ctx.read("git", ["ls-remote", "--exit-code", "--heads", "origin", `refs/heads/${branch}`], { cwd: repo }).then(() => true).catch(() => false);
9300
+ if (onOrigin) {
9301
+ const url = await openPullUrl(ctx, consumer, repo, train);
9302
+ ctx.say(`\u2022 ${name}: ${branch} is already on origin${url ? ` (${url})` : ""} \u2014 nothing to do; delete the branch to pull the train again`);
9303
+ if (url)
9304
+ summary.pulls.push(url);
9305
+ return summary;
9306
+ }
9307
+ const scratch = (await ctx.stage(run("mktemp", ["-d"])))?.trim() ?? "";
9308
+ const dir = join11(scratch, name);
9309
+ try {
9310
+ ctx.say(`\u2192 ${name}: worktree of origin/${consumer.branch}`);
9311
+ try {
9312
+ await ctx.stage(run("git", ["fetch", "--quiet", "origin", consumer.branch], { cwd: repo }));
9313
+ await ctx.stage(run("git", ["worktree", "add", "--quiet", "--detach", dir, `origin/${consumer.branch}`], { cwd: repo }));
9314
+ } catch (error) {
9315
+ ctx.say(` \u2717 FAILED \u2014 worktree of origin/${consumer.branch}${detail(error)}`);
9316
+ summary.failed.push(name);
9317
+ return summary;
9318
+ }
9319
+ await takeTrain(ctx, consumer, dir, { train, ref: train, opener: "`plx update`" }, summary);
9320
+ } finally {
9321
+ await ctx.stage(run("git", ["worktree", "remove", "--force", dir], { cwd: repo })).catch(() => void 0);
9322
+ if (scratch)
9323
+ await ctx.stage(run("rm", ["-rf", scratch]));
9324
+ }
9325
+ return summary;
9326
+ }
9327
+ async function newestTrain(ctx, preset) {
9328
+ const source = presetSource(preset).replace(/^gh:/, "");
9329
+ const tag = (await ctx.read("gh", ["api", `repos/${source}/releases/latest`, "--jq", ".tag_name"]).catch(() => "")).trim();
9330
+ if (!tag)
9331
+ throw new Error(`no release of ${source} found \u2014 pass the train explicitly (--train <version>)`);
9332
+ if (!isTrainVersion(tag))
9333
+ throw new Error(`the newest release of ${source} is '${tag}', not a train`);
9334
+ return tag;
9335
+ }
9336
+
9278
9337
  // src/program.ts
9279
9338
  import { createRequire } from "node:module";
9280
9339
 
@@ -10221,13 +10280,13 @@ import { AsyncResource as AsyncResource3 } from "node:async_hooks";
10221
10280
 
10222
10281
  // ../../node_modules/.pnpm/@inquirer+type@4.2.1_@types+node@26.6.5/node_modules/@inquirer/type/dist/utils.js
10223
10282
  function withResolver() {
10224
- let resolve10;
10283
+ let resolve11;
10225
10284
  let reject;
10226
10285
  const promise = new Promise((res, rej) => {
10227
- resolve10 = res;
10286
+ resolve11 = res;
10228
10287
  reject = rej;
10229
10288
  });
10230
- return { promise, resolve: resolve10, reject };
10289
+ return { promise, resolve: resolve11, reject };
10231
10290
  }
10232
10291
 
10233
10292
  // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/create-prompt.js
@@ -10606,7 +10665,7 @@ function createPrompt(view) {
10606
10665
  });
10607
10666
  output.mute();
10608
10667
  const screen = new ScreenManager(rl);
10609
- const { promise, resolve: resolve10, reject } = withResolver();
10668
+ const { promise, resolve: resolve11, reject } = withResolver();
10610
10669
  return withHooks(rl, (cycle) => {
10611
10670
  const clearEffects = AsyncResource3.bind(() => effectScheduler.clearAll());
10612
10671
  const settlePrompt = (settle) => {
@@ -10617,7 +10676,7 @@ function createPrompt(view) {
10617
10676
  reject(error);
10618
10677
  }
10619
10678
  };
10620
- const resolvePrompt = (value) => settlePrompt(() => resolve10(value));
10679
+ const resolvePrompt = (value) => settlePrompt(() => resolve11(value));
10621
10680
  const rejectPrompt = (error) => settlePrompt(() => reject(error));
10622
10681
  const promptPromise = Object.assign(promise.finally(() => {
10623
10682
  cleanups.forEach((cleanup) => cleanup());
@@ -14206,7 +14265,7 @@ function register4(program2) {
14206
14265
 
14207
14266
  // src/commands/doctor.ts
14208
14267
  import { existsSync } from "node:fs";
14209
- import { join as join11, resolve as resolve5 } from "node:path";
14268
+ import { join as join12, resolve as resolve5 } from "node:path";
14210
14269
 
14211
14270
  // src/style.ts
14212
14271
  function style(enabled) {
@@ -14224,8 +14283,8 @@ function register5(program2) {
14224
14283
  ).option("--color", "colour the report even when piped").option("--no-color", "never colour the report").action(async (dir, opts, command) => {
14225
14284
  const ctx = contextOf2(command);
14226
14285
  const root = resolve5(dir);
14227
- const compendium = resolve5(opts.compendium ?? join11(root, WORKSPACE_COMPENDIUM));
14228
- if (!existsSync(join11(compendium, "contracts")))
14286
+ const compendium = resolve5(opts.compendium ?? join12(root, WORKSPACE_COMPENDIUM));
14287
+ if (!existsSync(join12(compendium, "contracts")))
14229
14288
  throw new Error(
14230
14289
  `no compendium at ${compendium}; pass --compendium <path to a checkout of plexus-ms/compendium>`
14231
14290
  );
@@ -14315,7 +14374,7 @@ function register7(program2) {
14315
14374
  if (!isClean(result)) process.exitCode = 1;
14316
14375
  });
14317
14376
  }
14318
- function renderSummary(s, realCommand) {
14377
+ function renderSummary(s, realCommand, verb = "propagate") {
14319
14378
  const lines = ["\u2500\u2500 summary \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"];
14320
14379
  for (const p of s.clean) lines.push(` \u2713 clean: ${p}`);
14321
14380
  for (const p of s.conflicted) lines.push(` \u2717 conflicts: ${p}`);
@@ -14329,7 +14388,7 @@ function renderSummary(s, realCommand) {
14329
14388
  } else if (clean) {
14330
14389
  lines.push("\u2713 done \u2014 review and merge the pull requests");
14331
14390
  }
14332
- if (!clean) lines.push("\u2717 needs attention \u2014 see above; fix the consumer, then re-run propagate");
14391
+ if (!clean) lines.push(`\u2717 needs attention \u2014 see above; fix the consumer, then re-run ${verb}`);
14333
14392
  return lines.join("\n");
14334
14393
  }
14335
14394
 
@@ -14344,13 +14403,13 @@ function register8(program2) {
14344
14403
  }
14345
14404
 
14346
14405
  // src/commands/requirements.ts
14347
- import { join as join12, resolve as resolve9 } from "node:path";
14406
+ import { join as join13, resolve as resolve9 } from "node:path";
14348
14407
  function register9(program2) {
14349
14408
  program2.command("requirements").description("lint the standard's map and module pages, or extract one module's requirement lines").argument("[dir]", "a checkout of the compendium", ".").option("--extract <page>", "print a module page's requirement lines instead of linting (path relative to dir)").action(async (dir, opts, command) => {
14350
14409
  const ctx = contextOf2(command);
14351
14410
  const compendium = resolve9(dir);
14352
14411
  if (opts.extract) {
14353
- ctx.result(extractRequirements(await ctx.readFile(join12(compendium, opts.extract))));
14412
+ ctx.result(extractRequirements(await ctx.readFile(join13(compendium, opts.extract))));
14354
14413
  return;
14355
14414
  }
14356
14415
  const check = await checkCompendium(ctx, compendium);
@@ -14363,12 +14422,26 @@ function renderCheck(c) {
14363
14422
  return `\u2713 the map, ${c.contracts} contracts and ${c.approaches} approaches are well-formed`;
14364
14423
  }
14365
14424
 
14425
+ // src/commands/update.ts
14426
+ import { resolve as resolve10 } from "node:path";
14427
+ function register10(program2) {
14428
+ program2.command("update").description(
14429
+ "bring this repo up to a train, as a pull request on plexus/<version>: copier update at every site, the marker, the lockfile, the workflow refs"
14430
+ ).argument("[dir]", "a repo generated from the presets", ".").option("--train <version>", "the train to take (default: the preset's newest release)").action(async (dir, opts, command) => {
14431
+ const ctx = contextOf2(command);
14432
+ const result = await update(ctx, { repo: resolve10(dir), train: opts.train });
14433
+ ctx.result(result, (r) => renderSummary(r, `plx -y update --train ${r.train} ${dir}`, "update"));
14434
+ if (!isClean(result)) process.exitCode = 1;
14435
+ });
14436
+ }
14437
+
14366
14438
  // src/commands/index.ts
14367
14439
  var commands = [
14368
14440
  register6,
14369
14441
  register5,
14370
14442
  register7,
14371
14443
  register8,
14444
+ register10,
14372
14445
  register9,
14373
14446
  register2,
14374
14447
  register,
@@ -14383,7 +14456,7 @@ function buildProgram() {
14383
14456
  program2.addOption(new Option("-n, --dry-run", "print every effect this run would have, perform none").env("PLX_DRY_RUN")).addOption(
14384
14457
  new Option("-v, --verbose", "say more: once for reads and inputs, twice for command output").argParser((_value, previous) => previous + 1).default(0)
14385
14458
  ).addOption(new Option("-q, --quiet", "say nothing but results and errors").env("PLX_QUIET")).addOption(new Option("-y, --yes", "answer every confirmation with yes (needed off a terminal)").env("PLX_YES")).addOption(new Option("--json", "print results as JSON").env("PLX_JSON"));
14386
- for (const register10 of commands) register10(program2);
14459
+ for (const register11 of commands) register11(program2);
14387
14460
  return program2;
14388
14461
  }
14389
14462
  function contextOf2(command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexus-ms/cli",
3
- "version": "0.100.0",
3
+ "version": "0.102.0",
4
4
  "description": "plx, the Plexus command-line tool: a single-script adapter over @plexus-ms/lib",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "type": "module",
@@ -23,8 +23,8 @@
23
23
  "node": ">=24.22.1"
24
24
  },
25
25
  "dependencies": {
26
- "@inquirer/prompts": "^8.7.4",
27
- "@plexus-ms/lib": "0.100.0",
26
+ "@inquirer/prompts": "^8.7.5",
27
+ "@plexus-ms/lib": "0.102.0",
28
28
  "commander": "^15.0.0"
29
29
  },
30
30
  "devDependencies": {