@plexus-ms/cli 0.3.0 → 0.4.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 +267 -116
  2. package/package.json +7 -5
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 parse4(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 = parse4;
7313
7313
  exports.parseAllDocuments = parseAllDocuments;
7314
7314
  exports.parseDocument = parseDocument;
7315
7315
  exports.stringify = stringify;
@@ -7655,62 +7655,9 @@ 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;
7668
- }
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);
7699
- };
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}'`;
7706
- continue;
7707
- }
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
- }
7712
- return [...consumers.values()];
7713
- }
7658
+ // ../lib/dist/changeset.js
7659
+ var import_yaml2 = __toESM(require_dist(), 1);
7660
+ import { join as join2 } from "node:path";
7714
7661
 
7715
7662
  // ../lib/dist/io.js
7716
7663
  import { spawn } from "node:child_process";
@@ -7733,13 +7680,13 @@ function realIo(prompt) {
7733
7680
  stdout: process.stdout,
7734
7681
  stderr: process.stderr,
7735
7682
  prompt: prompt && isInteractive() ? prompt : void 0,
7736
- exec: (command, args, options = {}) => new Promise((resolve6, reject) => {
7683
+ exec: (command, args, options = {}) => new Promise((resolve7, reject) => {
7737
7684
  const child = spawn(command, args, {
7738
7685
  cwd: options.cwd,
7739
7686
  env: options.env,
7740
7687
  stdio: [
7741
7688
  options.input !== void 0 ? "pipe" : options.inherit ? "inherit" : "ignore",
7742
- options.inherit ? "inherit" : "pipe",
7689
+ options.inherit ? process.stderr.fd : "pipe",
7743
7690
  "pipe"
7744
7691
  ]
7745
7692
  });
@@ -7756,7 +7703,7 @@ function realIo(prompt) {
7756
7703
  child.on("error", reject);
7757
7704
  child.on("close", (code) => {
7758
7705
  if (code === 0)
7759
- resolve6(stdout);
7706
+ resolve7(stdout);
7760
7707
  else
7761
7708
  reject(new ExecError(render(command, args), code, stderr));
7762
7709
  });
@@ -7805,6 +7752,194 @@ EOF`;
7805
7752
  }
7806
7753
  }
7807
7754
 
7755
+ // ../lib/dist/changeset.js
7756
+ var CHANGESET_DIR = ".changeset";
7757
+ var FIELDS = ["dependencies", "optionalDependencies", "peerDependencies"];
7758
+ function changesetPath(branch) {
7759
+ const slug = branch.replace(/^(renovate|dependabot)\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-|-$/g, "").toLowerCase();
7760
+ return `${CHANGESET_DIR}/bot-${slug || "branch"}.md`;
7761
+ }
7762
+ function planChangeset(input) {
7763
+ const path3 = changesetPath(input.branch);
7764
+ const changes = [];
7765
+ const releases = {};
7766
+ for (const diff of input.packages) {
7767
+ const { before, after } = diff;
7768
+ if (!before || !after?.name || after.private || input.ignore.includes(after.name))
7769
+ continue;
7770
+ for (const field of FIELDS) {
7771
+ const names = /* @__PURE__ */ new Set([...Object.keys(before[field] ?? {}), ...Object.keys(after[field] ?? {})]);
7772
+ for (const dependency of [...names].sort()) {
7773
+ const from = resolveRange(before[field]?.[dependency], dependency, input.catalogsBefore);
7774
+ const to = resolveRange(after[field]?.[dependency], dependency, input.catalogsAfter);
7775
+ if (from === to)
7776
+ continue;
7777
+ changes.push({ package: after.name, dependency, field, from, to });
7778
+ const bump = field === "peerDependencies" ? "major" : "patch";
7779
+ if (releases[after.name] !== "major")
7780
+ releases[after.name] = bump;
7781
+ }
7782
+ }
7783
+ }
7784
+ const root = input.root;
7785
+ if (changes.length === 0) {
7786
+ const remove = run("rm", ["-f", path3], root ? { cwd: root } : {});
7787
+ return { path: path3, releases, changes, plan: input.exists ? [remove] : [] };
7788
+ }
7789
+ const header = Object.entries(releases).sort(([a], [b]) => a.localeCompare(b)).map(([name, bump]) => `'${name}': ${bump}`);
7790
+ const lines = [...new Set(changes.map(describeChange))].sort((a, b) => a.localeCompare(b));
7791
+ const content = `---
7792
+ ${header.join("\n")}
7793
+ ---
7794
+
7795
+ ${lines.join("\n")}
7796
+ `;
7797
+ return { path: path3, releases, changes, content, plan: [write(root ? join2(root, path3) : path3, content)] };
7798
+ }
7799
+ function describeChange(c) {
7800
+ const peer = c.field === "peerDependencies" ? " (peer)" : "";
7801
+ if (c.from === void 0)
7802
+ return `Add \`${c.dependency}\`${peer} at ${c.to}`;
7803
+ if (c.to === void 0)
7804
+ return `Remove \`${c.dependency}\`${peer}`;
7805
+ return `Update \`${c.dependency}\`${peer} from ${c.from} to ${c.to}`;
7806
+ }
7807
+ function resolveRange(value, dependency, catalogs) {
7808
+ if (value === void 0)
7809
+ return void 0;
7810
+ if (value === "catalog:" || value === "catalog:default")
7811
+ return catalogs.default[dependency] ?? value;
7812
+ if (value.startsWith("catalog:"))
7813
+ return catalogs.named[value.slice("catalog:".length)]?.[dependency] ?? value;
7814
+ return value;
7815
+ }
7816
+ function parseCatalogs(text) {
7817
+ const data = text === void 0 ? void 0 : (0, import_yaml2.parse)(text);
7818
+ const ws = data !== null && typeof data === "object" ? data : {};
7819
+ const map = (v) => v !== null && typeof v === "object" ? Object.fromEntries(Object.entries(v).map(([k, r]) => [k, String(r)])) : {};
7820
+ const named = ws.catalogs !== null && typeof ws.catalogs === "object" ? ws.catalogs : {};
7821
+ return {
7822
+ default: map(ws.catalog),
7823
+ named: Object.fromEntries(Object.entries(named).map(([name, entries]) => [name, map(entries)]))
7824
+ };
7825
+ }
7826
+ function workspaceGlobs(text) {
7827
+ const data = text === void 0 ? void 0 : (0, import_yaml2.parse)(text);
7828
+ const globs = data !== null && typeof data === "object" ? data.packages : void 0;
7829
+ return Array.isArray(globs) ? globs.map(String).filter((g) => !g.startsWith("!")) : [];
7830
+ }
7831
+ async function readChangeset(ctx, dir, base, branch) {
7832
+ const git2 = (args) => ctx.read("git", args, { cwd: dir });
7833
+ const head = branch ?? (await git2(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
7834
+ const mergeBase = (await git2(["merge-base", base, "HEAD"])).trim();
7835
+ const show = async (path4) => {
7836
+ try {
7837
+ return await git2(["show", `${mergeBase}:${path4}`]);
7838
+ } catch (error) {
7839
+ if (error instanceof ExecError)
7840
+ return void 0;
7841
+ throw error;
7842
+ }
7843
+ };
7844
+ const current = async (path4) => {
7845
+ try {
7846
+ return await ctx.readFile(join2(dir, path4));
7847
+ } catch {
7848
+ return void 0;
7849
+ }
7850
+ };
7851
+ const workspaceAfter = await current("pnpm-workspace.yaml");
7852
+ const packages = [];
7853
+ for (const glob of workspaceGlobs(workspaceAfter)) {
7854
+ for (const pkgDir of await expand(ctx, dir, glob)) {
7855
+ const path4 = `${pkgDir}/package.json`;
7856
+ const [before, after] = await Promise.all([show(path4), current(path4)]);
7857
+ packages.push({ path: path4, before: manifest(before), after: manifest(after) });
7858
+ }
7859
+ }
7860
+ const config = await current(`${CHANGESET_DIR}/config.json`);
7861
+ const ignore = config ? JSON.parse(config).ignore ?? [] : [];
7862
+ const entries = await ctx.readDir(join2(dir, CHANGESET_DIR)).catch(() => []);
7863
+ const path3 = changesetPath(head);
7864
+ return planChangeset({
7865
+ branch: head,
7866
+ packages,
7867
+ catalogsBefore: parseCatalogs(await show("pnpm-workspace.yaml")),
7868
+ catalogsAfter: parseCatalogs(workspaceAfter),
7869
+ ignore,
7870
+ exists: entries.some((e) => !e.directory && `${CHANGESET_DIR}/${e.name}` === path3),
7871
+ root: dir
7872
+ });
7873
+ }
7874
+ function manifest(text) {
7875
+ return text === void 0 ? void 0 : JSON.parse(text);
7876
+ }
7877
+ async function expand(ctx, root, glob) {
7878
+ const star = glob.indexOf("*");
7879
+ if (star < 0)
7880
+ return [glob.replace(/\/$/, "")];
7881
+ const parent = glob.slice(0, star).replace(/\/$/, "");
7882
+ const entries = await ctx.readDir(join2(root, parent)).catch(() => []);
7883
+ return entries.filter((e) => e.directory && !e.name.startsWith(".")).map((e) => parent ? `${parent}/${e.name}` : e.name).sort();
7884
+ }
7885
+
7886
+ // ../lib/dist/consumers.js
7887
+ import { relative } from "node:path";
7888
+ function expectedBranch(declaration) {
7889
+ if (!declaration)
7890
+ return void 0;
7891
+ if (declaration.modules.includes("PLX-Platform"))
7892
+ return "main";
7893
+ if (declaration.modules.includes("PLX-Repo"))
7894
+ return "develop";
7895
+ return void 0;
7896
+ }
7897
+ var presetSource = (preset) => `gh:plexus-ms/${preset}`;
7898
+ async function findConsumers(ctx, root, presets) {
7899
+ const sources = new Map(presets.map((p) => [presetSource(p), p]));
7900
+ const consumers = /* @__PURE__ */ new Map();
7901
+ const orphans = [];
7902
+ const visit = (dir, repo, inTemplate) => {
7903
+ const here = dir.git ? dir : repo;
7904
+ const template = inTemplate || Boolean(dir.template);
7905
+ const preset = dir.copier?.src ? sources.get(dir.copier.src) : void 0;
7906
+ if (preset && !template) {
7907
+ const site = { path: dir.path, rel: relative(root.path, dir.path) || ".", preset };
7908
+ if (!here)
7909
+ orphans.push(site);
7910
+ else {
7911
+ let consumer = consumers.get(here.path);
7912
+ if (!consumer) {
7913
+ consumer = {
7914
+ path: here.path,
7915
+ rel: relative(root.path, here.path) || ".",
7916
+ branch: here.git?.branch ?? "?",
7917
+ expected: expectedBranch(here.declaration),
7918
+ sites: []
7919
+ };
7920
+ consumers.set(here.path, consumer);
7921
+ }
7922
+ consumer.sites.push(site);
7923
+ }
7924
+ }
7925
+ for (const child of dir.children)
7926
+ visit(child, here, template);
7927
+ };
7928
+ visit(root, void 0, false);
7929
+ for (const site of orphans)
7930
+ ctx.say(`\u26A0 ${site.rel}: not inside a git repo \u2014 skipped`);
7931
+ for (const c of consumers.values()) {
7932
+ if (c.expected && c.branch !== c.expected) {
7933
+ c.skipped = `on '${c.branch}', expected '${c.expected}'`;
7934
+ continue;
7935
+ }
7936
+ const tracked = (await ctx.read("git", ["status", "--porcelain", "--untracked-files=no"], { cwd: c.path })).trim();
7937
+ if (tracked !== "")
7938
+ c.skipped = "has uncommitted changes";
7939
+ }
7940
+ return [...consumers.values()];
7941
+ }
7942
+
7808
7943
  // ../lib/dist/context.js
7809
7944
  var Context = class {
7810
7945
  dryRun;
@@ -7919,11 +8054,11 @@ function indent(text) {
7919
8054
  }
7920
8055
 
7921
8056
  // ../lib/dist/copier.js
7922
- var import_yaml2 = __toESM(require_dist(), 1);
8057
+ var import_yaml3 = __toESM(require_dist(), 1);
7923
8058
  var ANSWERS_FILE = ".copier-answers.yml";
7924
8059
  var TEMPLATE_FILES = ["copier.yml", "copier.yaml"];
7925
8060
  function parseAnswers(text) {
7926
- const data = (0, import_yaml2.parse)(text);
8061
+ const data = (0, import_yaml3.parse)(text);
7927
8062
  if (data === null || typeof data !== "object" || Array.isArray(data))
7928
8063
  throw new Error("answers file is not a mapping");
7929
8064
  const { _commit, _src_path, ...answers } = data;
@@ -8014,7 +8149,7 @@ function isNotable(dir) {
8014
8149
  }
8015
8150
 
8016
8151
  // ../lib/dist/overview.js
8017
- import { join as join2 } from "node:path";
8152
+ import { join as join3 } from "node:path";
8018
8153
  var PRUNED = /* @__PURE__ */ new Set([
8019
8154
  "node_modules",
8020
8155
  ".hg",
@@ -8062,7 +8197,7 @@ async function walk(ctx, path3, name, options) {
8062
8197
  }
8063
8198
  const subdirs = entries.filter((e) => e.directory && e.name !== ".git" && !PRUNED.has(e.name)).map((e) => e.name).sort();
8064
8199
  for (const sub of subdirs) {
8065
- const child = await walk(ctx, join2(path3, sub), sub, options);
8200
+ const child = await walk(ctx, join3(path3, sub), sub, options);
8066
8201
  if (isNotable(child) || child.children.length > 0)
8067
8202
  dir.children.push(child);
8068
8203
  }
@@ -8070,7 +8205,7 @@ async function walk(ctx, path3, name, options) {
8070
8205
  }
8071
8206
  async function facet(ctx, dir, file, read) {
8072
8207
  try {
8073
- read(await ctx.readFile(join2(dir.path, file)));
8208
+ read(await ctx.readFile(join3(dir.path, file)));
8074
8209
  } catch (error) {
8075
8210
  dir.problems ??= [];
8076
8211
  dir.problems.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
@@ -8109,10 +8244,10 @@ async function templateFacet(ctx, cwd) {
8109
8244
  }
8110
8245
 
8111
8246
  // ../lib/dist/propagate.js
8112
- import { join as join4, relative as relative3 } from "node:path";
8247
+ import { join as join5, relative as relative3 } from "node:path";
8113
8248
 
8114
8249
  // ../lib/dist/train.js
8115
- import { join as join3 } from "node:path";
8250
+ import { join as join4 } from "node:path";
8116
8251
  var TRAIN_REPOS = [
8117
8252
  "platform",
8118
8253
  "ci-cd",
@@ -8134,7 +8269,7 @@ async function readTrain(ctx, upstream) {
8134
8269
  const repos = [];
8135
8270
  const all = /* @__PURE__ */ new Set();
8136
8271
  for (const name of TRAIN_REPOS) {
8137
- const path3 = join3(upstream, name);
8272
+ const path3 = join4(upstream, name);
8138
8273
  try {
8139
8274
  await ctx.read("git", ["rev-parse", "--git-dir"], { cwd: path3 });
8140
8275
  } catch {
@@ -8195,7 +8330,7 @@ var isClean = (s) => s.conflicted.length === 0 && s.failed.length === 0 && s.ski
8195
8330
  var PREVIEW_BASE = "ship-base";
8196
8331
  async function propagate(ctx, workspace, train) {
8197
8332
  const summary = { train, preview: ctx.dryRun, clean: [], conflicted: [], failed: [], skipped: [] };
8198
- const upstream = join4(workspace, UPSTREAM);
8333
+ const upstream = join5(workspace, UPSTREAM);
8199
8334
  const tree = await overview(ctx, workspace);
8200
8335
  const upstreamDir = tree.children.find((c) => c.path === upstream);
8201
8336
  const presets = TRAIN_REPOS.filter((r) => upstreamDir?.children.some((c) => c.name === r && c.template));
@@ -8212,7 +8347,7 @@ async function propagate(ctx, workspace, train) {
8212
8347
  const cloneOf = async (consumer) => {
8213
8348
  let clone = clones.get(consumer.path);
8214
8349
  if (!clone) {
8215
- clone = join4(scratch ?? "", consumer.rel);
8350
+ clone = join5(scratch ?? "", consumer.rel);
8216
8351
  await ctx.stage(run("git", ["clone", "--quiet", consumer.path, clone]));
8217
8352
  await ctx.stage(run("git", ["tag", PREVIEW_BASE], { cwd: clone }));
8218
8353
  clones.set(consumer.path, clone);
@@ -8239,7 +8374,7 @@ async function propagate(ctx, workspace, train) {
8239
8374
  if (sites.length === 0)
8240
8375
  ctx.say(`\u2022 no consumers of ${presetSource(preset)} found under ${workspace}`);
8241
8376
  for (const [consumer, site] of sites)
8242
- await updateSite(ctx, consumer, site, join4(upstream, preset), summary, cloneOf, previewCommit);
8377
+ await updateSite(ctx, consumer, site, join5(upstream, preset), summary, cloneOf, previewCommit);
8243
8378
  }
8244
8379
  await refreshLockfiles(ctx, active, summary, clones);
8245
8380
  await bumpConsumerRefs(ctx, consumers, train);
@@ -8256,9 +8391,9 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
8256
8391
  if (ctx.dryRun) {
8257
8392
  ctx.say(`\u2192 ${site.rel}: copier update (preview from ${localPreset}@main)`);
8258
8393
  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}`)));
8394
+ target = join5(repo, relative3(consumer.path, site.path));
8395
+ const answers = await ctx.readFile(join5(site.path, ANSWERS_FILE));
8396
+ await ctx.stage(write(join5(target, ANSWERS_FILE), answers.replace(/^_src_path: .*$/m, `_src_path: ${localPreset}`)));
8262
8397
  await previewCommit(repo, `plx preview: ${site.rel} reads ${localPreset} from the local checkout`);
8263
8398
  await ctx.stage(run("copier", ["update", "--skip-answered", "--trust", "--defaults", "--skip-tasks", "--vcs-ref", "main"], {
8264
8399
  cwd: target
@@ -8277,7 +8412,7 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
8277
8412
  let conflict = hasUnmergedEntries(status);
8278
8413
  const changed = (await ctx.read("git", ["diff", "--name-only"], { cwd: repo })).split("\n").filter(Boolean);
8279
8414
  for (const f of changed) {
8280
- const text = await ctx.readFile(join4(repo, f)).catch(() => "");
8415
+ const text = await ctx.readFile(join5(repo, f)).catch(() => "");
8281
8416
  if (hasConflictMarkers(text))
8282
8417
  conflict = true;
8283
8418
  }
@@ -8295,17 +8430,17 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
8295
8430
  await previewCommit(repo, `plx preview: ${site.rel} updated`);
8296
8431
  }
8297
8432
  async function syncSiteMarker(ctx, site, target) {
8298
- const marker = await ctx.readFile(join4(target, MARKER_FILE)).catch(() => void 0);
8433
+ const marker = await ctx.readFile(join5(target, MARKER_FILE)).catch(() => void 0);
8299
8434
  if (marker === void 0)
8300
8435
  return;
8301
- const { commit } = parseAnswers(await ctx.readFile(join4(target, ANSWERS_FILE)));
8436
+ const { commit } = parseAnswers(await ctx.readFile(join5(target, ANSWERS_FILE)));
8302
8437
  if (!commit)
8303
8438
  return;
8304
8439
  const synced = syncMarker(marker, commit);
8305
8440
  if (synced === marker)
8306
8441
  return;
8307
8442
  ctx.say(` marker: plexus \u2192 ${commit} in ${site.rel}/${MARKER_FILE}`);
8308
- const step = write(join4(target, MARKER_FILE), synced);
8443
+ const step = write(join5(target, MARKER_FILE), synced);
8309
8444
  if (ctx.dryRun)
8310
8445
  await ctx.stage(step);
8311
8446
  else
@@ -8314,7 +8449,7 @@ async function syncSiteMarker(ctx, site, target) {
8314
8449
  async function refreshLockfiles(ctx, consumers, summary, clones) {
8315
8450
  const manifests = ["*package.json", "pnpm-workspace.yaml"];
8316
8451
  for (const c of consumers) {
8317
- if (await ctx.readFile(join4(c.path, "pnpm-workspace.yaml")).catch(() => void 0) === void 0)
8452
+ if (await ctx.readFile(join5(c.path, "pnpm-workspace.yaml")).catch(() => void 0) === void 0)
8318
8453
  continue;
8319
8454
  const repo = ctx.dryRun ? clones.get(c.path) : c.path;
8320
8455
  if (!repo)
@@ -8334,7 +8469,7 @@ async function refreshLockfiles(ctx, consumers, summary, clones) {
8334
8469
  }
8335
8470
  async function bumpConsumerRefs(ctx, consumers, train) {
8336
8471
  for (const c of consumers) {
8337
- const dir = join4(c.path, ".github", "workflows");
8472
+ const dir = join5(c.path, ".github", "workflows");
8338
8473
  const entries = await ctx.readDir(dir).catch(() => []);
8339
8474
  const files = entries.filter((e) => !e.directory && /\.ya?ml$/.test(e.name)).map((e) => e.name).sort();
8340
8475
  if (files.length === 0)
@@ -8344,14 +8479,14 @@ async function bumpConsumerRefs(ctx, consumers, train) {
8344
8479
  continue;
8345
8480
  }
8346
8481
  for (const f of files) {
8347
- const text = await ctx.readFile(join4(dir, f));
8482
+ const text = await ctx.readFile(join5(dir, f));
8348
8483
  if (!text.includes(WORKFLOW_REF))
8349
8484
  continue;
8350
8485
  const bumped = bumpWorkflowRefs(text, train);
8351
8486
  if (bumped === text)
8352
8487
  continue;
8353
8488
  ctx.say(`\u2192 ${c.rel}: ${f} ci-cd refs \u2192 @${train}`);
8354
- await ctx.apply([write(join4(dir, f), bumped)]);
8489
+ await ctx.apply([write(join5(dir, f), bumped)]);
8355
8490
  }
8356
8491
  }
8357
8492
  }
@@ -8362,7 +8497,7 @@ var detail = (error) => {
8362
8497
  };
8363
8498
 
8364
8499
  // ../lib/dist/requirements.js
8365
- import { join as join5 } from "node:path";
8500
+ import { join as join6 } from "node:path";
8366
8501
  var KEYWORD = /MUST|SHOULD|MAY/;
8367
8502
  var hasKeyword = (line) => KEYWORD.test(line.replace(/MUSTs|SHOULDs|MAYs/g, ""));
8368
8503
  var list = (value) => value.replace(/^\s*\[/, "").replace(/\].*$/, "").split(",").map((s) => s.trim()).filter(Boolean);
@@ -8455,7 +8590,7 @@ var MAP_PAGE = "essentials/standard.md";
8455
8590
  async function modulePages(ctx, compendium) {
8456
8591
  const pages = [];
8457
8592
  for (const dir of Object.keys(CATALOG_DIRS)) {
8458
- const entries = await ctx.readDir(join5(compendium, dir));
8593
+ const entries = await ctx.readDir(join6(compendium, dir));
8459
8594
  for (const e of entries.filter((e2) => !e2.directory && e2.name.endsWith(".md") && e2.name !== "index.md").sort())
8460
8595
  pages.push(`${dir}/${e.name}`);
8461
8596
  }
@@ -8463,11 +8598,11 @@ async function modulePages(ctx, compendium) {
8463
8598
  }
8464
8599
  async function checkCompendium(ctx, compendium) {
8465
8600
  const defects = [];
8466
- defects.push(...scanPage(await ctx.readFile(join5(compendium, MAP_PAGE)), MAP_PAGE, "map").defects);
8601
+ defects.push(...scanPage(await ctx.readFile(join6(compendium, MAP_PAGE)), MAP_PAGE, "map").defects);
8467
8602
  const pages = await modulePages(ctx, compendium);
8468
8603
  const scans = /* @__PURE__ */ new Map();
8469
8604
  for (const page of pages) {
8470
- const scan = scanPage(await ctx.readFile(join5(compendium, page)), page, "module");
8605
+ const scan = scanPage(await ctx.readFile(join6(compendium, page)), page, "module");
8471
8606
  defects.push(...scan.defects);
8472
8607
  scans.set(page, scan);
8473
8608
  }
@@ -8514,9 +8649,9 @@ async function checkCompendium(ctx, compendium) {
8514
8649
  }
8515
8650
 
8516
8651
  // ../lib/dist/ship.js
8517
- import { join as join6 } from "node:path";
8652
+ import { join as join7 } from "node:path";
8518
8653
  async function ship(ctx, options) {
8519
- const upstream = join6(options.workspace, UPSTREAM);
8654
+ const upstream = join7(options.workspace, UPSTREAM);
8520
8655
  const propagateOnly = options.propagateOnly ?? false;
8521
8656
  const train = await readTrain(ctx, upstream);
8522
8657
  if (!propagateOnly)
@@ -8567,7 +8702,7 @@ async function planBookkeeping(ctx, train, next, today2) {
8567
8702
  return r;
8568
8703
  };
8569
8704
  const edit = async (r, rel, what, transform) => {
8570
- const path3 = join6(r.path, rel);
8705
+ const path3 = join7(r.path, rel);
8571
8706
  const text = await ctx.readFile(path3).catch(() => void 0);
8572
8707
  if (text === void 0)
8573
8708
  return;
@@ -8588,7 +8723,7 @@ async function planBookkeeping(ctx, train, next, today2) {
8588
8723
  const comp = repo("compendium");
8589
8724
  await edit(comp, MAP_PAGE, `version \u2192 ${next}, timestamp \u2192 ${today2}`, (t) => stampStandard(t, next, today2));
8590
8725
  for (const page of await modulePages(ctx, comp.path)) {
8591
- const text = await ctx.readFile(join6(comp.path, page));
8726
+ const text = await ctx.readFile(join7(comp.path, page));
8592
8727
  if (frontmatterField(text, "maturity") !== "standard")
8593
8728
  continue;
8594
8729
  const previous = comp.tag ? await ctx.read("git", ["show", `${comp.tag}:${page}`], { cwd: comp.path }).catch(() => "") : "";
@@ -9551,13 +9686,13 @@ import { AsyncResource as AsyncResource3 } from "node:async_hooks";
9551
9686
 
9552
9687
  // ../../node_modules/.pnpm/@inquirer+type@4.2.0_@types+node@26.6.5/node_modules/@inquirer/type/dist/utils.js
9553
9688
  function withResolver() {
9554
- let resolve6;
9689
+ let resolve7;
9555
9690
  let reject;
9556
9691
  const promise = new Promise((res, rej) => {
9557
- resolve6 = res;
9692
+ resolve7 = res;
9558
9693
  reject = rej;
9559
9694
  });
9560
- return { promise, resolve: resolve6, reject };
9695
+ return { promise, resolve: resolve7, reject };
9561
9696
  }
9562
9697
 
9563
9698
  // ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/create-prompt.js
@@ -9936,7 +10071,7 @@ function createPrompt(view) {
9936
10071
  });
9937
10072
  output.mute();
9938
10073
  const screen = new ScreenManager(rl);
9939
- const { promise, resolve: resolve6, reject } = withResolver();
10074
+ const { promise, resolve: resolve7, reject } = withResolver();
9940
10075
  return withHooks(rl, (cycle) => {
9941
10076
  const clearEffects = AsyncResource3.bind(() => effectScheduler.clearAll());
9942
10077
  const settlePrompt = (settle) => {
@@ -9947,7 +10082,7 @@ function createPrompt(view) {
9947
10082
  reject(error);
9948
10083
  }
9949
10084
  };
9950
- const resolvePrompt = (value) => settlePrompt(() => resolve6(value));
10085
+ const resolvePrompt = (value) => settlePrompt(() => resolve7(value));
9951
10086
  const rejectPrompt = (error) => settlePrompt(() => reject(error));
9952
10087
  const promptPromise = Object.assign(promise.finally(() => {
9953
10088
  cleanups.forEach((cleanup) => cleanup());
@@ -13466,9 +13601,25 @@ function useColor() {
13466
13601
  // ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/index.js
13467
13602
  var program = new Command();
13468
13603
 
13604
+ // src/commands/changeset.ts
13605
+ import { resolve } from "node:path";
13606
+ function register(program2) {
13607
+ 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) => {
13608
+ const ctx = contextOf2(command);
13609
+ const changeset = await readChangeset(ctx, resolve(dir), opts.base, opts.branch);
13610
+ await ctx.apply(changeset.plan);
13611
+ ctx.result(changeset, renderChangeset);
13612
+ });
13613
+ }
13614
+ function renderChangeset(c) {
13615
+ const releases = Object.entries(c.releases);
13616
+ if (releases.length === 0) return `\u2713 nothing owed; no ${c.path}`;
13617
+ return [`\u2713 ${c.path}`, ...releases.map(([name, bump]) => ` ${name}: ${bump}`)].join("\n");
13618
+ }
13619
+
13469
13620
  // src/commands/doctor.ts
13470
13621
  import { existsSync } from "node:fs";
13471
- import { join as join7, resolve } from "node:path";
13622
+ import { join as join8, resolve as resolve2 } from "node:path";
13472
13623
 
13473
13624
  // src/style.ts
13474
13625
  function style(enabled) {
@@ -13479,15 +13630,15 @@ var colorByDefault = () => process.stdout.isTTY === true && !process.env.NO_COLO
13479
13630
 
13480
13631
  // src/commands/doctor.ts
13481
13632
  var WORKSPACE_COMPENDIUM = "plexus-ms/compendium";
13482
- function register(program2) {
13633
+ function register2(program2) {
13483
13634
  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
13635
  "--compendium <path>",
13485
13636
  `compendium checkout to read the modules from (default: <dir>/${WORKSPACE_COMPENDIUM})`
13486
13637
  ).option("--color", "colour the report even when piped").option("--no-color", "never colour the report").action(async (dir, opts, command) => {
13487
13638
  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")))
13639
+ const root = resolve2(dir);
13640
+ const compendium = resolve2(opts.compendium ?? join8(root, WORKSPACE_COMPENDIUM));
13641
+ if (!existsSync(join8(compendium, "contracts")))
13491
13642
  throw new Error(
13492
13643
  `no compendium at ${compendium}; pass --compendium <path to a checkout of plexus-ms/compendium>`
13493
13644
  );
@@ -13515,11 +13666,11 @@ function renderReport(report, s, showCurrent) {
13515
13666
  }
13516
13667
 
13517
13668
  // src/commands/overview.ts
13518
- import { resolve as resolve2 } from "node:path";
13519
- function register2(program2) {
13669
+ import { resolve as resolve3 } from "node:path";
13670
+ function register3(program2) {
13520
13671
  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
13672
  const ctx = contextOf2(command);
13522
- const tree = await overview(ctx, resolve2(dir));
13673
+ const tree = await overview(ctx, resolve3(dir));
13523
13674
  const s = style(opts.color ?? colorByDefault());
13524
13675
  ctx.result(tree, (t) => renderTree(t, s, ctx.verbose >= 1));
13525
13676
  });
@@ -13562,14 +13713,14 @@ function annotate(dir, s, modules) {
13562
13713
  }
13563
13714
 
13564
13715
  // src/commands/propagate.ts
13565
- import { resolve as resolve4 } from "node:path";
13716
+ import { resolve as resolve5 } from "node:path";
13566
13717
 
13567
13718
  // src/commands/ship.ts
13568
- import { resolve as resolve3 } from "node:path";
13569
- function register3(program2) {
13719
+ import { resolve as resolve4 } from "node:path";
13720
+ function register4(program2) {
13570
13721
  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
13722
  const ctx = contextOf2(command);
13572
- const result = await ship(ctx, { workspace: resolve3(dir), train: opts.train });
13723
+ const result = await ship(ctx, { workspace: resolve4(dir), train: opts.train });
13573
13724
  ctx.result(result, (r) => renderSummary(r, `plx -y ship --train ${r.train} ${dir}`));
13574
13725
  if (!isClean(result)) process.exitCode = 1;
13575
13726
  });
@@ -13592,23 +13743,23 @@ function renderSummary(s, realCommand) {
13592
13743
  }
13593
13744
 
13594
13745
  // src/commands/propagate.ts
13595
- function register4(program2) {
13746
+ function register5(program2) {
13596
13747
  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
13748
  const ctx = contextOf2(command);
13598
- const result = await ship(ctx, { workspace: resolve4(dir), train: opts.train, propagateOnly: true });
13749
+ const result = await ship(ctx, { workspace: resolve5(dir), train: opts.train, propagateOnly: true });
13599
13750
  ctx.result(result, (r) => renderSummary(r, `plx -y propagate --train ${r.train} ${dir}`));
13600
13751
  if (!isClean(result)) process.exitCode = 1;
13601
13752
  });
13602
13753
  }
13603
13754
 
13604
13755
  // src/commands/requirements.ts
13605
- import { join as join8, resolve as resolve5 } from "node:path";
13606
- function register5(program2) {
13756
+ import { join as join9, resolve as resolve6 } from "node:path";
13757
+ function register6(program2) {
13607
13758
  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
13759
  const ctx = contextOf2(command);
13609
- const compendium = resolve5(dir);
13760
+ const compendium = resolve6(dir);
13610
13761
  if (opts.extract) {
13611
- ctx.result(extractRequirements(await ctx.readFile(join8(compendium, opts.extract))));
13762
+ ctx.result(extractRequirements(await ctx.readFile(join9(compendium, opts.extract))));
13612
13763
  return;
13613
13764
  }
13614
13765
  const check = await checkCompendium(ctx, compendium);
@@ -13622,7 +13773,7 @@ function renderCheck(c) {
13622
13773
  }
13623
13774
 
13624
13775
  // src/commands/index.ts
13625
- var commands = [register2, register, register3, register4, register5];
13776
+ var commands = [register3, register2, register4, register5, register6, register];
13626
13777
 
13627
13778
  // src/program.ts
13628
13779
  var { version, description } = createRequire(import.meta.url)("../package.json");
@@ -13631,7 +13782,7 @@ function buildProgram() {
13631
13782
  program2.addOption(new Option("-n, --dry-run", "print every effect this run would have, perform none").env("PLX_DRY_RUN")).addOption(
13632
13783
  new Option("-v, --verbose", "say more: once for reads and inputs, twice for command output").argParser((_value, previous) => previous + 1).default(0)
13633
13784
  ).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);
13785
+ for (const register7 of commands) register7(program2);
13635
13786
  return program2;
13636
13787
  }
13637
13788
  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.4.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",
@@ -22,13 +22,15 @@
22
22
  "engines": {
23
23
  "node": ">=24"
24
24
  },
25
+ "dependencies": {
26
+ "@inquirer/prompts": "^8.7.4",
27
+ "@plexus-ms/lib": "0.4.0",
28
+ "commander": "^15.0.0"
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",