@plexus-ms/cli 0.101.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 +172 -104
  2. package/package.json +3 -3
package/dist/plx.js CHANGED
@@ -7761,7 +7761,7 @@ function realIo(prompt) {
7761
7761
  stdout: process.stdout,
7762
7762
  stderr: process.stderr,
7763
7763
  prompt: prompt && isInteractive() ? prompt : void 0,
7764
- exec: (command, args, options = {}) => new Promise((resolve10, reject) => {
7764
+ exec: (command, args, options = {}) => new Promise((resolve11, reject) => {
7765
7765
  const child = spawn(command, args, {
7766
7766
  cwd: options.cwd,
7767
7767
  env: options.env,
@@ -7784,7 +7784,7 @@ function realIo(prompt) {
7784
7784
  child.on("error", reject);
7785
7785
  child.on("close", (code) => {
7786
7786
  if (code === 0)
7787
- resolve10(stdout);
7787
+ resolve11(stdout);
7788
7788
  else
7789
7789
  reject(new ExecError(render(command, args), code, stderr));
7790
7790
  });
@@ -8683,22 +8683,22 @@ async function templateFacet(ctx, cwd) {
8683
8683
  // ../lib/dist/propagate.js
8684
8684
  import { join as join8, relative as relative3 } from "node:path";
8685
8685
  var isClean = (s) => s.conflicted.length === 0 && s.failed.length === 0 && s.skipped.length === 0;
8686
- var BASE_TAG = "ship-base";
8687
- var trainBranch = (train) => `train/${train}`;
8688
- 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) => [
8689
8697
  "env",
8690
- [`MISE_IGNORED_CONFIG_PATHS=${clone}`, "mise", ...copierVia(args)]
8698
+ [`MISE_IGNORED_CONFIG_PATHS=${dir}`, "mise", ...copierVia(args)]
8691
8699
  ];
8692
8700
  async function propagate(ctx, workspace, train) {
8693
- const summary = {
8694
- train,
8695
- preview: ctx.dryRun,
8696
- clean: [],
8697
- conflicted: [],
8698
- failed: [],
8699
- skipped: [],
8700
- pulls: []
8701
- };
8701
+ const summary = emptySummary(train, ctx.dryRun);
8702
8702
  const upstream = join8(workspace, UPSTREAM);
8703
8703
  const tree = await overview(ctx, workspace);
8704
8704
  const upstreamDir = tree.children.find((c) => c.path === upstream);
@@ -8713,6 +8713,7 @@ async function propagate(ctx, workspace, train) {
8713
8713
  for (const preset of presets)
8714
8714
  if (!consumers.some((c) => c.sites.some((s) => s.preset === preset)))
8715
8715
  ctx.say(`\u2022 no consumers of ${presetSource(preset)} found under ${workspace}`);
8716
+ const take = { train, opener: "`plx ship`", ...ctx.dryRun ? { previewFrom: upstream } : {} };
8716
8717
  const scratch = (await ctx.stage(run("mktemp", ["-d"])))?.trim() ?? "";
8717
8718
  try {
8718
8719
  for (const consumer of consumers) {
@@ -8722,17 +8723,12 @@ async function propagate(ctx, workspace, train) {
8722
8723
  ctx.say(`\u2192 ${consumer.rel}: clone ${consumer.origin} at ${consumer.branch}`);
8723
8724
  try {
8724
8725
  await ctx.stage(run("git", ["clone", "--quiet", "--branch", consumer.branch, consumer.origin, clone]));
8725
- await ctx.stage(run("git", ["tag", BASE_TAG], { cwd: clone }));
8726
8726
  } catch (error) {
8727
8727
  ctx.say(` \u2717 FAILED \u2014 clone${detail(error)}`);
8728
8728
  summary.failed.push(consumer.rel);
8729
8729
  continue;
8730
8730
  }
8731
- for (const site of consumer.sites)
8732
- await updateSite(ctx, consumer, site, clone, upstream, summary);
8733
- await refreshLockfile(ctx, consumer, clone, summary);
8734
- await bumpConsumerRefs(ctx, consumer, clone, train);
8735
- await openPull(ctx, consumer, clone, train, summary);
8731
+ await takeTrain(ctx, consumer, clone, take, summary);
8736
8732
  }
8737
8733
  } finally {
8738
8734
  if (scratch)
@@ -8740,7 +8736,15 @@ async function propagate(ctx, workspace, train) {
8740
8736
  }
8741
8737
  return summary;
8742
8738
  }
8743
- 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", [
8744
8748
  "-c",
8745
8749
  "commit.gpgsign=false",
8746
8750
  "-c",
@@ -8753,31 +8757,32 @@ var previewCommit = (ctx, clone, message) => ctx.stage(run("git", ["add", "-A"],
8753
8757
  "--allow-empty",
8754
8758
  "-m",
8755
8759
  message
8756
- ], { cwd: clone })));
8757
- async function updateSite(ctx, consumer, site, clone, upstream, summary) {
8758
- const target = join8(clone, relative3(consumer.path, site.path));
8759
- 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));
8760
8763
  try {
8761
- if (ctx.dryRun) {
8764
+ if (take.previewFrom) {
8765
+ const localPreset = join8(take.previewFrom, site.preset);
8762
8766
  ctx.say(`\u2192 ${site.rel}: copier update (preview from ${localPreset}@main)`);
8763
8767
  const answers = await ctx.readFile(join8(target, ANSWERS_FILE));
8764
8768
  await ctx.stage(write(join8(target, ANSWERS_FILE), answers.replace(/^_src_path: .*$/m, `_src_path: ${localPreset}`)));
8765
- await previewCommit(ctx, clone, `plx preview: ${site.rel} reads ${localPreset} from the local checkout`);
8766
- 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 = [
8767
8774
  "update",
8768
8775
  "--skip-answered",
8769
8776
  "--trust",
8770
- "--defaults",
8771
8777
  "--skip-tasks",
8772
- "--vcs-ref",
8773
- "main"
8774
- ]), {
8775
- cwd: target
8776
- }));
8777
- } else {
8778
- ctx.say(`\u2192 ${site.rel}: copier update`);
8779
- const args = ["update", "--skip-answered", "--trust", "--skip-tasks", ...ctx.yes ? ["--defaults"] : []];
8780
- 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]);
8781
8786
  }
8782
8787
  } catch (error) {
8783
8788
  ctx.say(` \u2717 FAILED \u2014 copier update exited nonzero${detail(error)}`);
@@ -8802,10 +8807,10 @@ async function updateSite(ctx, consumer, site, clone, upstream, summary) {
8802
8807
  }
8803
8808
  for (const line of status.split("\n").filter(Boolean))
8804
8809
  ctx.say(` ${line}`);
8805
- if (ctx.dryRun)
8806
- await previewCommit(ctx, clone, `plx preview: ${site.rel} updated`);
8810
+ if (take.previewFrom)
8811
+ await previewCommit(ctx, dir, `plx preview: ${site.rel} updated`);
8807
8812
  else
8808
- await ctx.stage(run("git", ["add", "-A"], { cwd: clone }));
8813
+ await ctx.stage(run("git", ["add", "-A"], { cwd: dir }));
8809
8814
  }
8810
8815
  async function syncSiteMarker(ctx, site, target) {
8811
8816
  const marker = await ctx.readFile(join8(target, MARKER_FILE)).catch(() => void 0);
@@ -8820,77 +8825,71 @@ async function syncSiteMarker(ctx, site, target) {
8820
8825
  ctx.say(` marker: plexus \u2192 ${commit} in ${site.rel}/${MARKER_FILE}`);
8821
8826
  await ctx.stage(write(join8(target, MARKER_FILE), synced));
8822
8827
  }
8823
- async function refreshLockfile(ctx, consumer, clone, summary) {
8824
- 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)
8825
8830
  return;
8826
8831
  const manifests = ["*package.json", "pnpm-workspace.yaml"];
8827
- 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);
8828
8833
  if (unchanged)
8829
8834
  return;
8830
8835
  ctx.say(`\u2192 ${consumer.rel}: pnpm install (manifests changed)`);
8831
8836
  try {
8832
- 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 })]);
8833
8838
  } catch (error) {
8834
8839
  ctx.say(` \u2717 pnpm install failed${detail(error)}`);
8835
8840
  summary.failed.push(`${consumer.rel}/pnpm-lock.yaml`);
8836
8841
  }
8837
8842
  }
8838
- async function bumpConsumerRefs(ctx, consumer, clone, train) {
8839
- const dir = join8(clone, ".github", "workflows");
8840
- 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(() => []);
8841
8846
  const files = entries.filter((e) => !e.directory && /\.ya?ml$/.test(e.name)).map((e) => e.name).sort();
8842
8847
  for (const f of files) {
8843
- const text = await ctx.readFile(join8(dir, f));
8848
+ const text = await ctx.readFile(join8(workflows, f));
8844
8849
  if (!text.includes(WORKFLOW_REF))
8845
8850
  continue;
8846
8851
  const bumped = bumpWorkflowRefs(text, train);
8847
8852
  if (bumped === text)
8848
8853
  continue;
8849
8854
  ctx.say(`\u2192 ${consumer.rel}: ${f} workflow refs \u2192 @${train}`);
8850
- await ctx.stage(write(join8(dir, f), bumped));
8855
+ await ctx.stage(write(join8(workflows, f), bumped));
8851
8856
  }
8852
8857
  }
8853
- 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;
8854
8864
  const branch = trainBranch(train);
8855
8865
  if (ctx.dryRun) {
8856
8866
  ctx.say(` (preview) ${consumer.rel}: would commit, push ${branch} and open a pull request against ${consumer.branch}`);
8857
8867
  return;
8858
8868
  }
8859
- if ((await ctx.read("git", ["status", "--porcelain"], { cwd: clone })).trim() === "") {
8869
+ if ((await ctx.read("git", ["status", "--porcelain"], { cwd: dir })).trim() === "") {
8860
8870
  ctx.say(`\u2022 ${consumer.rel}: nothing changed \u2014 no pull request`);
8861
8871
  return;
8862
8872
  }
8863
- const head = `${consumer.origin} ${branch}`;
8873
+ const head = `${consumer.origin ?? "origin"} ${branch}`;
8864
8874
  try {
8865
8875
  await ctx.apply([
8866
- run("git", ["add", "-A"], { cwd: clone }),
8867
- 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 })
8868
8878
  ]);
8869
8879
  await ctx.read("git", ["fetch", "--quiet", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`], {
8870
- cwd: clone
8880
+ cwd: dir
8871
8881
  }).catch(() => "");
8872
8882
  await ctx.apply([
8873
- 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
+ })
8874
8886
  ]);
8875
8887
  } catch (error) {
8876
8888
  ctx.say(` \u2717 FAILED \u2014 push to ${head}${detail(error)}`);
8877
8889
  summary.failed.push(consumer.rel);
8878
8890
  return;
8879
8891
  }
8880
- const existing = (await ctx.read("gh", [
8881
- "pr",
8882
- "list",
8883
- "--head",
8884
- branch,
8885
- "--base",
8886
- consumer.branch,
8887
- "--state",
8888
- "open",
8889
- "--json",
8890
- "url",
8891
- "--jq",
8892
- ".[0].url"
8893
- ], { cwd: clone }).catch(() => "")).trim();
8892
+ const existing = await openPullUrl(ctx, consumer, dir, train);
8894
8893
  if (existing) {
8895
8894
  ctx.say(`\u2022 ${consumer.rel}: pull request updated: ${existing}`);
8896
8895
  summary.pulls.push(existing);
@@ -8901,7 +8900,7 @@ async function openPull(ctx, consumer, clone, train, summary) {
8901
8900
  const body2 = [
8902
8901
  `Train ${train} (\xA7 1.5 PLX): \`copier update\` at ${sites.join(", ")}, the marker, the lockfile where manifests moved, and the workflow refs.`,
8903
8902
  conflicted.length > 0 ? `Conflicts to resolve, committed with their markers: ${conflicted.join(", ")}.` : "Merged clean at every site.",
8904
- "Opened by `plx ship`."
8903
+ `Opened by ${take.opener}.`
8905
8904
  ].join("\n\n");
8906
8905
  try {
8907
8906
  await ctx.apply([
@@ -8916,29 +8915,14 @@ async function openPull(ctx, consumer, clone, train, summary) {
8916
8915
  `chore: train ${train}`,
8917
8916
  "--body",
8918
8917
  body2
8919
- ], { cwd: clone })
8918
+ ], { cwd: dir })
8920
8919
  ]);
8921
8920
  } catch (error) {
8922
8921
  ctx.say(` \u2717 FAILED \u2014 pull request${detail(error)}`);
8923
8922
  summary.failed.push(consumer.rel);
8924
8923
  return;
8925
8924
  }
8926
- const url = (await ctx.read("gh", [
8927
- "pr",
8928
- "list",
8929
- "--head",
8930
- branch,
8931
- "--base",
8932
- consumer.branch,
8933
- "--state",
8934
- "open",
8935
- "--json",
8936
- "url",
8937
- "--jq",
8938
- ".[0].url"
8939
- ], {
8940
- cwd: clone
8941
- }).catch(() => "")).trim();
8925
+ const url = await openPullUrl(ctx, consumer, dir, train);
8942
8926
  ctx.say(`\u2022 ${consumer.rel}: pull request opened${url ? `: ${url}` : ""}`);
8943
8927
  summary.pulls.push(url || `${consumer.rel} ${branch}`);
8944
8928
  }
@@ -9218,7 +9202,7 @@ async function awaitPublished(ctx, next, wait) {
9218
9202
  break;
9219
9203
  if (Date.now() - started >= wait.timeout)
9220
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})`);
9221
- await new Promise((resolve10) => setTimeout(resolve10, wait.interval));
9205
+ await new Promise((resolve11) => setTimeout(resolve11, wait.interval));
9222
9206
  }
9223
9207
  ctx.say(` \u2713 ${list2} on npm`);
9224
9208
  }
@@ -9280,6 +9264,76 @@ async function planBookkeeping(ctx, train, repos2, next, today2) {
9280
9264
  return plan;
9281
9265
  }
9282
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
+
9283
9337
  // src/program.ts
9284
9338
  import { createRequire } from "node:module";
9285
9339
 
@@ -10226,13 +10280,13 @@ import { AsyncResource as AsyncResource3 } from "node:async_hooks";
10226
10280
 
10227
10281
  // ../../node_modules/.pnpm/@inquirer+type@4.2.1_@types+node@26.6.5/node_modules/@inquirer/type/dist/utils.js
10228
10282
  function withResolver() {
10229
- let resolve10;
10283
+ let resolve11;
10230
10284
  let reject;
10231
10285
  const promise = new Promise((res, rej) => {
10232
- resolve10 = res;
10286
+ resolve11 = res;
10233
10287
  reject = rej;
10234
10288
  });
10235
- return { promise, resolve: resolve10, reject };
10289
+ return { promise, resolve: resolve11, reject };
10236
10290
  }
10237
10291
 
10238
10292
  // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/create-prompt.js
@@ -10611,7 +10665,7 @@ function createPrompt(view) {
10611
10665
  });
10612
10666
  output.mute();
10613
10667
  const screen = new ScreenManager(rl);
10614
- const { promise, resolve: resolve10, reject } = withResolver();
10668
+ const { promise, resolve: resolve11, reject } = withResolver();
10615
10669
  return withHooks(rl, (cycle) => {
10616
10670
  const clearEffects = AsyncResource3.bind(() => effectScheduler.clearAll());
10617
10671
  const settlePrompt = (settle) => {
@@ -10622,7 +10676,7 @@ function createPrompt(view) {
10622
10676
  reject(error);
10623
10677
  }
10624
10678
  };
10625
- const resolvePrompt = (value) => settlePrompt(() => resolve10(value));
10679
+ const resolvePrompt = (value) => settlePrompt(() => resolve11(value));
10626
10680
  const rejectPrompt = (error) => settlePrompt(() => reject(error));
10627
10681
  const promptPromise = Object.assign(promise.finally(() => {
10628
10682
  cleanups.forEach((cleanup) => cleanup());
@@ -14211,7 +14265,7 @@ function register4(program2) {
14211
14265
 
14212
14266
  // src/commands/doctor.ts
14213
14267
  import { existsSync } from "node:fs";
14214
- import { join as join11, resolve as resolve5 } from "node:path";
14268
+ import { join as join12, resolve as resolve5 } from "node:path";
14215
14269
 
14216
14270
  // src/style.ts
14217
14271
  function style(enabled) {
@@ -14229,8 +14283,8 @@ function register5(program2) {
14229
14283
  ).option("--color", "colour the report even when piped").option("--no-color", "never colour the report").action(async (dir, opts, command) => {
14230
14284
  const ctx = contextOf2(command);
14231
14285
  const root = resolve5(dir);
14232
- const compendium = resolve5(opts.compendium ?? join11(root, WORKSPACE_COMPENDIUM));
14233
- if (!existsSync(join11(compendium, "contracts")))
14286
+ const compendium = resolve5(opts.compendium ?? join12(root, WORKSPACE_COMPENDIUM));
14287
+ if (!existsSync(join12(compendium, "contracts")))
14234
14288
  throw new Error(
14235
14289
  `no compendium at ${compendium}; pass --compendium <path to a checkout of plexus-ms/compendium>`
14236
14290
  );
@@ -14320,7 +14374,7 @@ function register7(program2) {
14320
14374
  if (!isClean(result)) process.exitCode = 1;
14321
14375
  });
14322
14376
  }
14323
- function renderSummary(s, realCommand) {
14377
+ function renderSummary(s, realCommand, verb = "propagate") {
14324
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"];
14325
14379
  for (const p of s.clean) lines.push(` \u2713 clean: ${p}`);
14326
14380
  for (const p of s.conflicted) lines.push(` \u2717 conflicts: ${p}`);
@@ -14334,7 +14388,7 @@ function renderSummary(s, realCommand) {
14334
14388
  } else if (clean) {
14335
14389
  lines.push("\u2713 done \u2014 review and merge the pull requests");
14336
14390
  }
14337
- 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}`);
14338
14392
  return lines.join("\n");
14339
14393
  }
14340
14394
 
@@ -14349,13 +14403,13 @@ function register8(program2) {
14349
14403
  }
14350
14404
 
14351
14405
  // src/commands/requirements.ts
14352
- import { join as join12, resolve as resolve9 } from "node:path";
14406
+ import { join as join13, resolve as resolve9 } from "node:path";
14353
14407
  function register9(program2) {
14354
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) => {
14355
14409
  const ctx = contextOf2(command);
14356
14410
  const compendium = resolve9(dir);
14357
14411
  if (opts.extract) {
14358
- ctx.result(extractRequirements(await ctx.readFile(join12(compendium, opts.extract))));
14412
+ ctx.result(extractRequirements(await ctx.readFile(join13(compendium, opts.extract))));
14359
14413
  return;
14360
14414
  }
14361
14415
  const check = await checkCompendium(ctx, compendium);
@@ -14368,12 +14422,26 @@ function renderCheck(c) {
14368
14422
  return `\u2713 the map, ${c.contracts} contracts and ${c.approaches} approaches are well-formed`;
14369
14423
  }
14370
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
+
14371
14438
  // src/commands/index.ts
14372
14439
  var commands = [
14373
14440
  register6,
14374
14441
  register5,
14375
14442
  register7,
14376
14443
  register8,
14444
+ register10,
14377
14445
  register9,
14378
14446
  register2,
14379
14447
  register,
@@ -14388,7 +14456,7 @@ function buildProgram() {
14388
14456
  program2.addOption(new Option("-n, --dry-run", "print every effect this run would have, perform none").env("PLX_DRY_RUN")).addOption(
14389
14457
  new Option("-v, --verbose", "say more: once for reads and inputs, twice for command output").argParser((_value, previous) => previous + 1).default(0)
14390
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"));
14391
- for (const register10 of commands) register10(program2);
14459
+ for (const register11 of commands) register11(program2);
14392
14460
  return program2;
14393
14461
  }
14394
14462
  function contextOf2(command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexus-ms/cli",
3
- "version": "0.101.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.101.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": {