@plexus-ms/cli 0.9.0 → 0.100.0-rc.3

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 +350 -174
  2. package/package.json +3 -3
package/dist/plx.js CHANGED
@@ -7567,9 +7567,10 @@ function frontmatterField(text, key) {
7567
7567
 
7568
7568
  // ../lib/dist/bookkeeping.js
7569
7569
  var bumpGalaxyVersion = (text, train) => text.replace(/^(version: )\d+\.\d+\.\d+/m, `$1${train}`);
7570
+ var COLLECTION_SOURCE = "plexus-ms/primitives.git";
7570
7571
  function bumpCollectionPin(text, train) {
7571
7572
  const lines = text.split("\n");
7572
- const start = lines.findIndex((l) => l.includes("plexus-ms/platform.git"));
7573
+ const start = lines.findIndex((l) => l.includes(COLLECTION_SOURCE));
7573
7574
  if (start === -1)
7574
7575
  return text;
7575
7576
  for (let i = start; i < lines.length; i++) {
@@ -7581,8 +7582,13 @@ function bumpCollectionPin(text, train) {
7581
7582
  }
7582
7583
  return lines.join("\n");
7583
7584
  }
7584
- var WORKFLOW_REF = "plexus-ms/ci-cd/.github/workflows/";
7585
- var bumpWorkflowRefs = (text, train) => text.replace(/(plexus-ms\/ci-cd\/\.github\/workflows\/[A-Za-z0-9._-]+)@v?[0-9.]+/g, `$1@${train}`);
7585
+ var WORKFLOW_REF = "plexus-ms/primitives/.github/workflows/";
7586
+ var bumpWorkflowRefs = (text, train) => text.replace(/(plexus-ms\/primitives\/\.github\/workflows\/[A-Za-z0-9._-]+)@v?[0-9.]+/g, `$1@${train}`);
7587
+ var bumpManifestVersion = (text, train) => text.replace(/^(\s*"version":\s*")[^"]*(")/m, `$1${train}$2`);
7588
+ var PLX_PIN = "npm:@plexus-ms/cli";
7589
+ var bumpPlxPin = (text, train) => text.replace(/("npm:@plexus-ms\/cli"\s*=\s*(?:\{\s*version\s*=\s*)?")[^"]*(")/g, `$1${train}$2`);
7590
+ var CONFIG_PIN = "@plexus-ms/config";
7591
+ var bumpConfigPin = (text, train) => text.replace(/("@plexus-ms\/config":\s*")(?!workspace:|catalog:)[^"]*(")/g, `$1${train}$2`);
7586
7592
  var stampStandard = (text, train, today2) => setFrontmatterField(setFrontmatterField(text, "version", train), "timestamp", today2);
7587
7593
  var stampChanged = (text, train) => setFrontmatterField(text, "changed", train, { after: "binds" });
7588
7594
  var hasConflictMarkers = (text) => /^<<<<<<< /m.test(text);
@@ -7968,13 +7974,13 @@ async function expand(ctx, root, glob) {
7968
7974
 
7969
7975
  // ../lib/dist/consumers.js
7970
7976
  import { relative } from "node:path";
7971
- function expectedBranch(declaration) {
7977
+ function expectedBranch(declaration, answers = {}) {
7972
7978
  if (!declaration)
7973
7979
  return void 0;
7974
7980
  if (declaration.modules.includes("PLX-Platform"))
7975
7981
  return "main";
7976
7982
  if (declaration.modules.includes("PLX-Repo"))
7977
- return "develop";
7983
+ return answers.publishes_packages === true ? "main" : "develop";
7978
7984
  return void 0;
7979
7985
  }
7980
7986
  var presetSource = (preset) => `gh:plexus-ms/${preset}`;
@@ -7993,11 +7999,12 @@ async function findConsumers(ctx, root, presets) {
7993
7999
  else {
7994
8000
  let consumer = consumers.get(here.path);
7995
8001
  if (!consumer) {
8002
+ const expected = expectedBranch(here.declaration, here.copier?.answers);
7996
8003
  consumer = {
7997
8004
  path: here.path,
7998
8005
  rel: relative(root.path, here.path) || ".",
7999
- branch: here.git?.branch ?? "?",
8000
- expected: expectedBranch(here.declaration),
8006
+ branch: expected ?? here.git?.branch ?? "main",
8007
+ expected,
8001
8008
  sites: []
8002
8009
  };
8003
8010
  consumers.set(here.path, consumer);
@@ -8012,13 +8019,11 @@ async function findConsumers(ctx, root, presets) {
8012
8019
  for (const site of orphans)
8013
8020
  ctx.say(`\u26A0 ${site.rel}: not inside a git repo \u2014 skipped`);
8014
8021
  for (const c of consumers.values()) {
8015
- if (c.expected && c.branch !== c.expected) {
8016
- c.skipped = `on '${c.branch}', expected '${c.expected}'`;
8017
- continue;
8018
- }
8019
- const tracked = (await ctx.read("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: c.path })).trim();
8020
- if (tracked !== "")
8021
- c.skipped = "has uncommitted changes";
8022
+ const origin = (await ctx.read("git", ["remote", "get-url", "origin"], { cwd: c.path }).catch(() => "")).trim();
8023
+ if (origin === "")
8024
+ c.skipped = "no origin remote";
8025
+ else
8026
+ c.origin = origin;
8022
8027
  }
8023
8028
  return [...consumers.values()];
8024
8029
  }
@@ -8222,14 +8227,20 @@ function compareTrains(a, b) {
8222
8227
  }
8223
8228
 
8224
8229
  // ../lib/dist/train.js
8230
+ var PRIMITIVES = "primitives";
8225
8231
  var TRAIN_REPOS = [
8226
- "platform",
8227
- "ci-cd",
8232
+ PRIMITIVES,
8228
8233
  "preset-repo-web",
8229
8234
  "preset-app-nextjs",
8230
8235
  "preset-repo-platform",
8231
8236
  "compendium"
8232
8237
  ];
8238
+ var PHASES = [[PRIMITIVES], TRAIN_REPOS.filter((r) => r !== PRIMITIVES)];
8239
+ var TRAIN_PACKAGES = [
8240
+ { name: "@plexus-ms/cli", dir: "packages/cli" },
8241
+ { name: "@plexus-ms/lib", dir: "packages/lib" },
8242
+ { name: "@plexus-ms/config", dir: "packages/config" }
8243
+ ];
8233
8244
  var UPSTREAM = "plexus-ms";
8234
8245
  var isTrainVersion = (v) => /^\d+\.\d+\.\d+$/.test(v);
8235
8246
  function nextMinor(version2) {
@@ -8265,6 +8276,7 @@ async function readTrain(ctx, upstream) {
8265
8276
  const latest = repos2.map((r) => r.latest);
8266
8277
  return { repos: repos2, last: latestTag([...all]), skew: latest.some((l) => l !== latest[0]) };
8267
8278
  }
8279
+ var phasesOf = (train) => PHASES.map((names) => train.repos.filter((r) => names.includes(r.name)));
8268
8280
  async function preflightRelease(ctx, train) {
8269
8281
  for (const { name, path: path3 } of train.repos) {
8270
8282
  const branch = (await ctx.read("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path3 })).trim();
@@ -8277,6 +8289,13 @@ async function preflightRelease(ctx, train) {
8277
8289
  throw new Error(`${name} is ${behind} commit(s) behind origin/main \u2014 pull first, or the tag lands on a stale commit`);
8278
8290
  }
8279
8291
  }
8292
+ async function preflightForge(ctx) {
8293
+ try {
8294
+ await ctx.read("gh", ["auth", "status"]);
8295
+ } catch {
8296
+ throw new Error("gh is not logged in (gh auth status failed) \u2014 run gh auth login first");
8297
+ }
8298
+ }
8280
8299
  function resolveVersion(train, explicit, propagateOnly) {
8281
8300
  if (explicit !== void 0 && !isTrainVersion(explicit))
8282
8301
  throw new UsageError(`'${explicit}' is not plain semver (X.Y.Z, no v)`);
@@ -8663,9 +8682,18 @@ async function templateFacet(ctx, cwd) {
8663
8682
  // ../lib/dist/propagate.js
8664
8683
  import { join as join8, relative as relative3 } from "node:path";
8665
8684
  var isClean = (s) => s.conflicted.length === 0 && s.failed.length === 0 && s.skipped.length === 0;
8666
- var PREVIEW_BASE = "ship-base";
8685
+ var BASE_TAG = "ship-base";
8686
+ var trainBranch = (train) => `train/${train}`;
8667
8687
  async function propagate(ctx, workspace, train) {
8668
- const summary = { train, preview: ctx.dryRun, clean: [], conflicted: [], failed: [], skipped: [] };
8688
+ const summary = {
8689
+ train,
8690
+ preview: ctx.dryRun,
8691
+ clean: [],
8692
+ conflicted: [],
8693
+ failed: [],
8694
+ skipped: [],
8695
+ pulls: []
8696
+ };
8669
8697
  const upstream = join8(workspace, UPSTREAM);
8670
8698
  const tree = await overview(ctx, workspace);
8671
8699
  const upstreamDir = tree.children.find((c) => c.path === upstream);
@@ -8677,64 +8705,63 @@ async function propagate(ctx, workspace, train) {
8677
8705
  ctx.say(`\u26A0 ${c.rel}: ${c.skipped} \u2014 skipped (fix it, then re-run propagate)`);
8678
8706
  summary.skipped.push({ path: c.rel, reason: c.skipped });
8679
8707
  }
8680
- const active = consumers.filter((c) => !c.skipped);
8681
- const scratch = ctx.dryRun ? (await ctx.stage(run("mktemp", ["-d"])))?.trim() : void 0;
8682
- const clones = /* @__PURE__ */ new Map();
8683
- const cloneOf = async (consumer) => {
8684
- let clone = clones.get(consumer.path);
8685
- if (!clone) {
8686
- clone = join8(scratch ?? "", consumer.rel);
8687
- await ctx.stage(run("git", ["clone", "--quiet", consumer.path, clone]));
8688
- await ctx.stage(run("git", ["tag", PREVIEW_BASE], { cwd: clone }));
8689
- clones.set(consumer.path, clone);
8690
- }
8691
- return clone;
8692
- };
8693
- const previewCommit = (clone, message) => ctx.stage(run("git", ["add", "-A"], { cwd: clone })).then(() => ctx.stage(run("git", [
8694
- "-c",
8695
- "commit.gpgsign=false",
8696
- "-c",
8697
- "user.name=plx",
8698
- "-c",
8699
- "user.email=plx@localhost",
8700
- "commit",
8701
- "--quiet",
8702
- "--no-verify",
8703
- "--allow-empty",
8704
- "-m",
8705
- message
8706
- ], { cwd: clone })));
8708
+ for (const preset of presets)
8709
+ if (!consumers.some((c) => c.sites.some((s) => s.preset === preset)))
8710
+ ctx.say(`\u2022 no consumers of ${presetSource(preset)} found under ${workspace}`);
8711
+ const scratch = (await ctx.stage(run("mktemp", ["-d"])))?.trim() ?? "";
8707
8712
  try {
8708
- for (const preset of presets) {
8709
- const sites = active.flatMap((c) => c.sites.filter((s) => s.preset === preset).map((s) => [c, s]));
8710
- if (sites.length === 0)
8711
- ctx.say(`\u2022 no consumers of ${presetSource(preset)} found under ${workspace}`);
8712
- for (const [consumer, site] of sites)
8713
- await updateSite(ctx, consumer, site, join8(upstream, preset), summary, cloneOf, previewCommit);
8714
- }
8715
- await refreshLockfiles(ctx, active, summary, clones);
8716
- await bumpConsumerRefs(ctx, consumers, train);
8713
+ for (const consumer of consumers) {
8714
+ if (consumer.skipped || !consumer.origin)
8715
+ continue;
8716
+ const clone = join8(scratch, consumer.rel);
8717
+ ctx.say(`\u2192 ${consumer.rel}: clone ${consumer.origin} at ${consumer.branch}`);
8718
+ try {
8719
+ await ctx.stage(run("git", ["clone", "--quiet", "--branch", consumer.branch, consumer.origin, clone]));
8720
+ await ctx.stage(run("git", ["tag", BASE_TAG], { cwd: clone }));
8721
+ } catch (error) {
8722
+ ctx.say(` \u2717 FAILED \u2014 clone${detail(error)}`);
8723
+ summary.failed.push(consumer.rel);
8724
+ continue;
8725
+ }
8726
+ for (const site of consumer.sites)
8727
+ await updateSite(ctx, consumer, site, clone, upstream, summary);
8728
+ await refreshLockfile(ctx, consumer, clone, summary);
8729
+ await bumpConsumerRefs(ctx, consumer, clone, train);
8730
+ await openPull(ctx, consumer, clone, train, summary);
8731
+ }
8717
8732
  } finally {
8718
8733
  if (scratch)
8719
8734
  await ctx.stage(run("rm", ["-rf", scratch]));
8720
8735
  }
8721
8736
  return summary;
8722
8737
  }
8723
- async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, previewCommit) {
8724
- let repo = consumer.path;
8725
- let target = site.path;
8738
+ var previewCommit = (ctx, clone, message) => ctx.stage(run("git", ["add", "-A"], { cwd: clone })).then(() => ctx.stage(run("git", [
8739
+ "-c",
8740
+ "commit.gpgsign=false",
8741
+ "-c",
8742
+ "user.name=plx",
8743
+ "-c",
8744
+ "user.email=plx@localhost",
8745
+ "commit",
8746
+ "--quiet",
8747
+ "--no-verify",
8748
+ "--allow-empty",
8749
+ "-m",
8750
+ message
8751
+ ], { cwd: clone })));
8752
+ async function updateSite(ctx, consumer, site, clone, upstream, summary) {
8753
+ const target = join8(clone, relative3(consumer.path, site.path));
8754
+ const localPreset = join8(upstream, site.preset);
8726
8755
  try {
8727
8756
  if (ctx.dryRun) {
8728
8757
  ctx.say(`\u2192 ${site.rel}: copier update (preview from ${localPreset}@main)`);
8729
- repo = await cloneOf(consumer);
8730
- target = join8(repo, relative3(consumer.path, site.path));
8731
- const answers = await ctx.readFile(join8(site.path, ANSWERS_FILE));
8758
+ const answers = await ctx.readFile(join8(target, ANSWERS_FILE));
8732
8759
  await ctx.stage(write(join8(target, ANSWERS_FILE), answers.replace(/^_src_path: .*$/m, `_src_path: ${localPreset}`)));
8733
- await previewCommit(repo, `plx preview: ${site.rel} reads ${localPreset} from the local checkout`);
8760
+ await previewCommit(ctx, clone, `plx preview: ${site.rel} reads ${localPreset} from the local checkout`);
8734
8761
  await ctx.stage(run("mise", copierVia(["update", "--skip-answered", "--trust", "--defaults", "--skip-tasks", "--vcs-ref", "main"]), { cwd: target }));
8735
8762
  } else {
8736
8763
  ctx.say(`\u2192 ${site.rel}: copier update`);
8737
- const args = ["update", "--skip-answered", "--trust", ...ctx.yes ? ["--defaults"] : []];
8764
+ const args = ["update", "--skip-answered", "--trust", "--skip-tasks", ...ctx.yes ? ["--defaults"] : []];
8738
8765
  await ctx.apply([run("mise", copierVia(args), { cwd: target, interactive: true })]);
8739
8766
  }
8740
8767
  } catch (error) {
@@ -8742,17 +8769,17 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
8742
8769
  summary.failed.push(site.rel);
8743
8770
  return;
8744
8771
  }
8745
- const status = await ctx.read("git", ["status", "--porcelain"], { cwd: repo });
8772
+ const status = await ctx.read("git", ["status", "--porcelain", "--", "."], { cwd: target });
8746
8773
  let conflict = hasUnmergedEntries(status);
8747
- const changed = (await ctx.read("git", ["diff", "--name-only"], { cwd: repo })).split("\n").filter(Boolean);
8774
+ const changed = (await ctx.read("git", ["diff", "--name-only", "--relative", "--", "."], { cwd: target })).split("\n").filter(Boolean);
8748
8775
  for (const f of changed) {
8749
- const text = await ctx.readFile(join8(repo, f)).catch(() => "");
8776
+ const text = await ctx.readFile(join8(target, f)).catch(() => "");
8750
8777
  if (hasConflictMarkers(text))
8751
8778
  conflict = true;
8752
8779
  }
8753
8780
  await syncSiteMarker(ctx, site, target);
8754
8781
  if (conflict) {
8755
- ctx.say(" \u2717 CONFLICTS \u2014 resolve the <<<<<<< markers, then git add:");
8782
+ ctx.say(" \u2717 CONFLICTS \u2014 committed with their <<<<<<< markers, to resolve in the pull request:");
8756
8783
  summary.conflicted.push(site.rel);
8757
8784
  } else {
8758
8785
  ctx.say(" \u2713 merged clean:");
@@ -8761,7 +8788,7 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
8761
8788
  for (const line of status.split("\n").filter(Boolean))
8762
8789
  ctx.say(` ${line}`);
8763
8790
  if (ctx.dryRun)
8764
- await previewCommit(repo, `plx preview: ${site.rel} updated`);
8791
+ await previewCommit(ctx, clone, `plx preview: ${site.rel} updated`);
8765
8792
  }
8766
8793
  async function syncSiteMarker(ctx, site, target) {
8767
8794
  const marker = await ctx.readFile(join8(target, MARKER_FILE)).catch(() => void 0);
@@ -8774,55 +8801,129 @@ async function syncSiteMarker(ctx, site, target) {
8774
8801
  if (synced === marker)
8775
8802
  return;
8776
8803
  ctx.say(` marker: plexus \u2192 ${commit} in ${site.rel}/${MARKER_FILE}`);
8777
- const step = write(join8(target, MARKER_FILE), synced);
8778
- if (ctx.dryRun)
8779
- await ctx.stage(step);
8780
- else
8781
- await ctx.apply([step]);
8804
+ await ctx.stage(write(join8(target, MARKER_FILE), synced));
8782
8805
  }
8783
- async function refreshLockfiles(ctx, consumers, summary, clones) {
8806
+ async function refreshLockfile(ctx, consumer, clone, summary) {
8807
+ if (await ctx.readFile(join8(clone, "pnpm-workspace.yaml")).catch(() => void 0) === void 0)
8808
+ return;
8784
8809
  const manifests = ["*package.json", "pnpm-workspace.yaml"];
8785
- for (const c of consumers) {
8786
- if (await ctx.readFile(join8(c.path, "pnpm-workspace.yaml")).catch(() => void 0) === void 0)
8787
- continue;
8788
- const repo = ctx.dryRun ? clones.get(c.path) : c.path;
8789
- if (!repo)
8790
- continue;
8791
- const range = ctx.dryRun ? [PREVIEW_BASE] : [];
8792
- const unchanged = await ctx.read("git", ["diff", "--quiet", ...range, "--", ...manifests], { cwd: repo }).then(() => true).catch(() => false);
8793
- if (unchanged)
8794
- continue;
8795
- ctx.say(`\u2192 ${c.rel}: pnpm install (manifests changed)`);
8796
- try {
8797
- await ctx.apply([run("mise", ["exec", "--", "pnpm", "install", "--reporter=silent"], { cwd: c.path })]);
8798
- } catch (error) {
8799
- ctx.say(` \u2717 pnpm install failed${detail(error)}`);
8800
- summary.failed.push(`${c.rel}/pnpm-lock.yaml`);
8801
- }
8810
+ const unchanged = await ctx.read("git", ["diff", "--quiet", BASE_TAG, "--", ...manifests], { cwd: clone }).then(() => true).catch(() => false);
8811
+ if (unchanged)
8812
+ return;
8813
+ ctx.say(`\u2192 ${consumer.rel}: pnpm install (manifests changed)`);
8814
+ try {
8815
+ await ctx.apply([run("mise", ["exec", "--", "pnpm", "install", "--reporter=silent"], { cwd: clone })]);
8816
+ } catch (error) {
8817
+ ctx.say(` \u2717 pnpm install failed${detail(error)}`);
8818
+ summary.failed.push(`${consumer.rel}/pnpm-lock.yaml`);
8802
8819
  }
8803
8820
  }
8804
- async function bumpConsumerRefs(ctx, consumers, train) {
8805
- for (const c of consumers) {
8806
- const dir = join8(c.path, ".github", "workflows");
8807
- const entries = await ctx.readDir(dir).catch(() => []);
8808
- const files = entries.filter((e) => !e.directory && /\.ya?ml$/.test(e.name)).map((e) => e.name).sort();
8809
- if (files.length === 0)
8821
+ async function bumpConsumerRefs(ctx, consumer, clone, train) {
8822
+ const dir = join8(clone, ".github", "workflows");
8823
+ const entries = await ctx.readDir(dir).catch(() => []);
8824
+ const files = entries.filter((e) => !e.directory && /\.ya?ml$/.test(e.name)).map((e) => e.name).sort();
8825
+ for (const f of files) {
8826
+ const text = await ctx.readFile(join8(dir, f));
8827
+ if (!text.includes(WORKFLOW_REF))
8810
8828
  continue;
8811
- if (c.skipped) {
8812
- ctx.say(`\u26A0 ${c.rel}: workflow refs not bumped (skipped above)`);
8829
+ const bumped = bumpWorkflowRefs(text, train);
8830
+ if (bumped === text)
8813
8831
  continue;
8814
- }
8815
- for (const f of files) {
8816
- const text = await ctx.readFile(join8(dir, f));
8817
- if (!text.includes(WORKFLOW_REF))
8818
- continue;
8819
- const bumped = bumpWorkflowRefs(text, train);
8820
- if (bumped === text)
8821
- continue;
8822
- ctx.say(`\u2192 ${c.rel}: ${f} ci-cd refs \u2192 @${train}`);
8823
- await ctx.apply([write(join8(dir, f), bumped)]);
8824
- }
8832
+ ctx.say(`\u2192 ${consumer.rel}: ${f} workflow refs \u2192 @${train}`);
8833
+ await ctx.stage(write(join8(dir, f), bumped));
8834
+ }
8835
+ }
8836
+ async function openPull(ctx, consumer, clone, train, summary) {
8837
+ const branch = trainBranch(train);
8838
+ if (ctx.dryRun) {
8839
+ ctx.say(` (preview) ${consumer.rel}: would commit, push ${branch} and open a pull request against ${consumer.branch}`);
8840
+ return;
8841
+ }
8842
+ if ((await ctx.read("git", ["status", "--porcelain"], { cwd: clone })).trim() === "") {
8843
+ ctx.say(`\u2022 ${consumer.rel}: nothing changed \u2014 no pull request`);
8844
+ return;
8825
8845
  }
8846
+ const head = `${consumer.origin} ${branch}`;
8847
+ try {
8848
+ await ctx.apply([
8849
+ run("git", ["add", "-A"], { cwd: clone }),
8850
+ run("git", ["commit", "--quiet", "--no-verify", "-m", `chore: train ${train}`], { cwd: clone })
8851
+ ]);
8852
+ await ctx.read("git", ["fetch", "--quiet", "origin", `+refs/heads/${branch}:refs/remotes/origin/${branch}`], {
8853
+ cwd: clone
8854
+ }).catch(() => "");
8855
+ await ctx.apply([
8856
+ run("git", ["push", "--quiet", "--force-with-lease", "origin", `HEAD:refs/heads/${branch}`], { cwd: clone })
8857
+ ]);
8858
+ } catch (error) {
8859
+ ctx.say(` \u2717 FAILED \u2014 push to ${head}${detail(error)}`);
8860
+ summary.failed.push(consumer.rel);
8861
+ return;
8862
+ }
8863
+ const existing = (await ctx.read("gh", [
8864
+ "pr",
8865
+ "list",
8866
+ "--head",
8867
+ branch,
8868
+ "--base",
8869
+ consumer.branch,
8870
+ "--state",
8871
+ "open",
8872
+ "--json",
8873
+ "url",
8874
+ "--jq",
8875
+ ".[0].url"
8876
+ ], { cwd: clone }).catch(() => "")).trim();
8877
+ if (existing) {
8878
+ ctx.say(`\u2022 ${consumer.rel}: pull request updated: ${existing}`);
8879
+ summary.pulls.push(existing);
8880
+ return;
8881
+ }
8882
+ const sites = consumer.sites.map((s) => s.rel);
8883
+ const conflicted = sites.filter((s) => summary.conflicted.includes(s));
8884
+ const body2 = [
8885
+ `Train ${train} (\xA7 1.5 PLX): \`copier update\` at ${sites.join(", ")}, the marker, the lockfile where manifests moved, and the workflow refs.`,
8886
+ conflicted.length > 0 ? `Conflicts to resolve, committed with their markers: ${conflicted.join(", ")}.` : "Merged clean at every site.",
8887
+ "Opened by `plx ship`."
8888
+ ].join("\n\n");
8889
+ try {
8890
+ await ctx.apply([
8891
+ run("gh", [
8892
+ "pr",
8893
+ "create",
8894
+ "--base",
8895
+ consumer.branch,
8896
+ "--head",
8897
+ branch,
8898
+ "--title",
8899
+ `chore: train ${train}`,
8900
+ "--body",
8901
+ body2
8902
+ ], { cwd: clone })
8903
+ ]);
8904
+ } catch (error) {
8905
+ ctx.say(` \u2717 FAILED \u2014 pull request${detail(error)}`);
8906
+ summary.failed.push(consumer.rel);
8907
+ return;
8908
+ }
8909
+ const url = (await ctx.read("gh", [
8910
+ "pr",
8911
+ "list",
8912
+ "--head",
8913
+ branch,
8914
+ "--base",
8915
+ consumer.branch,
8916
+ "--state",
8917
+ "open",
8918
+ "--json",
8919
+ "url",
8920
+ "--jq",
8921
+ ".[0].url"
8922
+ ], {
8923
+ cwd: clone
8924
+ }).catch(() => "")).trim();
8925
+ ctx.say(`\u2022 ${consumer.rel}: pull request opened${url ? `: ${url}` : ""}`);
8926
+ summary.pulls.push(url || `${consumer.rel} ${branch}`);
8826
8927
  }
8827
8928
  var detail = (error) => {
8828
8929
  const message = error instanceof Error ? error.message : String(error);
@@ -8996,59 +9097,118 @@ async function preflightSelf(ctx, running) {
8996
9097
 
8997
9098
  // ../lib/dist/ship.js
8998
9099
  import { join as join10 } from "node:path";
9100
+ var DEFAULT_WAIT = { interval: 15e3, timeout: 20 * 6e4 };
8999
9101
  async function ship(ctx, options) {
9000
9102
  const upstream = join10(options.workspace, UPSTREAM);
9001
9103
  const propagateOnly = options.propagateOnly ?? false;
9002
9104
  if (options.plx)
9003
9105
  await preflightSelf(ctx, options.plx);
9004
9106
  const train = await readTrain(ctx, upstream);
9005
- if (!propagateOnly)
9107
+ await preflightForge(ctx);
9108
+ if (!propagateOnly) {
9006
9109
  await preflightRelease(ctx, train);
9110
+ await preflightImmutable(ctx, train);
9111
+ }
9007
9112
  const next = resolveVersion(train, options.train, propagateOnly);
9008
9113
  if (!propagateOnly)
9009
- await release(ctx, train, next, options.today ?? today());
9114
+ await release(ctx, train, next, options.today ?? today(), { ...DEFAULT_WAIT, ...options.wait });
9010
9115
  const summary = await propagate(ctx, options.workspace, next);
9011
9116
  return { ...summary, released: !propagateOnly };
9012
9117
  }
9013
- async function release(ctx, train, next, today2) {
9118
+ async function preflightImmutable(ctx, train) {
9119
+ for (const repo of train.repos) {
9120
+ const newest = (await ctx.read("gh", ["api", "repos/{owner}/{repo}/releases?per_page=1", "--jq", ".[0].immutable"], { cwd: repo.path }).catch(() => "")).trim();
9121
+ if (newest === "false")
9122
+ throw new Error(mutableRelease(`${repo.name}: its newest GitHub release is not immutable`));
9123
+ }
9124
+ }
9125
+ var mutableRelease = (what) => `${what} \u2014 enable immutable releases on the plexus-ms organisation (Settings \u2192 General \u2192 Releases); existing releases stay as they are (never delete one: a deleted immutable release burns its number)`;
9126
+ async function release(ctx, train, next, today2, wait) {
9014
9127
  ctx.say(`\u2192 train ${train.last ?? "none"} \u2192 ${next}`);
9015
9128
  for (const repo of train.repos) {
9016
9129
  if (repo.pending.length === 0)
9017
9130
  ctx.say(` ${repo.name}: no changes since ${repo.latest} \u2014 tag only`);
9018
9131
  else {
9019
9132
  ctx.say(` ${repo.name} (since ${repo.latest ?? "the beginning"}):`);
9020
- for (const line of repo.pending)
9133
+ for (const line of repo.pending.slice(0, PENDING_SHOWN))
9021
9134
  ctx.say(` ${line}`);
9135
+ if (repo.pending.length > PENDING_SHOWN)
9136
+ ctx.say(` \u2026 and ${repo.pending.length - PENDING_SHOWN} more`);
9022
9137
  }
9023
9138
  }
9024
- if (!await ctx.confirm(`tag all ${train.repos.length} repos as ${next} and push?`))
9139
+ if (!await ctx.confirm(`tag all ${train.repos.length} repos as ${next}, release them and push?`))
9025
9140
  throw new Error("aborted");
9141
+ const [one = [], two = []] = phasesOf(train);
9026
9142
  try {
9027
- const plan = await planBookkeeping(ctx, train, next, today2);
9028
- await ctx.apply(plan);
9029
- for (const repo of train.repos) {
9030
- const tagged = await ctx.read("git", ["rev-parse", "-q", "--verify", `refs/tags/${next}`], { cwd: repo.path }).then(() => true).catch(() => false);
9031
- if (tagged)
9032
- ctx.say(`\u2022 ${repo.name}: already tagged ${next}`);
9033
- else
9034
- await ctx.apply([run("git", ["tag", "-a", next, "-m", `train ${next}`], { cwd: repo.path })]);
9035
- await ctx.apply([run("git", ["push", "origin", "main", next], { cwd: repo.path })]);
9036
- }
9143
+ await releasePhase(ctx, train, one, next, today2);
9144
+ await awaitPublished(ctx, next, wait);
9145
+ await releasePhase(ctx, train, two, next, today2);
9037
9146
  } catch (error) {
9038
9147
  const message = error instanceof Error ? error.message : String(error);
9039
9148
  throw new Error(`${message}
9040
- \u2717 ship stopped during the release; re-run with --train ${next} to finish it (tagged repos are skipped)`);
9149
+ \u2717 ship stopped during the release; re-run with --train ${next} to finish it (tagged repos and existing releases are skipped; if the registry already has ${next}, update plx first: it is the same code, built from the commit phase one tagged)`);
9041
9150
  }
9042
9151
  }
9043
- async function planBookkeeping(ctx, train, next, today2) {
9152
+ var PENDING_SHOWN = 20;
9153
+ async function releasePhase(ctx, train, repos2, next, today2) {
9154
+ ctx.say(`\u2192 phase: ${repos2.map((r) => r.name).join(", ")}`);
9155
+ await ctx.apply(await planBookkeeping(ctx, train, repos2, next, today2));
9156
+ for (const repo of repos2)
9157
+ await tagAndRelease(ctx, repo, next);
9158
+ }
9159
+ async function tagAndRelease(ctx, repo, next) {
9160
+ const tagged = await ctx.read("git", ["rev-parse", "-q", "--verify", `refs/tags/${next}`], { cwd: repo.path }).then(() => true).catch(() => false);
9161
+ if (tagged)
9162
+ ctx.say(`\u2022 ${repo.name}: already tagged ${next}`);
9163
+ else
9164
+ await ctx.apply([run("git", ["tag", "-a", next, "-m", `train ${next}`], { cwd: repo.path })]);
9165
+ await ctx.apply([run("git", ["push", "origin", "main", next], { cwd: repo.path })]);
9166
+ const released = await ctx.read("gh", ["release", "view", next], { cwd: repo.path }).then(() => true).catch(() => false);
9167
+ if (released) {
9168
+ ctx.say(`\u2022 ${repo.name}: release ${next} exists`);
9169
+ return;
9170
+ }
9171
+ await ctx.apply([
9172
+ run("gh", ["release", "create", next, "--verify-tag", "--generate-notes", "--title", next], { cwd: repo.path })
9173
+ ]);
9174
+ if (ctx.dryRun)
9175
+ return;
9176
+ const immutable = (await ctx.read("gh", ["api", `repos/{owner}/{repo}/releases/tags/${next}`, "--jq", ".immutable"], {
9177
+ cwd: repo.path
9178
+ })).trim();
9179
+ if (immutable !== "true")
9180
+ throw new Error(mutableRelease(`${repo.name}: release ${next} was created mutable, and its tag can still move`));
9181
+ }
9182
+ async function awaitPublished(ctx, next, wait) {
9183
+ const names = TRAIN_PACKAGES.map((p) => p.name);
9184
+ const list2 = names.map((n) => `${n}@${next}`).join(", ");
9185
+ if (ctx.dryRun) {
9186
+ ctx.say(`\u23F3 [dry-run] would wait for ${list2} on npm (${PRIMITIVES}' release workflow publishes them)`);
9187
+ return;
9188
+ }
9189
+ ctx.say(`\u23F3 waiting for ${list2} on npm (${PRIMITIVES}' release workflow publishes them)`);
9190
+ const started = Date.now();
9191
+ let missing = [...names];
9192
+ for (; ; ) {
9193
+ const still = [];
9194
+ for (const name of missing) {
9195
+ const seen = (await ctx.read("npm", ["view", `${name}@${next}`, "version"]).catch(() => "")).trim();
9196
+ if (seen !== next)
9197
+ still.push(name);
9198
+ }
9199
+ missing = still;
9200
+ if (missing.length === 0)
9201
+ break;
9202
+ if (Date.now() - started >= wait.timeout)
9203
+ 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})`);
9204
+ await new Promise((resolve10) => setTimeout(resolve10, wait.interval));
9205
+ }
9206
+ ctx.say(` \u2713 ${list2} on npm`);
9207
+ }
9208
+ async function planBookkeeping(ctx, train, repos2, next, today2) {
9044
9209
  const plan = [];
9045
9210
  const touched = /* @__PURE__ */ new Set();
9046
- const repo = (name) => {
9047
- const r = train.repos.find((r2) => r2.name === name);
9048
- if (!r)
9049
- throw new Error(`${name} is not a train repo`);
9050
- return r;
9051
- };
9211
+ const repo = (name) => repos2.find((r) => r.name === name);
9052
9212
  const edit = async (r, rel, what, transform) => {
9053
9213
  const path3 = join10(r.path, rel);
9054
9214
  const text = await ctx.readFile(path3).catch(() => void 0);
@@ -9061,23 +9221,36 @@ async function planBookkeeping(ctx, train, next, today2) {
9061
9221
  plan.push(write(path3, edited));
9062
9222
  touched.add(r);
9063
9223
  };
9064
- await edit(repo("platform"), "ansible/galaxy.yml", `version \u2192 ${next}`, (t) => bumpGalaxyVersion(t, next));
9065
- await edit(repo("preset-repo-platform"), "template/requirements.yml", `plexus.platform pin \u2192 ${next}`, (t) => bumpCollectionPin(t, next));
9066
- for (const r of train.repos) {
9067
- const files = await ctx.read("git", ["grep", "-l", WORKFLOW_REF, "--", "*.yml", "*.yaml", "*.jinja"], { cwd: r.path }).catch(() => "");
9068
- for (const f of files.split("\n").filter(Boolean))
9069
- await edit(r, f, `ci-cd refs \u2192 @${next}`, (t) => bumpWorkflowRefs(t, next));
9224
+ const grep = async (r, needle, pathspecs) => (await ctx.read("git", ["grep", "-l", needle, "--", ...pathspecs], { cwd: r.path }).catch(() => "")).split("\n").filter(Boolean);
9225
+ const primitives = repo(PRIMITIVES);
9226
+ if (primitives) {
9227
+ await edit(primitives, "ansible/galaxy.yml", `version \u2192 ${next}`, (t) => bumpGalaxyVersion(t, next));
9228
+ for (const p of TRAIN_PACKAGES)
9229
+ await edit(primitives, `${p.dir}/package.json`, `${p.name} \u2192 ${next}`, (t) => bumpManifestVersion(t, next));
9230
+ }
9231
+ const platformPreset = repo("preset-repo-platform");
9232
+ if (platformPreset)
9233
+ await edit(platformPreset, "template/requirements.yml", `plexus.platform pin \u2192 ${next}`, (t) => bumpCollectionPin(t, next));
9234
+ for (const r of repos2) {
9235
+ for (const f of await grep(r, WORKFLOW_REF, ["*.yml", "*.yaml", "*.jinja"]))
9236
+ await edit(r, f, `workflow refs \u2192 @${next}`, (t) => bumpWorkflowRefs(t, next));
9237
+ for (const f of await grep(r, PLX_PIN, ["*.toml", "*.toml.jinja"]))
9238
+ await edit(r, f, `plx pin \u2192 ${next}`, (t) => bumpPlxPin(t, next));
9239
+ for (const f of await grep(r, `"${CONFIG_PIN}"`, ["*package.json", "*package.json.jinja"]))
9240
+ await edit(r, f, `${CONFIG_PIN} pin \u2192 ${next}`, (t) => bumpConfigPin(t, next));
9070
9241
  }
9071
9242
  const comp = repo("compendium");
9072
- await edit(comp, MAP_PAGE, `version \u2192 ${next}, timestamp \u2192 ${today2}`, (t) => stampStandard(t, next, today2));
9073
- for (const page of await modulePages(ctx, comp.path)) {
9074
- const text = await ctx.readFile(join10(comp.path, page));
9075
- if (frontmatterField(text, "maturity") !== "standard")
9076
- continue;
9077
- const previous = comp.tag ? await ctx.read("git", ["show", `${comp.tag}:${page}`], { cwd: comp.path }).catch(() => "") : "";
9078
- if (extractRequirements(text).join("\n") === extractRequirements(previous).join("\n"))
9079
- continue;
9080
- await edit(comp, page, `requirements changed since ${comp.latest ?? "the beginning"} \u2192 changed: ${next}`, (t) => stampChanged(t, next));
9243
+ if (comp) {
9244
+ await edit(comp, MAP_PAGE, `version \u2192 ${next}, timestamp \u2192 ${today2}`, (t) => stampStandard(t, next, today2));
9245
+ for (const page of await modulePages(ctx, comp.path)) {
9246
+ const text = await ctx.readFile(join10(comp.path, page));
9247
+ if (frontmatterField(text, "maturity") !== "standard")
9248
+ continue;
9249
+ const previous = comp.tag ? await ctx.read("git", ["show", `${comp.tag}:${page}`], { cwd: comp.path }).catch(() => "") : "";
9250
+ if (extractRequirements(text).join("\n") === extractRequirements(previous).join("\n"))
9251
+ continue;
9252
+ await edit(comp, page, `requirements changed since ${comp.latest ?? "the beginning"} \u2192 changed: ${next}`, (t) => stampChanged(t, next));
9253
+ }
9081
9254
  }
9082
9255
  for (const r of train.repos) {
9083
9256
  if (!touched.has(r))
@@ -9091,7 +9264,7 @@ async function planBookkeeping(ctx, train, next, today2) {
9091
9264
  // src/program.ts
9092
9265
  import { createRequire } from "node:module";
9093
9266
 
9094
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/key.js
9267
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/key.js
9095
9268
  var keybindings = ["emacs", "vim"];
9096
9269
  var keybindingLookup = new Set(keybindings);
9097
9270
  function isKeybinding(value) {
@@ -9106,7 +9279,7 @@ function getDefaultKeybindings() {
9106
9279
  var isTabKey = (key) => key.name === "tab";
9107
9280
  var isEnterKey = (key) => key.name === "enter" || key.name === "return";
9108
9281
 
9109
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/errors.js
9282
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/errors.js
9110
9283
  var AbortPromptError = class extends Error {
9111
9284
  name = "AbortPromptError";
9112
9285
  message = "Prompt was aborted";
@@ -9129,10 +9302,10 @@ var ValidationError = class extends Error {
9129
9302
  name = "ValidationError";
9130
9303
  };
9131
9304
 
9132
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/use-state.js
9305
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/use-state.js
9133
9306
  import { AsyncResource as AsyncResource2 } from "node:async_hooks";
9134
9307
 
9135
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/hook-engine.js
9308
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/hook-engine.js
9136
9309
  import { AsyncLocalStorage, AsyncResource } from "node:async_hooks";
9137
9310
  var hookStorage = new AsyncLocalStorage();
9138
9311
  function createStore(rl) {
@@ -9236,7 +9409,7 @@ var effectScheduler = {
9236
9409
  }
9237
9410
  };
9238
9411
 
9239
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/use-state.js
9412
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/use-state.js
9240
9413
  function isFactory(value) {
9241
9414
  return typeof value === "function";
9242
9415
  }
@@ -9262,7 +9435,7 @@ function useState(defaultValue) {
9262
9435
  });
9263
9436
  }
9264
9437
 
9265
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/use-effect.js
9438
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/use-effect.js
9266
9439
  function useEffect(cb, depArray) {
9267
9440
  withPointer((pointer) => {
9268
9441
  const oldDeps = pointer.get();
@@ -9274,10 +9447,10 @@ function useEffect(cb, depArray) {
9274
9447
  });
9275
9448
  }
9276
9449
 
9277
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/theme.js
9450
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/theme.js
9278
9451
  import { styleText } from "node:util";
9279
9452
 
9280
- // ../../node_modules/.pnpm/@inquirer+figures@2.0.9/node_modules/@inquirer/figures/dist/index.js
9453
+ // ../../node_modules/.pnpm/@inquirer+figures@2.0.10/node_modules/@inquirer/figures/dist/index.js
9281
9454
  import process2 from "node:process";
9282
9455
  function isUnicodeSupported() {
9283
9456
  if (!process2.platform.startsWith("win")) {
@@ -9570,7 +9743,7 @@ var figures = shouldUseMain ? mainSymbols : fallbackSymbols;
9570
9743
  var dist_default = figures;
9571
9744
  var replacements = Object.entries(specialMainSymbols);
9572
9745
 
9573
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/theme.js
9746
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/theme.js
9574
9747
  var defaultTheme = {
9575
9748
  prefix: {
9576
9749
  idle: styleText("blue", "?"),
@@ -9598,7 +9771,7 @@ function getDefaultTheme() {
9598
9771
  };
9599
9772
  }
9600
9773
 
9601
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/make-theme.js
9774
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/make-theme.js
9602
9775
  function isPlainObject(value) {
9603
9776
  if (typeof value !== "object" || value === null)
9604
9777
  return false;
@@ -9626,7 +9799,7 @@ function makeTheme(...themes) {
9626
9799
  return deepMerge(...themesToMerge);
9627
9800
  }
9628
9801
 
9629
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/use-prefix.js
9802
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/use-prefix.js
9630
9803
  function usePrefix({ status = "idle", theme }) {
9631
9804
  const [showLoader, setShowLoader] = useState(false);
9632
9805
  const [tick, setTick] = useState(0);
@@ -9657,12 +9830,12 @@ function usePrefix({ status = "idle", theme }) {
9657
9830
  return typeof prefix === "string" ? prefix : prefix[iconName] ?? prefix["idle"];
9658
9831
  }
9659
9832
 
9660
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/use-ref.js
9833
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/use-ref.js
9661
9834
  function useRef(val) {
9662
9835
  return useState({ current: val })[0];
9663
9836
  }
9664
9837
 
9665
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/use-keypress.js
9838
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/use-keypress.js
9666
9839
  function useKeypress(userHandler) {
9667
9840
  const signal = useRef(userHandler);
9668
9841
  signal.current = userHandler;
@@ -9681,7 +9854,7 @@ function useKeypress(userHandler) {
9681
9854
  }, []);
9682
9855
  }
9683
9856
 
9684
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/utils.js
9857
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/utils.js
9685
9858
  var import_cli_width = __toESM(require_cli_width(), 1);
9686
9859
 
9687
9860
  // ../../node_modules/.pnpm/fast-string-truncated-width@3.0.3/node_modules/fast-string-truncated-width/dist/utils.js
@@ -10020,7 +10193,7 @@ function wrapAnsi(string, columns, options) {
10020
10193
  return String(string).normalize().split(CRLF_OR_LF).map((line) => exec(line, columns, options)).join("\n");
10021
10194
  }
10022
10195
 
10023
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/utils.js
10196
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/utils.js
10024
10197
  function breakLines(content, width) {
10025
10198
  return content.split("\n").flatMap((line) => wrapAnsi(line, width, { trim: false, wordWrap: false }).split("\n").map((str) => str.trimEnd())).join("\n");
10026
10199
  }
@@ -10028,11 +10201,11 @@ function readlineWidth() {
10028
10201
  return (0, import_cli_width.default)({ defaultWidth: 80, output: readline().output });
10029
10202
  }
10030
10203
 
10031
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/create-prompt.js
10204
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/create-prompt.js
10032
10205
  import * as readline2 from "node:readline";
10033
10206
  import { AsyncResource as AsyncResource3 } from "node:async_hooks";
10034
10207
 
10035
- // ../../node_modules/.pnpm/@inquirer+type@4.2.0_@types+node@26.6.5/node_modules/@inquirer/type/dist/utils.js
10208
+ // ../../node_modules/.pnpm/@inquirer+type@4.2.1_@types+node@26.6.5/node_modules/@inquirer/type/dist/utils.js
10036
10209
  function withResolver() {
10037
10210
  let resolve10;
10038
10211
  let reject;
@@ -10043,7 +10216,7 @@ function withResolver() {
10043
10216
  return { promise, resolve: resolve10, reject };
10044
10217
  }
10045
10218
 
10046
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/create-prompt.js
10219
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/create-prompt.js
10047
10220
  var import_mute_stream = __toESM(require_lib(), 1);
10048
10221
 
10049
10222
  // ../../node_modules/.pnpm/signal-exit@4.1.0/node_modules/signal-exit/dist/mjs/signals.js
@@ -10297,10 +10470,10 @@ var {
10297
10470
  unload
10298
10471
  } = signalExitWrap(processOk(process3) ? new SignalExit(process3) : new SignalExitFallback());
10299
10472
 
10300
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/screen-manager.js
10473
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/screen-manager.js
10301
10474
  import { stripVTControlCharacters } from "node:util";
10302
10475
 
10303
- // ../../node_modules/.pnpm/@inquirer+ansi@2.0.9/node_modules/@inquirer/ansi/dist/index.js
10476
+ // ../../node_modules/.pnpm/@inquirer+ansi@2.0.10/node_modules/@inquirer/ansi/dist/index.js
10304
10477
  var ESC2 = "\x1B[";
10305
10478
  var cursorLeft = ESC2 + "G";
10306
10479
  var cursorHide = ESC2 + "?25l";
@@ -10316,7 +10489,7 @@ var cursorTo = (x, y) => {
10316
10489
  var eraseLine = ESC2 + "2K";
10317
10490
  var eraseLines = (lines) => lines > 0 ? (eraseLine + cursorUp(1)).repeat(lines - 1) + eraseLine + cursorLeft : "";
10318
10491
 
10319
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/screen-manager.js
10492
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/screen-manager.js
10320
10493
  var height = (content) => content.split("\n").length;
10321
10494
  var lastLine = (content) => content.split("\n").pop() ?? "";
10322
10495
  var ScreenManager = class {
@@ -10377,7 +10550,7 @@ var ScreenManager = class {
10377
10550
  }
10378
10551
  };
10379
10552
 
10380
- // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/create-prompt.js
10553
+ // ../../node_modules/.pnpm/@inquirer+core@12.0.6_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/create-prompt.js
10381
10554
  import path from "node:path";
10382
10555
  var nativeSetImmediate = globalThis.setImmediate;
10383
10556
  var nativeClearImmediate = globalThis.clearImmediate;
@@ -10485,7 +10658,7 @@ function createPrompt(view) {
10485
10658
  }
10486
10659
  });
10487
10660
  };
10488
- if ("readableFlowing" in input) {
10661
+ if ("readableFlowing" in input && input.isTTY) {
10489
10662
  const immediate = nativeSetImmediate(startCycle);
10490
10663
  cleanups.add(() => nativeClearImmediate(immediate));
10491
10664
  } else {
@@ -10497,7 +10670,7 @@ function createPrompt(view) {
10497
10670
  return prompt;
10498
10671
  }
10499
10672
 
10500
- // ../../node_modules/.pnpm/@inquirer+confirm@6.3.4_@types+node@26.6.5/node_modules/@inquirer/confirm/dist/index.js
10673
+ // ../../node_modules/.pnpm/@inquirer+confirm@6.3.5_@types+node@26.6.5/node_modules/@inquirer/confirm/dist/index.js
10501
10674
  import { styleText as styleText2 } from "node:util";
10502
10675
  var confirmTheme = {
10503
10676
  keywords: {
@@ -14119,7 +14292,9 @@ import { resolve as resolve8 } from "node:path";
14119
14292
  // src/commands/ship.ts
14120
14293
  import { resolve as resolve7 } from "node:path";
14121
14294
  function register7(program2) {
14122
- program2.command("ship").description("release the train: tag every train repo identically, then copier-update every consumer").argument("[dir]", "the workspace: plexus-ms/ and the consumers sit under it", ".").option("--train <version>", "the train version, plain semver (default: minor+1 over the latest train)").action(async (dir, opts, command) => {
14295
+ program2.command("ship").description(
14296
+ "release the train: tag and release every train repo identically, then open a pull request per consumer"
14297
+ ).argument("[dir]", "the workspace: plexus-ms/ and the consumers sit under it", ".").option("--train <version>", "the train version, plain semver (default: minor+1 over the latest train)").action(async (dir, opts, command) => {
14123
14298
  const ctx = contextOf2(command);
14124
14299
  const result = await ship(ctx, { workspace: resolve7(dir), train: opts.train, plx: version });
14125
14300
  ctx.result(result, (r) => renderSummary(r, `plx -y ship --train ${r.train} ${dir}`));
@@ -14132,12 +14307,13 @@ function renderSummary(s, realCommand) {
14132
14307
  for (const p of s.conflicted) lines.push(` \u2717 conflicts: ${p}`);
14133
14308
  for (const p of s.failed) lines.push(` \u2717 failed: ${p}`);
14134
14309
  for (const { path: path3, reason } of s.skipped) lines.push(` \u26A0 skipped: ${path3} (${reason})`);
14310
+ for (const url of s.pulls) lines.push(` \u2192 pull: ${url}`);
14135
14311
  const clean = isClean(s);
14136
14312
  if (s.preview) {
14137
14313
  lines.push(" (preview only \u2014 nothing was changed)");
14138
14314
  if (clean) lines.push(`\u2713 dry run clean \u2014 run it for real: ${realCommand}`);
14139
14315
  } else if (clean) {
14140
- lines.push("\u2713 done \u2014 review consumer diffs, commit (web repos on develop, platform repos on main), push");
14316
+ lines.push("\u2713 done \u2014 review and merge the pull requests");
14141
14317
  }
14142
14318
  if (!clean) lines.push("\u2717 needs attention \u2014 see above; fix the consumer, then re-run propagate");
14143
14319
  return lines.join("\n");
@@ -14145,7 +14321,7 @@ function renderSummary(s, realCommand) {
14145
14321
 
14146
14322
  // src/commands/propagate.ts
14147
14323
  function register8(program2) {
14148
- program2.command("propagate").description("copier-update every consumer of the presets to the current train, and pin their ci-cd workflow refs").argument("[dir]", "the workspace: plexus-ms/ and the consumers sit under it", ".").option("--train <version>", "the train to propagate (default: the current one)").action(async (dir, opts, command) => {
14324
+ program2.command("propagate").description("copier-update every consumer of the presets to the current train, as one pull request per consumer").argument("[dir]", "the workspace: plexus-ms/ and the consumers sit under it", ".").option("--train <version>", "the train to propagate (default: the current one)").action(async (dir, opts, command) => {
14149
14325
  const ctx = contextOf2(command);
14150
14326
  const result = await ship(ctx, { workspace: resolve8(dir), train: opts.train, propagateOnly: true, plx: version });
14151
14327
  ctx.result(result, (r) => renderSummary(r, `plx -y propagate --train ${r.train} ${dir}`));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexus-ms/cli",
3
- "version": "0.9.0",
3
+ "version": "0.100.0-rc.3",
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",
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "git+https://github.com/plexus-ms/packages.git",
15
+ "url": "git+https://github.com/plexus-ms/primitives.git",
16
16
  "directory": "packages/cli"
17
17
  },
18
18
  "publishConfig": {
@@ -24,7 +24,7 @@
24
24
  },
25
25
  "dependencies": {
26
26
  "@inquirer/prompts": "^8.7.4",
27
- "@plexus-ms/lib": "0.9.0",
27
+ "@plexus-ms/lib": "0.100.0-rc.3",
28
28
  "commander": "^15.0.0"
29
29
  },
30
30
  "devDependencies": {