@plexus-ms/cli 0.3.0 → 0.5.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 +495 -189
  2. package/package.json +8 -6
package/dist/plx.js CHANGED
@@ -7268,7 +7268,7 @@ var require_public_api = __commonJS({
7268
7268
  }
7269
7269
  return doc;
7270
7270
  }
7271
- function parse3(src, reviver, options) {
7271
+ function parse5(src, reviver, options) {
7272
7272
  let _reviver = void 0;
7273
7273
  if (typeof reviver === "function") {
7274
7274
  _reviver = reviver;
@@ -7309,7 +7309,7 @@ var require_public_api = __commonJS({
7309
7309
  return value.toString(options);
7310
7310
  return new Document.Document(value, _replacer, options).toString(options);
7311
7311
  }
7312
- exports.parse = parse3;
7312
+ exports.parse = parse5;
7313
7313
  exports.parseAllDocuments = parseAllDocuments;
7314
7314
  exports.parseDocument = parseDocument;
7315
7315
  exports.stringify = stringify;
@@ -7655,63 +7655,84 @@ async function readCatalog(ctx, compendium) {
7655
7655
  return catalog;
7656
7656
  }
7657
7657
 
7658
- // ../lib/dist/consumers.js
7659
- import { relative } from "node:path";
7660
- function expectedBranch(declaration) {
7661
- if (!declaration)
7662
- return void 0;
7663
- if (declaration.modules.includes("PLX-Platform"))
7664
- return "main";
7665
- if (declaration.modules.includes("PLX-Repo"))
7666
- return "develop";
7667
- return void 0;
7658
+ // ../lib/dist/changedApps.js
7659
+ import { join as join2 } from "node:path";
7660
+ var BOOKKEEPING = /(^|\/)(PLEXUS\.md|\.copier-answers\.yml)$/;
7661
+ var GLOBALS = /^(pnpm-lock\.yaml|pnpm-workspace\.yaml|package\.json|turbo\.json|mise\.toml|\.npmrc|\.node-version)$/;
7662
+ var NO_REF = /^0*$/;
7663
+ async function selectChangedApps(input) {
7664
+ const all = [...input.all].sort();
7665
+ if (input.changed === void 0)
7666
+ return { apps: all, reason: "no base ref: every app counts as changed" };
7667
+ const changed = input.changed.filter((f) => !BOOKKEEPING.test(f));
7668
+ const under = (app) => changed.some((f) => f.startsWith(`apps/${app}/`));
7669
+ if (!input.workspace)
7670
+ return { apps: all.filter(under), reason: "files under the apps changed" };
7671
+ if (changed.length === 0)
7672
+ return { apps: [], reason: "nothing but bookkeeping changed" };
7673
+ if (changed.some((f) => GLOBALS.test(f)))
7674
+ return { apps: all, reason: "a workspace-global input changed: every app counts as changed" };
7675
+ const outside = changed.some((f) => !f.startsWith("apps/"));
7676
+ const listed = new Set(await input.dependents?.() ?? []);
7677
+ return {
7678
+ apps: all.filter((app) => listed.has(app) && (outside || under(app))),
7679
+ reason: "sources or their dependencies changed"
7680
+ };
7668
7681
  }
7669
- var presetSource = (preset) => `gh:plexus-ms/${preset}`;
7670
- async function findConsumers(ctx, root, presets) {
7671
- const sources = new Map(presets.map((p) => [presetSource(p), p]));
7672
- const consumers = /* @__PURE__ */ new Map();
7673
- const orphans = [];
7674
- const visit = (dir, repo, inTemplate) => {
7675
- const here = dir.git ? dir : repo;
7676
- const template = inTemplate || Boolean(dir.template);
7677
- const preset = dir.copier?.src ? sources.get(dir.copier.src) : void 0;
7678
- if (preset && !template) {
7679
- const site = { path: dir.path, rel: relative(root.path, dir.path) || ".", preset };
7680
- if (!here)
7681
- orphans.push(site);
7682
- else {
7683
- let consumer = consumers.get(here.path);
7684
- if (!consumer) {
7685
- consumer = {
7686
- path: here.path,
7687
- rel: relative(root.path, here.path) || ".",
7688
- branch: here.git?.branch ?? "?",
7689
- expected: expectedBranch(here.declaration),
7690
- sites: []
7691
- };
7692
- consumers.set(here.path, consumer);
7693
- }
7694
- consumer.sites.push(site);
7695
- }
7696
- }
7697
- for (const child of dir.children)
7698
- visit(child, here, template);
7682
+ async function readChangedApps(ctx, dir, from, to = "HEAD") {
7683
+ const git2 = (args) => ctx.read("git", args, { cwd: dir });
7684
+ const workspace = await exists(ctx, join2(dir, "pnpm-workspace.yaml"));
7685
+ const all = await deployableApps(ctx, dir, workspace);
7686
+ let changed;
7687
+ if (from !== "" && !NO_REF.test(from) && await isCommit(git2, from))
7688
+ changed = (await git2(["diff", "--name-only", from, to])).split("\n").filter(Boolean);
7689
+ const dependents = async () => {
7690
+ const out = await ctx.read("pnpm", ["--filter", `...[${from}...${to}]`, "ls", "--depth", "-1", "--parseable"], {
7691
+ cwd: dir
7692
+ });
7693
+ return out.split("\n").map((line) => /(?:^|\/)apps\/([^/]+)$/.exec(line.trim())?.[1]).filter((app) => app !== void 0);
7699
7694
  };
7700
- visit(root, void 0, false);
7701
- for (const site of orphans)
7702
- ctx.say(`\u26A0 ${site.rel}: not inside a git repo \u2014 skipped`);
7703
- for (const c of consumers.values()) {
7704
- if (c.expected && c.branch !== c.expected) {
7705
- c.skipped = `on '${c.branch}', expected '${c.expected}'`;
7695
+ const result = await selectChangedApps({ workspace, all, changed, dependents });
7696
+ ctx.say(`\u2192 ${result.reason}`, 1);
7697
+ return result;
7698
+ }
7699
+ async function isCommit(git2, ref) {
7700
+ try {
7701
+ await git2(["rev-parse", "--verify", "--quiet", `${ref}^{commit}`]);
7702
+ return true;
7703
+ } catch {
7704
+ return false;
7705
+ }
7706
+ }
7707
+ async function deployableApps(ctx, dir, workspace) {
7708
+ const entries = await ctx.readDir(join2(dir, "apps")).catch(() => []);
7709
+ const apps = [];
7710
+ for (const e of entries) {
7711
+ if (!e.directory)
7706
7712
  continue;
7713
+ const marks = workspace ? ["package.json"] : ["docker/compose.yaml", "compose.yaml"];
7714
+ for (const mark of marks) {
7715
+ if (await exists(ctx, join2(dir, "apps", e.name, mark))) {
7716
+ apps.push(e.name);
7717
+ break;
7718
+ }
7707
7719
  }
7708
- const tracked = (await ctx.read("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: c.path })).trim();
7709
- if (tracked !== "")
7710
- c.skipped = "has uncommitted changes";
7711
7720
  }
7712
- return [...consumers.values()];
7721
+ return apps.sort();
7722
+ }
7723
+ async function exists(ctx, path3) {
7724
+ try {
7725
+ await ctx.readFile(path3);
7726
+ return true;
7727
+ } catch {
7728
+ return false;
7729
+ }
7713
7730
  }
7714
7731
 
7732
+ // ../lib/dist/changeset.js
7733
+ var import_yaml2 = __toESM(require_dist(), 1);
7734
+ import { join as join3 } from "node:path";
7735
+
7715
7736
  // ../lib/dist/io.js
7716
7737
  import { spawn } from "node:child_process";
7717
7738
  import { readdir, readFile, writeFile } from "node:fs/promises";
@@ -7733,13 +7754,13 @@ function realIo(prompt) {
7733
7754
  stdout: process.stdout,
7734
7755
  stderr: process.stderr,
7735
7756
  prompt: prompt && isInteractive() ? prompt : void 0,
7736
- exec: (command, args, options = {}) => new Promise((resolve6, reject) => {
7757
+ exec: (command, args, options = {}) => new Promise((resolve9, reject) => {
7737
7758
  const child = spawn(command, args, {
7738
7759
  cwd: options.cwd,
7739
7760
  env: options.env,
7740
7761
  stdio: [
7741
7762
  options.input !== void 0 ? "pipe" : options.inherit ? "inherit" : "ignore",
7742
- options.inherit ? "inherit" : "pipe",
7763
+ options.inherit ? process.stderr.fd : "pipe",
7743
7764
  "pipe"
7744
7765
  ]
7745
7766
  });
@@ -7756,7 +7777,7 @@ function realIo(prompt) {
7756
7777
  child.on("error", reject);
7757
7778
  child.on("close", (code) => {
7758
7779
  if (code === 0)
7759
- resolve6(stdout);
7780
+ resolve9(stdout);
7760
7781
  else
7761
7782
  reject(new ExecError(render(command, args), code, stderr));
7762
7783
  });
@@ -7805,6 +7826,194 @@ EOF`;
7805
7826
  }
7806
7827
  }
7807
7828
 
7829
+ // ../lib/dist/changeset.js
7830
+ var CHANGESET_DIR = ".changeset";
7831
+ var FIELDS = ["dependencies", "optionalDependencies", "peerDependencies"];
7832
+ function changesetPath(branch) {
7833
+ const slug = branch.replace(/^(renovate|dependabot)\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase();
7834
+ return `${CHANGESET_DIR}/bot-${slug || "branch"}.md`;
7835
+ }
7836
+ function planChangeset(input) {
7837
+ const path3 = changesetPath(input.branch);
7838
+ const changes = [];
7839
+ const releases = {};
7840
+ for (const diff of input.packages) {
7841
+ const { before, after } = diff;
7842
+ if (!before || !after?.name || after.private || input.ignore.includes(after.name))
7843
+ continue;
7844
+ for (const field of FIELDS) {
7845
+ const names = /* @__PURE__ */ new Set([...Object.keys(before[field] ?? {}), ...Object.keys(after[field] ?? {})]);
7846
+ for (const dependency of [...names].sort()) {
7847
+ const from = resolveRange(before[field]?.[dependency], dependency, input.catalogsBefore);
7848
+ const to = resolveRange(after[field]?.[dependency], dependency, input.catalogsAfter);
7849
+ if (from === to)
7850
+ continue;
7851
+ changes.push({ package: after.name, dependency, field, from, to });
7852
+ const bump = field === "peerDependencies" ? "major" : "patch";
7853
+ if (releases[after.name] !== "major")
7854
+ releases[after.name] = bump;
7855
+ }
7856
+ }
7857
+ }
7858
+ const root = input.root;
7859
+ if (changes.length === 0) {
7860
+ const remove = run("rm", ["-f", path3], root ? { cwd: root } : {});
7861
+ return { path: path3, releases, changes, plan: input.exists ? [remove] : [] };
7862
+ }
7863
+ const header = Object.entries(releases).sort(([a], [b]) => a.localeCompare(b)).map(([name, bump]) => `'${name}': ${bump}`);
7864
+ const lines = [...new Set(changes.map(describeChange))].sort((a, b) => a.localeCompare(b));
7865
+ const content = `---
7866
+ ${header.join("\n")}
7867
+ ---
7868
+
7869
+ ${lines.join("\n")}
7870
+ `;
7871
+ return { path: path3, releases, changes, content, plan: [write(root ? join3(root, path3) : path3, content)] };
7872
+ }
7873
+ function describeChange(c) {
7874
+ const peer = c.field === "peerDependencies" ? " (peer)" : "";
7875
+ if (c.from === void 0)
7876
+ return `Add \`${c.dependency}\`${peer} at ${c.to}`;
7877
+ if (c.to === void 0)
7878
+ return `Remove \`${c.dependency}\`${peer}`;
7879
+ return `Update \`${c.dependency}\`${peer} from ${c.from} to ${c.to}`;
7880
+ }
7881
+ function resolveRange(value, dependency, catalogs) {
7882
+ if (value === void 0)
7883
+ return void 0;
7884
+ if (value === "catalog:" || value === "catalog:default")
7885
+ return catalogs.default[dependency] ?? value;
7886
+ if (value.startsWith("catalog:"))
7887
+ return catalogs.named[value.slice("catalog:".length)]?.[dependency] ?? value;
7888
+ return value;
7889
+ }
7890
+ function parseCatalogs(text) {
7891
+ const data = text === void 0 ? void 0 : (0, import_yaml2.parse)(text);
7892
+ const ws = data !== null && typeof data === "object" ? data : {};
7893
+ const map = (v) => v !== null && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, r]) => [k, String(r)])) : {};
7894
+ const named = ws.catalogs !== null && typeof ws.catalogs === "object" ? ws.catalogs : {};
7895
+ return {
7896
+ default: map(ws.catalog),
7897
+ named: Object.fromEntries(Object.entries(named).map(([name, entries]) => [name, map(entries)]))
7898
+ };
7899
+ }
7900
+ function workspaceGlobs(text) {
7901
+ const data = text === void 0 ? void 0 : (0, import_yaml2.parse)(text);
7902
+ const globs = data !== null && typeof data === "object" ? data.packages : void 0;
7903
+ return Array.isArray(globs) ? globs.map(String).filter((g) => !g.startsWith("!")) : [];
7904
+ }
7905
+ async function readChangeset(ctx, dir, base, branch) {
7906
+ const git2 = (args) => ctx.read("git", args, { cwd: dir });
7907
+ const head = branch ?? (await git2(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
7908
+ const mergeBase = (await git2(["merge-base", base, "HEAD"])).trim();
7909
+ const show = async (path4) => {
7910
+ try {
7911
+ return await git2(["show", `${mergeBase}:${path4}`]);
7912
+ } catch (error) {
7913
+ if (error instanceof ExecError)
7914
+ return void 0;
7915
+ throw error;
7916
+ }
7917
+ };
7918
+ const current = async (path4) => {
7919
+ try {
7920
+ return await ctx.readFile(join3(dir, path4));
7921
+ } catch {
7922
+ return void 0;
7923
+ }
7924
+ };
7925
+ const workspaceAfter = await current("pnpm-workspace.yaml");
7926
+ const packages = [];
7927
+ for (const glob of workspaceGlobs(workspaceAfter)) {
7928
+ for (const pkgDir of await expand(ctx, dir, glob)) {
7929
+ const path4 = `${pkgDir}/package.json`;
7930
+ const [before, after] = await Promise.all([show(path4), current(path4)]);
7931
+ packages.push({ path: path4, before: manifest(before), after: manifest(after) });
7932
+ }
7933
+ }
7934
+ const config = await current(`${CHANGESET_DIR}/config.json`);
7935
+ const ignore = config ? JSON.parse(config).ignore ?? [] : [];
7936
+ const entries = await ctx.readDir(join3(dir, CHANGESET_DIR)).catch(() => []);
7937
+ const path3 = changesetPath(head);
7938
+ return planChangeset({
7939
+ branch: head,
7940
+ packages,
7941
+ catalogsBefore: parseCatalogs(await show("pnpm-workspace.yaml")),
7942
+ catalogsAfter: parseCatalogs(workspaceAfter),
7943
+ ignore,
7944
+ exists: entries.some((e) => !e.directory && `${CHANGESET_DIR}/${e.name}` === path3),
7945
+ root: dir
7946
+ });
7947
+ }
7948
+ function manifest(text) {
7949
+ return text === void 0 ? void 0 : JSON.parse(text);
7950
+ }
7951
+ async function expand(ctx, root, glob) {
7952
+ const star = glob.indexOf("*");
7953
+ if (star < 0)
7954
+ return [glob.replace(/\/$/, "")];
7955
+ const parent = glob.slice(0, star).replace(/\/$/, "");
7956
+ const entries = await ctx.readDir(join3(root, parent)).catch(() => []);
7957
+ return entries.filter((e) => e.directory && !e.name.startsWith(".")).map((e) => parent ? `${parent}/${e.name}` : e.name).sort();
7958
+ }
7959
+
7960
+ // ../lib/dist/consumers.js
7961
+ import { relative } from "node:path";
7962
+ function expectedBranch(declaration) {
7963
+ if (!declaration)
7964
+ return void 0;
7965
+ if (declaration.modules.includes("PLX-Platform"))
7966
+ return "main";
7967
+ if (declaration.modules.includes("PLX-Repo"))
7968
+ return "develop";
7969
+ return void 0;
7970
+ }
7971
+ var presetSource = (preset) => `gh:plexus-ms/${preset}`;
7972
+ async function findConsumers(ctx, root, presets) {
7973
+ const sources = new Map(presets.map((p) => [presetSource(p), p]));
7974
+ const consumers = /* @__PURE__ */ new Map();
7975
+ const orphans = [];
7976
+ const visit = (dir, repo, inTemplate) => {
7977
+ const here = dir.git ? dir : repo;
7978
+ const template = inTemplate || Boolean(dir.template);
7979
+ const preset = dir.copier?.src ? sources.get(dir.copier.src) : void 0;
7980
+ if (preset && !template) {
7981
+ const site = { path: dir.path, rel: relative(root.path, dir.path) || ".", preset };
7982
+ if (!here)
7983
+ orphans.push(site);
7984
+ else {
7985
+ let consumer = consumers.get(here.path);
7986
+ if (!consumer) {
7987
+ consumer = {
7988
+ path: here.path,
7989
+ rel: relative(root.path, here.path) || ".",
7990
+ branch: here.git?.branch ?? "?",
7991
+ expected: expectedBranch(here.declaration),
7992
+ sites: []
7993
+ };
7994
+ consumers.set(here.path, consumer);
7995
+ }
7996
+ consumer.sites.push(site);
7997
+ }
7998
+ }
7999
+ for (const child of dir.children)
8000
+ visit(child, here, template);
8001
+ };
8002
+ visit(root, void 0, false);
8003
+ for (const site of orphans)
8004
+ ctx.say(`\u26A0 ${site.rel}: not inside a git repo \u2014 skipped`);
8005
+ for (const c of consumers.values()) {
8006
+ if (c.expected && c.branch !== c.expected) {
8007
+ c.skipped = `on '${c.branch}', expected '${c.expected}'`;
8008
+ continue;
8009
+ }
8010
+ const tracked = (await ctx.read("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: c.path })).trim();
8011
+ if (tracked !== "")
8012
+ c.skipped = "has uncommitted changes";
8013
+ }
8014
+ return [...consumers.values()];
8015
+ }
8016
+
7808
8017
  // ../lib/dist/context.js
7809
8018
  var Context = class {
7810
8019
  dryRun;
@@ -7840,6 +8049,8 @@ var Context = class {
7840
8049
  */
7841
8050
  result(data, human = renderHuman) {
7842
8051
  const text = this.json ? JSON.stringify(data, null, 2) : human(data);
8052
+ if (text === "")
8053
+ return;
7843
8054
  this.io.stdout.write(text.endsWith("\n") ? text : `${text}
7844
8055
  `);
7845
8056
  }
@@ -7919,11 +8130,11 @@ function indent(text) {
7919
8130
  }
7920
8131
 
7921
8132
  // ../lib/dist/copier.js
7922
- var import_yaml2 = __toESM(require_dist(), 1);
8133
+ var import_yaml3 = __toESM(require_dist(), 1);
7923
8134
  var ANSWERS_FILE = ".copier-answers.yml";
7924
8135
  var TEMPLATE_FILES = ["copier.yml", "copier.yaml"];
7925
8136
  function parseAnswers(text) {
7926
- const data = (0, import_yaml2.parse)(text);
8137
+ const data = (0, import_yaml3.parse)(text);
7927
8138
  if (data === null || typeof data !== "object" || Array.isArray(data))
7928
8139
  throw new Error("answers file is not a mapping");
7929
8140
  const { _commit, _src_path, ...answers } = data;
@@ -7934,6 +8145,13 @@ function parseAnswers(text) {
7934
8145
  };
7935
8146
  }
7936
8147
 
8148
+ // ../lib/dist/deployTargets.js
8149
+ var import_yaml4 = __toESM(require_dist(), 1);
8150
+ import { join as join5 } from "node:path";
8151
+
8152
+ // ../lib/dist/train.js
8153
+ import { join as join4 } from "node:path";
8154
+
7937
8155
  // ../lib/dist/doctor.js
7938
8156
  import { relative as relative2 } from "node:path";
7939
8157
  function diagnose(root, catalog) {
@@ -7993,6 +8211,130 @@ function compareTrains(a, b) {
7993
8211
  return 0;
7994
8212
  }
7995
8213
 
8214
+ // ../lib/dist/train.js
8215
+ var TRAIN_REPOS = [
8216
+ "platform",
8217
+ "ci-cd",
8218
+ "preset-repo-web",
8219
+ "preset-app-nextjs",
8220
+ "preset-repo-platform",
8221
+ "compendium"
8222
+ ];
8223
+ var UPSTREAM = "plexus-ms";
8224
+ var isTrainVersion = (v) => /^\d+\.\d+\.\d+$/.test(v);
8225
+ function nextMinor(version2) {
8226
+ const [major = "0", minor = "0"] = version2.split(".");
8227
+ return `${major}.${Number(minor) + 1}.0`;
8228
+ }
8229
+ function latestTag(tags) {
8230
+ return tags.filter((t) => /^v?\d/.test(t)).sort((a, b) => compareTrains(a.replace(/^v/, ""), b.replace(/^v/, ""))).at(-1);
8231
+ }
8232
+ async function readTrain(ctx, upstream) {
8233
+ const repos = [];
8234
+ const all = /* @__PURE__ */ new Set();
8235
+ for (const name of TRAIN_REPOS) {
8236
+ const path3 = join4(upstream, name);
8237
+ try {
8238
+ await ctx.read("git", ["rev-parse", "--git-dir"], { cwd: path3 });
8239
+ } catch {
8240
+ throw new Error(`${path3} is not a git repo`);
8241
+ }
8242
+ try {
8243
+ await ctx.read("git", ["fetch", "origin", "--tags", "--quiet"], { cwd: path3 });
8244
+ } catch {
8245
+ throw new Error(`${name}: fetch from origin failed`);
8246
+ }
8247
+ const tags = (await ctx.read("git", ["tag", "--list", "v*", "[0-9]*"], { cwd: path3 })).split("\n").filter(Boolean);
8248
+ for (const t of tags)
8249
+ all.add(t.replace(/^v/, ""));
8250
+ const tag = latestTag(tags);
8251
+ const range = tag ? `${tag}..HEAD` : "HEAD";
8252
+ const pending = (await ctx.read("git", ["log", "--oneline", range], { cwd: path3 })).split("\n").filter(Boolean);
8253
+ repos.push({ name, path: path3, tag, latest: tag?.replace(/^v/, ""), pending });
8254
+ }
8255
+ const latest = repos.map((r) => r.latest);
8256
+ return { repos, last: latestTag([...all]), skew: latest.some((l) => l !== latest[0]) };
8257
+ }
8258
+ async function preflightRelease(ctx, train) {
8259
+ for (const { name, path: path3 } of train.repos) {
8260
+ const branch = (await ctx.read("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path3 })).trim();
8261
+ if (branch !== "main")
8262
+ throw new Error(`${name} is on '${branch}', trains cut from main`);
8263
+ if ((await ctx.read("git", ["status", "--porcelain"], { cwd: path3 })).trim() !== "")
8264
+ throw new Error(`${name} has uncommitted changes \u2014 commit first`);
8265
+ const behind = (await ctx.read("git", ["rev-list", "--count", "main..origin/main"], { cwd: path3 })).trim();
8266
+ if (behind !== "0")
8267
+ throw new Error(`${name} is ${behind} commit(s) behind origin/main \u2014 pull first, or the tag lands on a stale commit`);
8268
+ }
8269
+ }
8270
+ function resolveVersion(train, explicit, propagateOnly) {
8271
+ if (explicit !== void 0 && !isTrainVersion(explicit))
8272
+ throw new UsageError(`'${explicit}' is not plain semver (X.Y.Z, no v)`);
8273
+ if (propagateOnly) {
8274
+ const v = explicit ?? train.last;
8275
+ if (!v)
8276
+ throw new Error("no train version found to propagate");
8277
+ return v;
8278
+ }
8279
+ if (explicit)
8280
+ return explicit;
8281
+ if (train.skew) {
8282
+ const report = train.repos.map((r) => ` ${r.name}: ${r.latest ?? "none"}`).join("\n");
8283
+ throw new Error(`the train repos' latest tags disagree:
8284
+ ${report}
8285
+ pass the version explicitly (--train ${train.last}) to finish the half-tagged train, or a higher one to start a new one`);
8286
+ }
8287
+ return train.last ? nextMinor(train.last) : "0.1.0";
8288
+ }
8289
+ var UsageError = class extends Error {
8290
+ };
8291
+
8292
+ // ../lib/dist/deployTargets.js
8293
+ var VARS_DIRS = ["group_vars", "host_vars"];
8294
+ function parseInventoryFile(path3, text) {
8295
+ const data = (0, import_yaml4.parse)(text);
8296
+ const vars = data !== null && typeof data === "object" ? data : {};
8297
+ const target = typeof vars.plexus_deploy_target === "string" ? vars.plexus_deploy_target : void 0;
8298
+ const records = Array.isArray(vars.plexus_apps) ? vars.plexus_apps : [];
8299
+ const apps = records.map((r) => r !== null && typeof r === "object" ? r.name : void 0).filter((n) => typeof n === "string");
8300
+ return { path: path3, target, apps };
8301
+ }
8302
+ function selectDeployTargets(files) {
8303
+ const targets = {};
8304
+ const declaredIn = {};
8305
+ for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
8306
+ if (file.target === void 0)
8307
+ continue;
8308
+ for (const app of file.apps) {
8309
+ if (app in targets)
8310
+ throw new Error(`app "${app}" is declared with a deploy target in both ${declaredIn[app]} and ${file.path}`);
8311
+ targets[app] = file.target;
8312
+ declaredIn[app] = file.path;
8313
+ }
8314
+ }
8315
+ return Object.fromEntries(Object.entries(targets).sort(([a], [b]) => a.localeCompare(b)));
8316
+ }
8317
+ async function readDeployTargets(ctx, inventory) {
8318
+ const files = [];
8319
+ let found = false;
8320
+ for (const sub of VARS_DIRS) {
8321
+ const dir = join5(inventory, sub);
8322
+ const entries = await ctx.readDir(dir).catch(() => void 0);
8323
+ if (entries === void 0)
8324
+ continue;
8325
+ found = true;
8326
+ for (const e of entries) {
8327
+ if (e.directory || !/\.ya?ml$/.test(e.name))
8328
+ continue;
8329
+ const path3 = join5(dir, e.name);
8330
+ files.push(parseInventoryFile(path3, await ctx.readFile(path3)));
8331
+ }
8332
+ }
8333
+ if (!found && await ctx.readDir(inventory).catch(() => void 0) === void 0)
8334
+ throw new UsageError(`no inventory directory at ${inventory}`);
8335
+ return selectDeployTargets(files);
8336
+ }
8337
+
7996
8338
  // ../lib/dist/model.js
7997
8339
  function kindOf(declaration) {
7998
8340
  const has = (code) => declaration.modules.includes(code);
@@ -8014,7 +8356,7 @@ function isNotable(dir) {
8014
8356
  }
8015
8357
 
8016
8358
  // ../lib/dist/overview.js
8017
- import { join as join2 } from "node:path";
8359
+ import { join as join6 } from "node:path";
8018
8360
  var PRUNED = /* @__PURE__ */ new Set([
8019
8361
  "node_modules",
8020
8362
  ".hg",
@@ -8062,7 +8404,7 @@ async function walk(ctx, path3, name, options) {
8062
8404
  }
8063
8405
  const subdirs = entries.filter((e) => e.directory && e.name !== ".git" && !PRUNED.has(e.name)).map((e) => e.name).sort();
8064
8406
  for (const sub of subdirs) {
8065
- const child = await walk(ctx, join2(path3, sub), sub, options);
8407
+ const child = await walk(ctx, join6(path3, sub), sub, options);
8066
8408
  if (isNotable(child) || child.children.length > 0)
8067
8409
  dir.children.push(child);
8068
8410
  }
@@ -8070,7 +8412,7 @@ async function walk(ctx, path3, name, options) {
8070
8412
  }
8071
8413
  async function facet(ctx, dir, file, read) {
8072
8414
  try {
8073
- read(await ctx.readFile(join2(dir.path, file)));
8415
+ read(await ctx.readFile(join6(dir.path, file)));
8074
8416
  } catch (error) {
8075
8417
  dir.problems ??= [];
8076
8418
  dir.problems.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
@@ -8109,93 +8451,12 @@ async function templateFacet(ctx, cwd) {
8109
8451
  }
8110
8452
 
8111
8453
  // ../lib/dist/propagate.js
8112
- import { join as join4, relative as relative3 } from "node:path";
8113
-
8114
- // ../lib/dist/train.js
8115
- import { join as join3 } from "node:path";
8116
- var TRAIN_REPOS = [
8117
- "platform",
8118
- "ci-cd",
8119
- "preset-repo-web",
8120
- "preset-app-nextjs",
8121
- "preset-repo-platform",
8122
- "compendium"
8123
- ];
8124
- var UPSTREAM = "plexus-ms";
8125
- var isTrainVersion = (v) => /^\d+\.\d+\.\d+$/.test(v);
8126
- function nextMinor(version2) {
8127
- const [major = "0", minor = "0"] = version2.split(".");
8128
- return `${major}.${Number(minor) + 1}.0`;
8129
- }
8130
- function latestTag(tags) {
8131
- return tags.filter((t) => /^v?\d/.test(t)).sort((a, b) => compareTrains(a.replace(/^v/, ""), b.replace(/^v/, ""))).at(-1);
8132
- }
8133
- async function readTrain(ctx, upstream) {
8134
- const repos = [];
8135
- const all = /* @__PURE__ */ new Set();
8136
- for (const name of TRAIN_REPOS) {
8137
- const path3 = join3(upstream, name);
8138
- try {
8139
- await ctx.read("git", ["rev-parse", "--git-dir"], { cwd: path3 });
8140
- } catch {
8141
- throw new Error(`${path3} is not a git repo`);
8142
- }
8143
- try {
8144
- await ctx.read("git", ["fetch", "origin", "--tags", "--quiet"], { cwd: path3 });
8145
- } catch {
8146
- throw new Error(`${name}: fetch from origin failed`);
8147
- }
8148
- const tags = (await ctx.read("git", ["tag", "--list", "v*", "[0-9]*"], { cwd: path3 })).split("\n").filter(Boolean);
8149
- for (const t of tags)
8150
- all.add(t.replace(/^v/, ""));
8151
- const tag = latestTag(tags);
8152
- const range = tag ? `${tag}..HEAD` : "HEAD";
8153
- const pending = (await ctx.read("git", ["log", "--oneline", range], { cwd: path3 })).split("\n").filter(Boolean);
8154
- repos.push({ name, path: path3, tag, latest: tag?.replace(/^v/, ""), pending });
8155
- }
8156
- const latest = repos.map((r) => r.latest);
8157
- return { repos, last: latestTag([...all]), skew: latest.some((l) => l !== latest[0]) };
8158
- }
8159
- async function preflightRelease(ctx, train) {
8160
- for (const { name, path: path3 } of train.repos) {
8161
- const branch = (await ctx.read("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path3 })).trim();
8162
- if (branch !== "main")
8163
- throw new Error(`${name} is on '${branch}', trains cut from main`);
8164
- if ((await ctx.read("git", ["status", "--porcelain"], { cwd: path3 })).trim() !== "")
8165
- throw new Error(`${name} has uncommitted changes \u2014 commit first`);
8166
- const behind = (await ctx.read("git", ["rev-list", "--count", "main..origin/main"], { cwd: path3 })).trim();
8167
- if (behind !== "0")
8168
- throw new Error(`${name} is ${behind} commit(s) behind origin/main \u2014 pull first, or the tag lands on a stale commit`);
8169
- }
8170
- }
8171
- function resolveVersion(train, explicit, propagateOnly) {
8172
- if (explicit !== void 0 && !isTrainVersion(explicit))
8173
- throw new UsageError(`'${explicit}' is not plain semver (X.Y.Z, no v)`);
8174
- if (propagateOnly) {
8175
- const v = explicit ?? train.last;
8176
- if (!v)
8177
- throw new Error("no train version found to propagate");
8178
- return v;
8179
- }
8180
- if (explicit)
8181
- return explicit;
8182
- if (train.skew) {
8183
- const report = train.repos.map((r) => ` ${r.name}: ${r.latest ?? "none"}`).join("\n");
8184
- throw new Error(`the train repos' latest tags disagree:
8185
- ${report}
8186
- pass the version explicitly (--train ${train.last}) to finish the half-tagged train, or a higher one to start a new one`);
8187
- }
8188
- return train.last ? nextMinor(train.last) : "0.1.0";
8189
- }
8190
- var UsageError = class extends Error {
8191
- };
8192
-
8193
- // ../lib/dist/propagate.js
8454
+ import { join as join7, relative as relative3 } from "node:path";
8194
8455
  var isClean = (s) => s.conflicted.length === 0 && s.failed.length === 0 && s.skipped.length === 0;
8195
8456
  var PREVIEW_BASE = "ship-base";
8196
8457
  async function propagate(ctx, workspace, train) {
8197
8458
  const summary = { train, preview: ctx.dryRun, clean: [], conflicted: [], failed: [], skipped: [] };
8198
- const upstream = join4(workspace, UPSTREAM);
8459
+ const upstream = join7(workspace, UPSTREAM);
8199
8460
  const tree = await overview(ctx, workspace);
8200
8461
  const upstreamDir = tree.children.find((c) => c.path === upstream);
8201
8462
  const presets = TRAIN_REPOS.filter((r) => upstreamDir?.children.some((c) => c.name === r && c.template));
@@ -8212,7 +8473,7 @@ async function propagate(ctx, workspace, train) {
8212
8473
  const cloneOf = async (consumer) => {
8213
8474
  let clone = clones.get(consumer.path);
8214
8475
  if (!clone) {
8215
- clone = join4(scratch ?? "", consumer.rel);
8476
+ clone = join7(scratch ?? "", consumer.rel);
8216
8477
  await ctx.stage(run("git", ["clone", "--quiet", consumer.path, clone]));
8217
8478
  await ctx.stage(run("git", ["tag", PREVIEW_BASE], { cwd: clone }));
8218
8479
  clones.set(consumer.path, clone);
@@ -8239,7 +8500,7 @@ async function propagate(ctx, workspace, train) {
8239
8500
  if (sites.length === 0)
8240
8501
  ctx.say(`\u2022 no consumers of ${presetSource(preset)} found under ${workspace}`);
8241
8502
  for (const [consumer, site] of sites)
8242
- await updateSite(ctx, consumer, site, join4(upstream, preset), summary, cloneOf, previewCommit);
8503
+ await updateSite(ctx, consumer, site, join7(upstream, preset), summary, cloneOf, previewCommit);
8243
8504
  }
8244
8505
  await refreshLockfiles(ctx, active, summary, clones);
8245
8506
  await bumpConsumerRefs(ctx, consumers, train);
@@ -8256,9 +8517,9 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
8256
8517
  if (ctx.dryRun) {
8257
8518
  ctx.say(`\u2192 ${site.rel}: copier update (preview from ${localPreset}@main)`);
8258
8519
  repo = await cloneOf(consumer);
8259
- target = join4(repo, relative3(consumer.path, site.path));
8260
- const answers = await ctx.readFile(join4(site.path, ANSWERS_FILE));
8261
- await ctx.stage(write(join4(target, ANSWERS_FILE), answers.replace(/^_src_path: .*$/m, `_src_path: ${localPreset}`)));
8520
+ target = join7(repo, relative3(consumer.path, site.path));
8521
+ const answers = await ctx.readFile(join7(site.path, ANSWERS_FILE));
8522
+ await ctx.stage(write(join7(target, ANSWERS_FILE), answers.replace(/^_src_path: .*$/m, `_src_path: ${localPreset}`)));
8262
8523
  await previewCommit(repo, `plx preview: ${site.rel} reads ${localPreset} from the local checkout`);
8263
8524
  await ctx.stage(run("copier", ["update", "--skip-answered", "--trust", "--defaults", "--skip-tasks", "--vcs-ref", "main"], {
8264
8525
  cwd: target
@@ -8277,7 +8538,7 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
8277
8538
  let conflict = hasUnmergedEntries(status);
8278
8539
  const changed = (await ctx.read("git", ["diff", "--name-only"], { cwd: repo })).split("\n").filter(Boolean);
8279
8540
  for (const f of changed) {
8280
- const text = await ctx.readFile(join4(repo, f)).catch(() => "");
8541
+ const text = await ctx.readFile(join7(repo, f)).catch(() => "");
8281
8542
  if (hasConflictMarkers(text))
8282
8543
  conflict = true;
8283
8544
  }
@@ -8295,17 +8556,17 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
8295
8556
  await previewCommit(repo, `plx preview: ${site.rel} updated`);
8296
8557
  }
8297
8558
  async function syncSiteMarker(ctx, site, target) {
8298
- const marker = await ctx.readFile(join4(target, MARKER_FILE)).catch(() => void 0);
8559
+ const marker = await ctx.readFile(join7(target, MARKER_FILE)).catch(() => void 0);
8299
8560
  if (marker === void 0)
8300
8561
  return;
8301
- const { commit } = parseAnswers(await ctx.readFile(join4(target, ANSWERS_FILE)));
8562
+ const { commit } = parseAnswers(await ctx.readFile(join7(target, ANSWERS_FILE)));
8302
8563
  if (!commit)
8303
8564
  return;
8304
8565
  const synced = syncMarker(marker, commit);
8305
8566
  if (synced === marker)
8306
8567
  return;
8307
8568
  ctx.say(` marker: plexus \u2192 ${commit} in ${site.rel}/${MARKER_FILE}`);
8308
- const step = write(join4(target, MARKER_FILE), synced);
8569
+ const step = write(join7(target, MARKER_FILE), synced);
8309
8570
  if (ctx.dryRun)
8310
8571
  await ctx.stage(step);
8311
8572
  else
@@ -8314,7 +8575,7 @@ async function syncSiteMarker(ctx, site, target) {
8314
8575
  async function refreshLockfiles(ctx, consumers, summary, clones) {
8315
8576
  const manifests = ["*package.json", "pnpm-workspace.yaml"];
8316
8577
  for (const c of consumers) {
8317
- if (await ctx.readFile(join4(c.path, "pnpm-workspace.yaml")).catch(() => void 0) === void 0)
8578
+ if (await ctx.readFile(join7(c.path, "pnpm-workspace.yaml")).catch(() => void 0) === void 0)
8318
8579
  continue;
8319
8580
  const repo = ctx.dryRun ? clones.get(c.path) : c.path;
8320
8581
  if (!repo)
@@ -8334,7 +8595,7 @@ async function refreshLockfiles(ctx, consumers, summary, clones) {
8334
8595
  }
8335
8596
  async function bumpConsumerRefs(ctx, consumers, train) {
8336
8597
  for (const c of consumers) {
8337
- const dir = join4(c.path, ".github", "workflows");
8598
+ const dir = join7(c.path, ".github", "workflows");
8338
8599
  const entries = await ctx.readDir(dir).catch(() => []);
8339
8600
  const files = entries.filter((e) => !e.directory && /\.ya?ml$/.test(e.name)).map((e) => e.name).sort();
8340
8601
  if (files.length === 0)
@@ -8344,14 +8605,14 @@ async function bumpConsumerRefs(ctx, consumers, train) {
8344
8605
  continue;
8345
8606
  }
8346
8607
  for (const f of files) {
8347
- const text = await ctx.readFile(join4(dir, f));
8608
+ const text = await ctx.readFile(join7(dir, f));
8348
8609
  if (!text.includes(WORKFLOW_REF))
8349
8610
  continue;
8350
8611
  const bumped = bumpWorkflowRefs(text, train);
8351
8612
  if (bumped === text)
8352
8613
  continue;
8353
8614
  ctx.say(`\u2192 ${c.rel}: ${f} ci-cd refs \u2192 @${train}`);
8354
- await ctx.apply([write(join4(dir, f), bumped)]);
8615
+ await ctx.apply([write(join7(dir, f), bumped)]);
8355
8616
  }
8356
8617
  }
8357
8618
  }
@@ -8362,7 +8623,7 @@ var detail = (error) => {
8362
8623
  };
8363
8624
 
8364
8625
  // ../lib/dist/requirements.js
8365
- import { join as join5 } from "node:path";
8626
+ import { join as join8 } from "node:path";
8366
8627
  var KEYWORD = /MUST|SHOULD|MAY/;
8367
8628
  var hasKeyword = (line) => KEYWORD.test(line.replace(/MUSTs|SHOULDs|MAYs/g, ""));
8368
8629
  var list = (value) => value.replace(/^\s*\[/, "").replace(/\].*$/, "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -8455,7 +8716,7 @@ var MAP_PAGE = "essentials/standard.md";
8455
8716
  async function modulePages(ctx, compendium) {
8456
8717
  const pages = [];
8457
8718
  for (const dir of Object.keys(CATALOG_DIRS)) {
8458
- const entries = await ctx.readDir(join5(compendium, dir));
8719
+ const entries = await ctx.readDir(join8(compendium, dir));
8459
8720
  for (const e of entries.filter((e2) => !e2.directory && e2.name.endsWith(".md") && e2.name !== "index.md").sort())
8460
8721
  pages.push(`${dir}/${e.name}`);
8461
8722
  }
@@ -8463,11 +8724,11 @@ async function modulePages(ctx, compendium) {
8463
8724
  }
8464
8725
  async function checkCompendium(ctx, compendium) {
8465
8726
  const defects = [];
8466
- defects.push(...scanPage(await ctx.readFile(join5(compendium, MAP_PAGE)), MAP_PAGE, "map").defects);
8727
+ defects.push(...scanPage(await ctx.readFile(join8(compendium, MAP_PAGE)), MAP_PAGE, "map").defects);
8467
8728
  const pages = await modulePages(ctx, compendium);
8468
8729
  const scans = /* @__PURE__ */ new Map();
8469
8730
  for (const page of pages) {
8470
- const scan = scanPage(await ctx.readFile(join5(compendium, page)), page, "module");
8731
+ const scan = scanPage(await ctx.readFile(join8(compendium, page)), page, "module");
8471
8732
  defects.push(...scan.defects);
8472
8733
  scans.set(page, scan);
8473
8734
  }
@@ -8514,9 +8775,9 @@ async function checkCompendium(ctx, compendium) {
8514
8775
  }
8515
8776
 
8516
8777
  // ../lib/dist/ship.js
8517
- import { join as join6 } from "node:path";
8778
+ import { join as join9 } from "node:path";
8518
8779
  async function ship(ctx, options) {
8519
- const upstream = join6(options.workspace, UPSTREAM);
8780
+ const upstream = join9(options.workspace, UPSTREAM);
8520
8781
  const propagateOnly = options.propagateOnly ?? false;
8521
8782
  const train = await readTrain(ctx, upstream);
8522
8783
  if (!propagateOnly)
@@ -8567,7 +8828,7 @@ async function planBookkeeping(ctx, train, next, today2) {
8567
8828
  return r;
8568
8829
  };
8569
8830
  const edit = async (r, rel, what, transform) => {
8570
- const path3 = join6(r.path, rel);
8831
+ const path3 = join9(r.path, rel);
8571
8832
  const text = await ctx.readFile(path3).catch(() => void 0);
8572
8833
  if (text === void 0)
8573
8834
  return;
@@ -8588,7 +8849,7 @@ async function planBookkeeping(ctx, train, next, today2) {
8588
8849
  const comp = repo("compendium");
8589
8850
  await edit(comp, MAP_PAGE, `version \u2192 ${next}, timestamp \u2192 ${today2}`, (t) => stampStandard(t, next, today2));
8590
8851
  for (const page of await modulePages(ctx, comp.path)) {
8591
- const text = await ctx.readFile(join6(comp.path, page));
8852
+ const text = await ctx.readFile(join9(comp.path, page));
8592
8853
  if (frontmatterField(text, "maturity") !== "standard")
8593
8854
  continue;
8594
8855
  const previous = comp.tag ? await ctx.read("git", ["show", `${comp.tag}:${page}`], { cwd: comp.path }).catch(() => "") : "";
@@ -9551,13 +9812,13 @@ import { AsyncResource as AsyncResource3 } from "node:async_hooks";
9551
9812
 
9552
9813
  // ../../node_modules/.pnpm/@inquirer+type@4.2.0_@types+node@26.6.5/node_modules/@inquirer/type/dist/utils.js
9553
9814
  function withResolver() {
9554
- let resolve6;
9815
+ let resolve9;
9555
9816
  let reject;
9556
9817
  const promise = new Promise((res, rej) => {
9557
- resolve6 = res;
9818
+ resolve9 = res;
9558
9819
  reject = rej;
9559
9820
  });
9560
- return { promise, resolve: resolve6, reject };
9821
+ return { promise, resolve: resolve9, reject };
9561
9822
  }
9562
9823
 
9563
9824
  // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/create-prompt.js
@@ -9936,7 +10197,7 @@ function createPrompt(view) {
9936
10197
  });
9937
10198
  output.mute();
9938
10199
  const screen = new ScreenManager(rl);
9939
- const { promise, resolve: resolve6, reject } = withResolver();
10200
+ const { promise, resolve: resolve9, reject } = withResolver();
9940
10201
  return withHooks(rl, (cycle) => {
9941
10202
  const clearEffects = AsyncResource3.bind(() => effectScheduler.clearAll());
9942
10203
  const settlePrompt = (settle) => {
@@ -9947,7 +10208,7 @@ function createPrompt(view) {
9947
10208
  reject(error);
9948
10209
  }
9949
10210
  };
9950
- const resolvePrompt = (value) => settlePrompt(() => resolve6(value));
10211
+ const resolvePrompt = (value) => settlePrompt(() => resolve9(value));
9951
10212
  const rejectPrompt = (error) => settlePrompt(() => reject(error));
9952
10213
  const promptPromise = Object.assign(promise.finally(() => {
9953
10214
  cleanups.forEach((cleanup) => cleanup());
@@ -13466,9 +13727,45 @@ function useColor() {
13466
13727
  // ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/index.js
13467
13728
  var program = new Command();
13468
13729
 
13730
+ // src/commands/changed-apps.ts
13731
+ import { resolve } from "node:path";
13732
+ function register(program2) {
13733
+ program2.command("changed-apps").description("list the apps whose sources changed between two refs, dependents of a changed package included").argument("<from>", "the base ref; empty, all zeros or unknown lists every app").argument("[to]", "the ref compared against", "HEAD").option("-C, --dir <dir>", "the checkout to read", ".").action(async (from, to, opts, command) => {
13734
+ const ctx = contextOf2(command);
13735
+ const result = await readChangedApps(ctx, resolve(opts.dir), from, to);
13736
+ ctx.result(result.apps);
13737
+ });
13738
+ }
13739
+
13740
+ // src/commands/changeset.ts
13741
+ import { resolve as resolve2 } from "node:path";
13742
+ function register2(program2) {
13743
+ program2.command("changeset").description("write the changeset a dependency bump owes: a patch per package whose runtime dependencies moved").argument("[dir]", "a checkout of the package repo, on the bump branch", ".").option("--base <ref>", "the trunk the branch is diffed against, at the merge base", "origin/main").option("--branch <name>", "the branch name that names the file (default: the checked-out branch)").action(async (dir, opts, command) => {
13744
+ const ctx = contextOf2(command);
13745
+ const changeset = await readChangeset(ctx, resolve2(dir), opts.base, opts.branch);
13746
+ await ctx.apply(changeset.plan);
13747
+ ctx.result(changeset, renderChangeset);
13748
+ });
13749
+ }
13750
+ function renderChangeset(c) {
13751
+ const releases = Object.entries(c.releases);
13752
+ if (releases.length === 0) return `\u2713 nothing owed; no ${c.path}`;
13753
+ return [`\u2713 ${c.path}`, ...releases.map(([name, bump]) => ` ${name}: ${bump}`)].join("\n");
13754
+ }
13755
+
13756
+ // src/commands/deploy-targets.ts
13757
+ import { resolve as resolve3 } from "node:path";
13758
+ function register3(program2) {
13759
+ program2.command("deploy-targets").description("map each app of a platform repo to the deploy target of the inventory file that declares it").argument("[inventory]", "the inventory directory", "inventory").action(async (inventory, _opts, command) => {
13760
+ const ctx = contextOf2(command);
13761
+ const targets = await readDeployTargets(ctx, resolve3(inventory));
13762
+ ctx.result(targets, (t) => JSON.stringify(t));
13763
+ });
13764
+ }
13765
+
13469
13766
  // src/commands/doctor.ts
13470
13767
  import { existsSync } from "node:fs";
13471
- import { join as join7, resolve } from "node:path";
13768
+ import { join as join10, resolve as resolve4 } from "node:path";
13472
13769
 
13473
13770
  // src/style.ts
13474
13771
  function style(enabled) {
@@ -13479,15 +13776,15 @@ var colorByDefault = () => process.stdout.isTTY === true && !process.env.NO_COLO
13479
13776
 
13480
13777
  // src/commands/doctor.ts
13481
13778
  var WORKSPACE_COMPENDIUM = "plexus-ms/compendium";
13482
- function register(program2) {
13779
+ function register4(program2) {
13483
13780
  program2.command("doctor").description("check every PLEXUS.md under a directory for modules that changed since and dependencies not declared").argument("[dir]", "directory to check", ".").option(
13484
13781
  "--compendium <path>",
13485
13782
  `compendium checkout to read the modules from (default: <dir>/${WORKSPACE_COMPENDIUM})`
13486
13783
  ).option("--color", "colour the report even when piped").option("--no-color", "never colour the report").action(async (dir, opts, command) => {
13487
13784
  const ctx = contextOf2(command);
13488
- const root = resolve(dir);
13489
- const compendium = resolve(opts.compendium ?? join7(root, WORKSPACE_COMPENDIUM));
13490
- if (!existsSync(join7(compendium, "contracts")))
13785
+ const root = resolve4(dir);
13786
+ const compendium = resolve4(opts.compendium ?? join10(root, WORKSPACE_COMPENDIUM));
13787
+ if (!existsSync(join10(compendium, "contracts")))
13491
13788
  throw new Error(
13492
13789
  `no compendium at ${compendium}; pass --compendium <path to a checkout of plexus-ms/compendium>`
13493
13790
  );
@@ -13515,11 +13812,11 @@ function renderReport(report, s, showCurrent) {
13515
13812
  }
13516
13813
 
13517
13814
  // src/commands/overview.ts
13518
- import { resolve as resolve2 } from "node:path";
13519
- function register2(program2) {
13815
+ import { resolve as resolve5 } from "node:path";
13816
+ function register5(program2) {
13520
13817
  program2.command("overview").description("map the git repos, copier templates and projects, and Plexus units under a directory").argument("[dir]", "directory to map", ".").option("--color", "colour the tree even when piped").option("--no-color", "never colour the tree").action(async (dir, opts, command) => {
13521
13818
  const ctx = contextOf2(command);
13522
- const tree = await overview(ctx, resolve2(dir));
13819
+ const tree = await overview(ctx, resolve5(dir));
13523
13820
  const s = style(opts.color ?? colorByDefault());
13524
13821
  ctx.result(tree, (t) => renderTree(t, s, ctx.verbose >= 1));
13525
13822
  });
@@ -13562,14 +13859,14 @@ function annotate(dir, s, modules) {
13562
13859
  }
13563
13860
 
13564
13861
  // src/commands/propagate.ts
13565
- import { resolve as resolve4 } from "node:path";
13862
+ import { resolve as resolve7 } from "node:path";
13566
13863
 
13567
13864
  // src/commands/ship.ts
13568
- import { resolve as resolve3 } from "node:path";
13569
- function register3(program2) {
13865
+ import { resolve as resolve6 } from "node:path";
13866
+ function register6(program2) {
13570
13867
  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) => {
13571
13868
  const ctx = contextOf2(command);
13572
- const result = await ship(ctx, { workspace: resolve3(dir), train: opts.train });
13869
+ const result = await ship(ctx, { workspace: resolve6(dir), train: opts.train });
13573
13870
  ctx.result(result, (r) => renderSummary(r, `plx -y ship --train ${r.train} ${dir}`));
13574
13871
  if (!isClean(result)) process.exitCode = 1;
13575
13872
  });
@@ -13592,23 +13889,23 @@ function renderSummary(s, realCommand) {
13592
13889
  }
13593
13890
 
13594
13891
  // src/commands/propagate.ts
13595
- function register4(program2) {
13892
+ function register7(program2) {
13596
13893
  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) => {
13597
13894
  const ctx = contextOf2(command);
13598
- const result = await ship(ctx, { workspace: resolve4(dir), train: opts.train, propagateOnly: true });
13895
+ const result = await ship(ctx, { workspace: resolve7(dir), train: opts.train, propagateOnly: true });
13599
13896
  ctx.result(result, (r) => renderSummary(r, `plx -y propagate --train ${r.train} ${dir}`));
13600
13897
  if (!isClean(result)) process.exitCode = 1;
13601
13898
  });
13602
13899
  }
13603
13900
 
13604
13901
  // src/commands/requirements.ts
13605
- import { join as join8, resolve as resolve5 } from "node:path";
13606
- function register5(program2) {
13902
+ import { join as join11, resolve as resolve8 } from "node:path";
13903
+ function register8(program2) {
13607
13904
  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) => {
13608
13905
  const ctx = contextOf2(command);
13609
- const compendium = resolve5(dir);
13906
+ const compendium = resolve8(dir);
13610
13907
  if (opts.extract) {
13611
- ctx.result(extractRequirements(await ctx.readFile(join8(compendium, opts.extract))));
13908
+ ctx.result(extractRequirements(await ctx.readFile(join11(compendium, opts.extract))));
13612
13909
  return;
13613
13910
  }
13614
13911
  const check = await checkCompendium(ctx, compendium);
@@ -13622,7 +13919,16 @@ function renderCheck(c) {
13622
13919
  }
13623
13920
 
13624
13921
  // src/commands/index.ts
13625
- var commands = [register2, register, register3, register4, register5];
13922
+ var commands = [
13923
+ register5,
13924
+ register4,
13925
+ register6,
13926
+ register7,
13927
+ register8,
13928
+ register2,
13929
+ register,
13930
+ register3
13931
+ ];
13626
13932
 
13627
13933
  // src/program.ts
13628
13934
  var { version, description } = createRequire(import.meta.url)("../package.json");
@@ -13631,7 +13937,7 @@ function buildProgram() {
13631
13937
  program2.addOption(new Option("-n, --dry-run", "print every effect this run would have, perform none").env("PLX_DRY_RUN")).addOption(
13632
13938
  new Option("-v, --verbose", "say more: once for reads and inputs, twice for command output").argParser((_value, previous) => previous + 1).default(0)
13633
13939
  ).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"));
13634
- for (const register6 of commands) register6(program2);
13940
+ for (const register9 of commands) register9(program2);
13635
13941
  return program2;
13636
13942
  }
13637
13943
  function contextOf2(command) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plexus-ms/cli",
3
- "version": "0.3.0",
3
+ "version": "0.5.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",
@@ -20,15 +20,17 @@
20
20
  "access": "public"
21
21
  },
22
22
  "engines": {
23
- "node": ">=24"
23
+ "node": ">=24.22.1"
24
+ },
25
+ "dependencies": {
26
+ "@inquirer/prompts": "^8.7.4",
27
+ "@plexus-ms/lib": "0.5.0",
28
+ "commander": "^15.0.0"
24
29
  },
25
30
  "devDependencies": {
26
- "@inquirer/prompts": "^8.0.0",
27
31
  "@types/node": "^26.6.5",
28
- "commander": "^15.0.0",
29
32
  "esbuild": "^0.28.2",
30
- "typescript": "^6.0.3",
31
- "@plexus-ms/lib": "0.3.0"
33
+ "typescript": "^6.0.3"
32
34
  },
33
35
  "scripts": {
34
36
  "build": "node build.js",