@plexus-ms/cli 0.4.0 → 0.6.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.
- package/dist/plx.js +497 -150
- package/package.json +3 -3
package/dist/plx.js
CHANGED
|
@@ -7268,7 +7268,7 @@ var require_public_api = __commonJS({
|
|
|
7268
7268
|
}
|
|
7269
7269
|
return doc;
|
|
7270
7270
|
}
|
|
7271
|
-
function
|
|
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 =
|
|
7312
|
+
exports.parse = parse5;
|
|
7313
7313
|
exports.parseAllDocuments = parseAllDocuments;
|
|
7314
7314
|
exports.parseDocument = parseDocument;
|
|
7315
7315
|
exports.stringify = stringify;
|
|
@@ -7655,9 +7655,83 @@ async function readCatalog(ctx, compendium) {
|
|
|
7655
7655
|
return catalog;
|
|
7656
7656
|
}
|
|
7657
7657
|
|
|
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
|
+
};
|
|
7681
|
+
}
|
|
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);
|
|
7694
|
+
};
|
|
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)
|
|
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
|
+
}
|
|
7719
|
+
}
|
|
7720
|
+
}
|
|
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
|
+
}
|
|
7730
|
+
}
|
|
7731
|
+
|
|
7658
7732
|
// ../lib/dist/changeset.js
|
|
7659
7733
|
var import_yaml2 = __toESM(require_dist(), 1);
|
|
7660
|
-
import { join as
|
|
7734
|
+
import { join as join3 } from "node:path";
|
|
7661
7735
|
|
|
7662
7736
|
// ../lib/dist/io.js
|
|
7663
7737
|
import { spawn } from "node:child_process";
|
|
@@ -7680,7 +7754,7 @@ function realIo(prompt) {
|
|
|
7680
7754
|
stdout: process.stdout,
|
|
7681
7755
|
stderr: process.stderr,
|
|
7682
7756
|
prompt: prompt && isInteractive() ? prompt : void 0,
|
|
7683
|
-
exec: (command, args, options = {}) => new Promise((
|
|
7757
|
+
exec: (command, args, options = {}) => new Promise((resolve10, reject) => {
|
|
7684
7758
|
const child = spawn(command, args, {
|
|
7685
7759
|
cwd: options.cwd,
|
|
7686
7760
|
env: options.env,
|
|
@@ -7703,7 +7777,7 @@ function realIo(prompt) {
|
|
|
7703
7777
|
child.on("error", reject);
|
|
7704
7778
|
child.on("close", (code) => {
|
|
7705
7779
|
if (code === 0)
|
|
7706
|
-
|
|
7780
|
+
resolve10(stdout);
|
|
7707
7781
|
else
|
|
7708
7782
|
reject(new ExecError(render(command, args), code, stderr));
|
|
7709
7783
|
});
|
|
@@ -7734,8 +7808,6 @@ function describe(step) {
|
|
|
7734
7808
|
switch (step.kind) {
|
|
7735
7809
|
case "run":
|
|
7736
7810
|
return `$ ${step.cwd ? `(cd ${step.cwd} && ` : ""}${render(step.command, step.args)}${step.cwd ? ")" : ""}`;
|
|
7737
|
-
case "ssh":
|
|
7738
|
-
return `$ ${render("ssh", [step.host, "bash", "-s"])} <<'EOF'`;
|
|
7739
7811
|
case "write":
|
|
7740
7812
|
return `> ${step.path} (${Buffer.byteLength(step.content)} bytes)`;
|
|
7741
7813
|
}
|
|
@@ -7744,9 +7816,6 @@ function body(step) {
|
|
|
7744
7816
|
switch (step.kind) {
|
|
7745
7817
|
case "run":
|
|
7746
7818
|
return step.input;
|
|
7747
|
-
case "ssh":
|
|
7748
|
-
return `${step.script.trimEnd()}
|
|
7749
|
-
EOF`;
|
|
7750
7819
|
case "write":
|
|
7751
7820
|
return step.content;
|
|
7752
7821
|
}
|
|
@@ -7794,7 +7863,7 @@ ${header.join("\n")}
|
|
|
7794
7863
|
|
|
7795
7864
|
${lines.join("\n")}
|
|
7796
7865
|
`;
|
|
7797
|
-
return { path: path3, releases, changes, content, plan: [write(root ?
|
|
7866
|
+
return { path: path3, releases, changes, content, plan: [write(root ? join3(root, path3) : path3, content)] };
|
|
7798
7867
|
}
|
|
7799
7868
|
function describeChange(c) {
|
|
7800
7869
|
const peer = c.field === "peerDependencies" ? " (peer)" : "";
|
|
@@ -7843,7 +7912,7 @@ async function readChangeset(ctx, dir, base, branch) {
|
|
|
7843
7912
|
};
|
|
7844
7913
|
const current = async (path4) => {
|
|
7845
7914
|
try {
|
|
7846
|
-
return await ctx.readFile(
|
|
7915
|
+
return await ctx.readFile(join3(dir, path4));
|
|
7847
7916
|
} catch {
|
|
7848
7917
|
return void 0;
|
|
7849
7918
|
}
|
|
@@ -7859,7 +7928,7 @@ async function readChangeset(ctx, dir, base, branch) {
|
|
|
7859
7928
|
}
|
|
7860
7929
|
const config = await current(`${CHANGESET_DIR}/config.json`);
|
|
7861
7930
|
const ignore = config ? JSON.parse(config).ignore ?? [] : [];
|
|
7862
|
-
const entries = await ctx.readDir(
|
|
7931
|
+
const entries = await ctx.readDir(join3(dir, CHANGESET_DIR)).catch(() => []);
|
|
7863
7932
|
const path3 = changesetPath(head);
|
|
7864
7933
|
return planChangeset({
|
|
7865
7934
|
branch: head,
|
|
@@ -7879,7 +7948,7 @@ async function expand(ctx, root, glob) {
|
|
|
7879
7948
|
if (star < 0)
|
|
7880
7949
|
return [glob.replace(/\/$/, "")];
|
|
7881
7950
|
const parent = glob.slice(0, star).replace(/\/$/, "");
|
|
7882
|
-
const entries = await ctx.readDir(
|
|
7951
|
+
const entries = await ctx.readDir(join3(root, parent)).catch(() => []);
|
|
7883
7952
|
return entries.filter((e) => e.directory && !e.name.startsWith(".")).map((e) => parent ? `${parent}/${e.name}` : e.name).sort();
|
|
7884
7953
|
}
|
|
7885
7954
|
|
|
@@ -7975,6 +8044,8 @@ var Context = class {
|
|
|
7975
8044
|
*/
|
|
7976
8045
|
result(data, human = renderHuman) {
|
|
7977
8046
|
const text = this.json ? JSON.stringify(data, null, 2) : human(data);
|
|
8047
|
+
if (text === "")
|
|
8048
|
+
return;
|
|
7978
8049
|
this.io.stdout.write(text.endsWith("\n") ? text : `${text}
|
|
7979
8050
|
`);
|
|
7980
8051
|
}
|
|
@@ -8034,8 +8105,6 @@ var Context = class {
|
|
|
8034
8105
|
switch (step.kind) {
|
|
8035
8106
|
case "run":
|
|
8036
8107
|
return this.io.exec(step.command, step.args, { cwd: step.cwd, input: step.input, inherit });
|
|
8037
|
-
case "ssh":
|
|
8038
|
-
return this.io.exec("ssh", [step.host, "bash", "-s"], { input: step.script, inherit });
|
|
8039
8108
|
case "write":
|
|
8040
8109
|
await this.io.writeFile(step.path, step.content);
|
|
8041
8110
|
return void 0;
|
|
@@ -8069,6 +8138,14 @@ function parseAnswers(text) {
|
|
|
8069
8138
|
};
|
|
8070
8139
|
}
|
|
8071
8140
|
|
|
8141
|
+
// ../lib/dist/deploy.js
|
|
8142
|
+
import { randomBytes } from "node:crypto";
|
|
8143
|
+
import { tmpdir } from "node:os";
|
|
8144
|
+
import { join as join5 } from "node:path";
|
|
8145
|
+
|
|
8146
|
+
// ../lib/dist/train.js
|
|
8147
|
+
import { join as join4 } from "node:path";
|
|
8148
|
+
|
|
8072
8149
|
// ../lib/dist/doctor.js
|
|
8073
8150
|
import { relative as relative2 } from "node:path";
|
|
8074
8151
|
function diagnose(root, catalog) {
|
|
@@ -8128,6 +8205,297 @@ function compareTrains(a, b) {
|
|
|
8128
8205
|
return 0;
|
|
8129
8206
|
}
|
|
8130
8207
|
|
|
8208
|
+
// ../lib/dist/train.js
|
|
8209
|
+
var TRAIN_REPOS = [
|
|
8210
|
+
"platform",
|
|
8211
|
+
"ci-cd",
|
|
8212
|
+
"preset-repo-web",
|
|
8213
|
+
"preset-app-nextjs",
|
|
8214
|
+
"preset-repo-platform",
|
|
8215
|
+
"compendium"
|
|
8216
|
+
];
|
|
8217
|
+
var UPSTREAM = "plexus-ms";
|
|
8218
|
+
var isTrainVersion = (v) => /^\d+\.\d+\.\d+$/.test(v);
|
|
8219
|
+
function nextMinor(version2) {
|
|
8220
|
+
const [major = "0", minor = "0"] = version2.split(".");
|
|
8221
|
+
return `${major}.${Number(minor) + 1}.0`;
|
|
8222
|
+
}
|
|
8223
|
+
function latestTag(tags) {
|
|
8224
|
+
return tags.filter((t) => /^v?\d/.test(t)).sort((a, b) => compareTrains(a.replace(/^v/, ""), b.replace(/^v/, ""))).at(-1);
|
|
8225
|
+
}
|
|
8226
|
+
async function readTrain(ctx, upstream) {
|
|
8227
|
+
const repos = [];
|
|
8228
|
+
const all = /* @__PURE__ */ new Set();
|
|
8229
|
+
for (const name of TRAIN_REPOS) {
|
|
8230
|
+
const path3 = join4(upstream, name);
|
|
8231
|
+
try {
|
|
8232
|
+
await ctx.read("git", ["rev-parse", "--git-dir"], { cwd: path3 });
|
|
8233
|
+
} catch {
|
|
8234
|
+
throw new Error(`${path3} is not a git repo`);
|
|
8235
|
+
}
|
|
8236
|
+
try {
|
|
8237
|
+
await ctx.read("git", ["fetch", "origin", "--tags", "--quiet"], { cwd: path3 });
|
|
8238
|
+
} catch {
|
|
8239
|
+
throw new Error(`${name}: fetch from origin failed`);
|
|
8240
|
+
}
|
|
8241
|
+
const tags = (await ctx.read("git", ["tag", "--list", "v*", "[0-9]*"], { cwd: path3 })).split("\n").filter(Boolean);
|
|
8242
|
+
for (const t of tags)
|
|
8243
|
+
all.add(t.replace(/^v/, ""));
|
|
8244
|
+
const tag = latestTag(tags);
|
|
8245
|
+
const range = tag ? `${tag}..HEAD` : "HEAD";
|
|
8246
|
+
const pending = (await ctx.read("git", ["log", "--oneline", range], { cwd: path3 })).split("\n").filter(Boolean);
|
|
8247
|
+
repos.push({ name, path: path3, tag, latest: tag?.replace(/^v/, ""), pending });
|
|
8248
|
+
}
|
|
8249
|
+
const latest = repos.map((r) => r.latest);
|
|
8250
|
+
return { repos, last: latestTag([...all]), skew: latest.some((l) => l !== latest[0]) };
|
|
8251
|
+
}
|
|
8252
|
+
async function preflightRelease(ctx, train) {
|
|
8253
|
+
for (const { name, path: path3 } of train.repos) {
|
|
8254
|
+
const branch = (await ctx.read("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path3 })).trim();
|
|
8255
|
+
if (branch !== "main")
|
|
8256
|
+
throw new Error(`${name} is on '${branch}', trains cut from main`);
|
|
8257
|
+
if ((await ctx.read("git", ["status", "--porcelain"], { cwd: path3 })).trim() !== "")
|
|
8258
|
+
throw new Error(`${name} has uncommitted changes \u2014 commit first`);
|
|
8259
|
+
const behind = (await ctx.read("git", ["rev-list", "--count", "main..origin/main"], { cwd: path3 })).trim();
|
|
8260
|
+
if (behind !== "0")
|
|
8261
|
+
throw new Error(`${name} is ${behind} commit(s) behind origin/main \u2014 pull first, or the tag lands on a stale commit`);
|
|
8262
|
+
}
|
|
8263
|
+
}
|
|
8264
|
+
function resolveVersion(train, explicit, propagateOnly) {
|
|
8265
|
+
if (explicit !== void 0 && !isTrainVersion(explicit))
|
|
8266
|
+
throw new UsageError(`'${explicit}' is not plain semver (X.Y.Z, no v)`);
|
|
8267
|
+
if (propagateOnly) {
|
|
8268
|
+
const v = explicit ?? train.last;
|
|
8269
|
+
if (!v)
|
|
8270
|
+
throw new Error("no train version found to propagate");
|
|
8271
|
+
return v;
|
|
8272
|
+
}
|
|
8273
|
+
if (explicit)
|
|
8274
|
+
return explicit;
|
|
8275
|
+
if (train.skew) {
|
|
8276
|
+
const report = train.repos.map((r) => ` ${r.name}: ${r.latest ?? "none"}`).join("\n");
|
|
8277
|
+
throw new Error(`the train repos' latest tags disagree:
|
|
8278
|
+
${report}
|
|
8279
|
+
pass the version explicitly (--train ${train.last}) to finish the half-tagged train, or a higher one to start a new one`);
|
|
8280
|
+
}
|
|
8281
|
+
return train.last ? nextMinor(train.last) : "0.1.0";
|
|
8282
|
+
}
|
|
8283
|
+
var UsageError = class extends Error {
|
|
8284
|
+
};
|
|
8285
|
+
|
|
8286
|
+
// ../lib/dist/deploy.js
|
|
8287
|
+
var PLACEHOLDER = /\$\{PLEXUS_DEPLOYMENT_IMAGE(:[^}]*)?\}/g;
|
|
8288
|
+
var INCOMING = ".incoming";
|
|
8289
|
+
function renderCompose(text, image, path3) {
|
|
8290
|
+
const has = PLACEHOLDER.test(text);
|
|
8291
|
+
PLACEHOLDER.lastIndex = 0;
|
|
8292
|
+
if (image) {
|
|
8293
|
+
if (!has)
|
|
8294
|
+
throw new UsageError(`${path3} does not reference \${PLEXUS_DEPLOYMENT_IMAGE}; an image ref was given but nothing would use it (\xA7 3 PLX-App)`);
|
|
8295
|
+
return text.replace(PLACEHOLDER, image);
|
|
8296
|
+
}
|
|
8297
|
+
if (has)
|
|
8298
|
+
throw new UsageError(`${path3} references \${PLEXUS_DEPLOYMENT_IMAGE} but no image ref was given`);
|
|
8299
|
+
return text;
|
|
8300
|
+
}
|
|
8301
|
+
var q = (word) => `'${word.replaceAll("'", "'\\''")}'`;
|
|
8302
|
+
var Remote = class {
|
|
8303
|
+
socket = join5(tmpdir(), `plx-deploy-${randomBytes(4).toString("hex")}.sock`);
|
|
8304
|
+
ctx;
|
|
8305
|
+
host;
|
|
8306
|
+
constructor(ctx, host) {
|
|
8307
|
+
this.ctx = ctx;
|
|
8308
|
+
this.host = host;
|
|
8309
|
+
}
|
|
8310
|
+
args(command) {
|
|
8311
|
+
return [
|
|
8312
|
+
"-o",
|
|
8313
|
+
"ControlMaster=auto",
|
|
8314
|
+
"-o",
|
|
8315
|
+
`ControlPath=${this.socket}`,
|
|
8316
|
+
"-o",
|
|
8317
|
+
"ControlPersist=60",
|
|
8318
|
+
"-o",
|
|
8319
|
+
"StrictHostKeyChecking=accept-new",
|
|
8320
|
+
this.host,
|
|
8321
|
+
"--",
|
|
8322
|
+
command
|
|
8323
|
+
];
|
|
8324
|
+
}
|
|
8325
|
+
/** An effect on the host: narrated, performed unless --dry-run. */
|
|
8326
|
+
exec(command, input) {
|
|
8327
|
+
return this.ctx.apply([run("ssh", this.args(command), input === void 0 ? {} : { input })]);
|
|
8328
|
+
}
|
|
8329
|
+
/**
|
|
8330
|
+
* A read from the host: its output, or undefined under --dry-run, where no
|
|
8331
|
+
* host is reached. A read that is a step of the procedure (`show`) is
|
|
8332
|
+
* printed by the dry run like an effect, so the operator copies it too.
|
|
8333
|
+
*/
|
|
8334
|
+
async read(command, show = false) {
|
|
8335
|
+
if (this.ctx.dryRun) {
|
|
8336
|
+
this.ctx.say(`${show ? "[dry-run] $" : "?"} ssh ${this.host} -- ${q(command)}`, show ? 0 : 1);
|
|
8337
|
+
return void 0;
|
|
8338
|
+
}
|
|
8339
|
+
return (await this.ctx.read("ssh", this.args(command))).trimEnd();
|
|
8340
|
+
}
|
|
8341
|
+
/** A check on the host: whether the command succeeds, or undefined under --dry-run. */
|
|
8342
|
+
async test(command, show = false) {
|
|
8343
|
+
try {
|
|
8344
|
+
return await this.read(command, show) === void 0 ? void 0 : true;
|
|
8345
|
+
} catch (error) {
|
|
8346
|
+
if (error instanceof ExecError)
|
|
8347
|
+
return false;
|
|
8348
|
+
throw error;
|
|
8349
|
+
}
|
|
8350
|
+
}
|
|
8351
|
+
/** Close the shared connection; nothing to close when no call was made. */
|
|
8352
|
+
close() {
|
|
8353
|
+
return this.ctx.apply([run("ssh", ["-o", `ControlPath=${this.socket}`, "-O", "exit", this.host])]);
|
|
8354
|
+
}
|
|
8355
|
+
};
|
|
8356
|
+
async function deploy(ctx, options) {
|
|
8357
|
+
const { host, tenant, app } = options;
|
|
8358
|
+
const dir = options.dir ?? ".";
|
|
8359
|
+
const image = options.image || void 0;
|
|
8360
|
+
const appDir = options.appDir || `/opt/stacks/${tenant}/${app}`;
|
|
8361
|
+
const source = options.source || `apps/${app}`;
|
|
8362
|
+
const retries = options.retries ?? 30;
|
|
8363
|
+
let compose;
|
|
8364
|
+
let text = "";
|
|
8365
|
+
for (const candidate of [`${source}/docker/compose.yaml`, `${source}/compose.yaml`]) {
|
|
8366
|
+
try {
|
|
8367
|
+
text = await ctx.readFile(join5(dir, candidate));
|
|
8368
|
+
compose = candidate;
|
|
8369
|
+
break;
|
|
8370
|
+
} catch {
|
|
8371
|
+
}
|
|
8372
|
+
}
|
|
8373
|
+
if (compose === void 0)
|
|
8374
|
+
throw new UsageError(`no compose.yaml under ${source}; run from the app's source repo (\xA7 2 PLX-App)`);
|
|
8375
|
+
const rendered = renderCompose(text, image, compose);
|
|
8376
|
+
ctx.say(`\u2192 deploying ${image ?? "<image pinned in compose.yaml>"}`);
|
|
8377
|
+
ctx.say(` host: ${host}`);
|
|
8378
|
+
ctx.say(` dir: ${appDir}`);
|
|
8379
|
+
ctx.say(` compose: ${compose}`);
|
|
8380
|
+
ctx.say(` health: ${options.healthUrl || "<resolved on host>"}`);
|
|
8381
|
+
const remote = new Remote(ctx, host);
|
|
8382
|
+
const D = q(appDir);
|
|
8383
|
+
const dc = `cd ${D} && docker compose`;
|
|
8384
|
+
const dci = `${dc} --project-directory . -f ${INCOMING}/compose.yaml`;
|
|
8385
|
+
const liveImage = `${dc} ps -q web 2>/dev/null | head -1 | xargs -r docker inspect --format '{{.Config.Image}}' 2>/dev/null || true`;
|
|
8386
|
+
const result = { host, appDir, compose, image, healthy: false, rolledBack: false };
|
|
8387
|
+
let staged = false;
|
|
8388
|
+
try {
|
|
8389
|
+
await remote.exec(`rm -rf ${D}/${INCOMING} && mkdir -p ${D}/${INCOMING} && cat > ${D}/${INCOMING}/compose.yaml`, rendered);
|
|
8390
|
+
staged = true;
|
|
8391
|
+
result.previous = await remote.read(liveImage) || void 0;
|
|
8392
|
+
ctx.say(` previous: ${result.previous ?? (ctx.dryRun ? "<read on host>" : "<none>")}`);
|
|
8393
|
+
await remote.exec(`${dci} pull`);
|
|
8394
|
+
const migrate = await remote.test(`${dci} --profile migrate config --services 2>/dev/null | grep -qx migrate`);
|
|
8395
|
+
if (migrate === void 0)
|
|
8396
|
+
ctx.say(" (if compose.yaml declares a migrate service:)");
|
|
8397
|
+
if (migrate !== false)
|
|
8398
|
+
await remote.exec(`${dci} run --rm migrate </dev/null`);
|
|
8399
|
+
const hadPrev = await remote.test(`test -f ${D}/compose.yaml`);
|
|
8400
|
+
if (hadPrev !== false)
|
|
8401
|
+
await remote.exec(`cd ${D} && cp -p compose.yaml ${INCOMING}/compose.yaml.prev`);
|
|
8402
|
+
await remote.exec(`cd ${D} && mv ${INCOMING}/compose.yaml compose.yaml`);
|
|
8403
|
+
await remote.exec(`${dc} up -d`);
|
|
8404
|
+
const url = options.healthUrl || await resolveHealthUrl(remote, D, dc, ctx);
|
|
8405
|
+
const poll = `cd ${D} && for _ in $(seq 1 ${retries}); do curl -fsS --max-time 5 -o /dev/null ${q(url)} && exit 0; sleep 2; done; exit 1`;
|
|
8406
|
+
const started = Date.now();
|
|
8407
|
+
const healthy = await remote.test(poll, true);
|
|
8408
|
+
if (healthy !== false) {
|
|
8409
|
+
result.live = await remote.read(liveImage) || image;
|
|
8410
|
+
result.healthy = true;
|
|
8411
|
+
if (ctx.dryRun)
|
|
8412
|
+
ctx.say(" (on failure: restore the previous compose.yaml, up -d, poll again)");
|
|
8413
|
+
else
|
|
8414
|
+
ctx.done(`${result.live ?? "the app"} is live and healthy`);
|
|
8415
|
+
return result;
|
|
8416
|
+
}
|
|
8417
|
+
ctx.say(`\u2717 healthcheck failed after ${Math.round((Date.now() - started) / 1e3)}s; last lines from the app:`);
|
|
8418
|
+
const logs = await remote.read(`${dc} logs --no-color --tail 50 web 2>&1 || true`);
|
|
8419
|
+
if (logs)
|
|
8420
|
+
ctx.say(logs.replace(/^/gm, " "));
|
|
8421
|
+
const differs = hadPrev && await remote.test(`cd ${D} && ! cmp -s compose.yaml ${INCOMING}/compose.yaml.prev`);
|
|
8422
|
+
if (!differs) {
|
|
8423
|
+
ctx.done("nothing to roll back to (first deploy, or the same run shape re-deployed); manual intervention needed", false);
|
|
8424
|
+
return result;
|
|
8425
|
+
}
|
|
8426
|
+
await remote.exec(`cd ${D} && cp -p ${INCOMING}/compose.yaml.prev compose.yaml`);
|
|
8427
|
+
ctx.say(`\u21A9 rolling back to ${result.previous ?? "the previous compose.yaml"}`);
|
|
8428
|
+
await remote.exec(`${dc} up -d`);
|
|
8429
|
+
result.rolledBack = true;
|
|
8430
|
+
if (await remote.test(poll)) {
|
|
8431
|
+
result.live = result.previous;
|
|
8432
|
+
ctx.done(`rolled back to ${result.previous ?? "the previous compose.yaml"}`);
|
|
8433
|
+
} else {
|
|
8434
|
+
ctx.done("rollback also unhealthy; manual intervention needed", false);
|
|
8435
|
+
}
|
|
8436
|
+
return result;
|
|
8437
|
+
} finally {
|
|
8438
|
+
if (staged)
|
|
8439
|
+
await remote.exec(`rm -rf ${D}/${INCOMING}`).catch(() => void 0);
|
|
8440
|
+
await remote.close().catch(() => void 0);
|
|
8441
|
+
}
|
|
8442
|
+
}
|
|
8443
|
+
async function resolveHealthUrl(remote, D, dc, ctx) {
|
|
8444
|
+
const port = await remote.read(`sed -n 's/^PLEXUS_LOOPBACK_PORT=//p' ${D}/.env 2>/dev/null | tail -1`);
|
|
8445
|
+
const path3 = await remote.read(`${dc} ps -q web 2>/dev/null | head -1 | xargs -r docker inspect --format '{{ index .Config.Labels "plexus.healthz" }}' 2>/dev/null || true`);
|
|
8446
|
+
const url = ctx.dryRun ? "http://127.0.0.1:${PLEXUS_LOOPBACK_PORT:-3000}${plexus.healthz:-/healthz}" : `http://127.0.0.1:${port || "3000"}${path3 || "/healthz"}`;
|
|
8447
|
+
ctx.say(` health: ${url}`);
|
|
8448
|
+
return url;
|
|
8449
|
+
}
|
|
8450
|
+
|
|
8451
|
+
// ../lib/dist/deployTargets.js
|
|
8452
|
+
var import_yaml4 = __toESM(require_dist(), 1);
|
|
8453
|
+
import { join as join6 } from "node:path";
|
|
8454
|
+
var VARS_DIRS = ["group_vars", "host_vars"];
|
|
8455
|
+
function parseInventoryFile(path3, text) {
|
|
8456
|
+
const data = (0, import_yaml4.parse)(text);
|
|
8457
|
+
const vars = data !== null && typeof data === "object" ? data : {};
|
|
8458
|
+
const target = typeof vars.plexus_deploy_target === "string" ? vars.plexus_deploy_target : void 0;
|
|
8459
|
+
const records = Array.isArray(vars.plexus_apps) ? vars.plexus_apps : [];
|
|
8460
|
+
const apps = records.map((r) => r !== null && typeof r === "object" ? r.name : void 0).filter((n) => typeof n === "string");
|
|
8461
|
+
return { path: path3, target, apps };
|
|
8462
|
+
}
|
|
8463
|
+
function selectDeployTargets(files) {
|
|
8464
|
+
const targets = {};
|
|
8465
|
+
const declaredIn = {};
|
|
8466
|
+
for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
|
|
8467
|
+
if (file.target === void 0)
|
|
8468
|
+
continue;
|
|
8469
|
+
for (const app of file.apps) {
|
|
8470
|
+
if (app in targets)
|
|
8471
|
+
throw new Error(`app "${app}" is declared with a deploy target in both ${declaredIn[app]} and ${file.path}`);
|
|
8472
|
+
targets[app] = file.target;
|
|
8473
|
+
declaredIn[app] = file.path;
|
|
8474
|
+
}
|
|
8475
|
+
}
|
|
8476
|
+
return Object.fromEntries(Object.entries(targets).sort(([a], [b]) => a.localeCompare(b)));
|
|
8477
|
+
}
|
|
8478
|
+
async function readDeployTargets(ctx, inventory) {
|
|
8479
|
+
const files = [];
|
|
8480
|
+
let found = false;
|
|
8481
|
+
for (const sub of VARS_DIRS) {
|
|
8482
|
+
const dir = join6(inventory, sub);
|
|
8483
|
+
const entries = await ctx.readDir(dir).catch(() => void 0);
|
|
8484
|
+
if (entries === void 0)
|
|
8485
|
+
continue;
|
|
8486
|
+
found = true;
|
|
8487
|
+
for (const e of entries) {
|
|
8488
|
+
if (e.directory || !/\.ya?ml$/.test(e.name))
|
|
8489
|
+
continue;
|
|
8490
|
+
const path3 = join6(dir, e.name);
|
|
8491
|
+
files.push(parseInventoryFile(path3, await ctx.readFile(path3)));
|
|
8492
|
+
}
|
|
8493
|
+
}
|
|
8494
|
+
if (!found && await ctx.readDir(inventory).catch(() => void 0) === void 0)
|
|
8495
|
+
throw new UsageError(`no inventory directory at ${inventory}`);
|
|
8496
|
+
return selectDeployTargets(files);
|
|
8497
|
+
}
|
|
8498
|
+
|
|
8131
8499
|
// ../lib/dist/model.js
|
|
8132
8500
|
function kindOf(declaration) {
|
|
8133
8501
|
const has = (code) => declaration.modules.includes(code);
|
|
@@ -8149,7 +8517,7 @@ function isNotable(dir) {
|
|
|
8149
8517
|
}
|
|
8150
8518
|
|
|
8151
8519
|
// ../lib/dist/overview.js
|
|
8152
|
-
import { join as
|
|
8520
|
+
import { join as join7 } from "node:path";
|
|
8153
8521
|
var PRUNED = /* @__PURE__ */ new Set([
|
|
8154
8522
|
"node_modules",
|
|
8155
8523
|
".hg",
|
|
@@ -8197,7 +8565,7 @@ async function walk(ctx, path3, name, options) {
|
|
|
8197
8565
|
}
|
|
8198
8566
|
const subdirs = entries.filter((e) => e.directory && e.name !== ".git" && !PRUNED.has(e.name)).map((e) => e.name).sort();
|
|
8199
8567
|
for (const sub of subdirs) {
|
|
8200
|
-
const child = await walk(ctx,
|
|
8568
|
+
const child = await walk(ctx, join7(path3, sub), sub, options);
|
|
8201
8569
|
if (isNotable(child) || child.children.length > 0)
|
|
8202
8570
|
dir.children.push(child);
|
|
8203
8571
|
}
|
|
@@ -8205,7 +8573,7 @@ async function walk(ctx, path3, name, options) {
|
|
|
8205
8573
|
}
|
|
8206
8574
|
async function facet(ctx, dir, file, read) {
|
|
8207
8575
|
try {
|
|
8208
|
-
read(await ctx.readFile(
|
|
8576
|
+
read(await ctx.readFile(join7(dir.path, file)));
|
|
8209
8577
|
} catch (error) {
|
|
8210
8578
|
dir.problems ??= [];
|
|
8211
8579
|
dir.problems.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -8244,93 +8612,12 @@ async function templateFacet(ctx, cwd) {
|
|
|
8244
8612
|
}
|
|
8245
8613
|
|
|
8246
8614
|
// ../lib/dist/propagate.js
|
|
8247
|
-
import { join as
|
|
8248
|
-
|
|
8249
|
-
// ../lib/dist/train.js
|
|
8250
|
-
import { join as join4 } from "node:path";
|
|
8251
|
-
var TRAIN_REPOS = [
|
|
8252
|
-
"platform",
|
|
8253
|
-
"ci-cd",
|
|
8254
|
-
"preset-repo-web",
|
|
8255
|
-
"preset-app-nextjs",
|
|
8256
|
-
"preset-repo-platform",
|
|
8257
|
-
"compendium"
|
|
8258
|
-
];
|
|
8259
|
-
var UPSTREAM = "plexus-ms";
|
|
8260
|
-
var isTrainVersion = (v) => /^\d+\.\d+\.\d+$/.test(v);
|
|
8261
|
-
function nextMinor(version2) {
|
|
8262
|
-
const [major = "0", minor = "0"] = version2.split(".");
|
|
8263
|
-
return `${major}.${Number(minor) + 1}.0`;
|
|
8264
|
-
}
|
|
8265
|
-
function latestTag(tags) {
|
|
8266
|
-
return tags.filter((t) => /^v?\d/.test(t)).sort((a, b) => compareTrains(a.replace(/^v/, ""), b.replace(/^v/, ""))).at(-1);
|
|
8267
|
-
}
|
|
8268
|
-
async function readTrain(ctx, upstream) {
|
|
8269
|
-
const repos = [];
|
|
8270
|
-
const all = /* @__PURE__ */ new Set();
|
|
8271
|
-
for (const name of TRAIN_REPOS) {
|
|
8272
|
-
const path3 = join4(upstream, name);
|
|
8273
|
-
try {
|
|
8274
|
-
await ctx.read("git", ["rev-parse", "--git-dir"], { cwd: path3 });
|
|
8275
|
-
} catch {
|
|
8276
|
-
throw new Error(`${path3} is not a git repo`);
|
|
8277
|
-
}
|
|
8278
|
-
try {
|
|
8279
|
-
await ctx.read("git", ["fetch", "origin", "--tags", "--quiet"], { cwd: path3 });
|
|
8280
|
-
} catch {
|
|
8281
|
-
throw new Error(`${name}: fetch from origin failed`);
|
|
8282
|
-
}
|
|
8283
|
-
const tags = (await ctx.read("git", ["tag", "--list", "v*", "[0-9]*"], { cwd: path3 })).split("\n").filter(Boolean);
|
|
8284
|
-
for (const t of tags)
|
|
8285
|
-
all.add(t.replace(/^v/, ""));
|
|
8286
|
-
const tag = latestTag(tags);
|
|
8287
|
-
const range = tag ? `${tag}..HEAD` : "HEAD";
|
|
8288
|
-
const pending = (await ctx.read("git", ["log", "--oneline", range], { cwd: path3 })).split("\n").filter(Boolean);
|
|
8289
|
-
repos.push({ name, path: path3, tag, latest: tag?.replace(/^v/, ""), pending });
|
|
8290
|
-
}
|
|
8291
|
-
const latest = repos.map((r) => r.latest);
|
|
8292
|
-
return { repos, last: latestTag([...all]), skew: latest.some((l) => l !== latest[0]) };
|
|
8293
|
-
}
|
|
8294
|
-
async function preflightRelease(ctx, train) {
|
|
8295
|
-
for (const { name, path: path3 } of train.repos) {
|
|
8296
|
-
const branch = (await ctx.read("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: path3 })).trim();
|
|
8297
|
-
if (branch !== "main")
|
|
8298
|
-
throw new Error(`${name} is on '${branch}', trains cut from main`);
|
|
8299
|
-
if ((await ctx.read("git", ["status", "--porcelain"], { cwd: path3 })).trim() !== "")
|
|
8300
|
-
throw new Error(`${name} has uncommitted changes \u2014 commit first`);
|
|
8301
|
-
const behind = (await ctx.read("git", ["rev-list", "--count", "main..origin/main"], { cwd: path3 })).trim();
|
|
8302
|
-
if (behind !== "0")
|
|
8303
|
-
throw new Error(`${name} is ${behind} commit(s) behind origin/main \u2014 pull first, or the tag lands on a stale commit`);
|
|
8304
|
-
}
|
|
8305
|
-
}
|
|
8306
|
-
function resolveVersion(train, explicit, propagateOnly) {
|
|
8307
|
-
if (explicit !== void 0 && !isTrainVersion(explicit))
|
|
8308
|
-
throw new UsageError(`'${explicit}' is not plain semver (X.Y.Z, no v)`);
|
|
8309
|
-
if (propagateOnly) {
|
|
8310
|
-
const v = explicit ?? train.last;
|
|
8311
|
-
if (!v)
|
|
8312
|
-
throw new Error("no train version found to propagate");
|
|
8313
|
-
return v;
|
|
8314
|
-
}
|
|
8315
|
-
if (explicit)
|
|
8316
|
-
return explicit;
|
|
8317
|
-
if (train.skew) {
|
|
8318
|
-
const report = train.repos.map((r) => ` ${r.name}: ${r.latest ?? "none"}`).join("\n");
|
|
8319
|
-
throw new Error(`the train repos' latest tags disagree:
|
|
8320
|
-
${report}
|
|
8321
|
-
pass the version explicitly (--train ${train.last}) to finish the half-tagged train, or a higher one to start a new one`);
|
|
8322
|
-
}
|
|
8323
|
-
return train.last ? nextMinor(train.last) : "0.1.0";
|
|
8324
|
-
}
|
|
8325
|
-
var UsageError = class extends Error {
|
|
8326
|
-
};
|
|
8327
|
-
|
|
8328
|
-
// ../lib/dist/propagate.js
|
|
8615
|
+
import { join as join8, relative as relative3 } from "node:path";
|
|
8329
8616
|
var isClean = (s) => s.conflicted.length === 0 && s.failed.length === 0 && s.skipped.length === 0;
|
|
8330
8617
|
var PREVIEW_BASE = "ship-base";
|
|
8331
8618
|
async function propagate(ctx, workspace, train) {
|
|
8332
8619
|
const summary = { train, preview: ctx.dryRun, clean: [], conflicted: [], failed: [], skipped: [] };
|
|
8333
|
-
const upstream =
|
|
8620
|
+
const upstream = join8(workspace, UPSTREAM);
|
|
8334
8621
|
const tree = await overview(ctx, workspace);
|
|
8335
8622
|
const upstreamDir = tree.children.find((c) => c.path === upstream);
|
|
8336
8623
|
const presets = TRAIN_REPOS.filter((r) => upstreamDir?.children.some((c) => c.name === r && c.template));
|
|
@@ -8347,7 +8634,7 @@ async function propagate(ctx, workspace, train) {
|
|
|
8347
8634
|
const cloneOf = async (consumer) => {
|
|
8348
8635
|
let clone = clones.get(consumer.path);
|
|
8349
8636
|
if (!clone) {
|
|
8350
|
-
clone =
|
|
8637
|
+
clone = join8(scratch ?? "", consumer.rel);
|
|
8351
8638
|
await ctx.stage(run("git", ["clone", "--quiet", consumer.path, clone]));
|
|
8352
8639
|
await ctx.stage(run("git", ["tag", PREVIEW_BASE], { cwd: clone }));
|
|
8353
8640
|
clones.set(consumer.path, clone);
|
|
@@ -8374,7 +8661,7 @@ async function propagate(ctx, workspace, train) {
|
|
|
8374
8661
|
if (sites.length === 0)
|
|
8375
8662
|
ctx.say(`\u2022 no consumers of ${presetSource(preset)} found under ${workspace}`);
|
|
8376
8663
|
for (const [consumer, site] of sites)
|
|
8377
|
-
await updateSite(ctx, consumer, site,
|
|
8664
|
+
await updateSite(ctx, consumer, site, join8(upstream, preset), summary, cloneOf, previewCommit);
|
|
8378
8665
|
}
|
|
8379
8666
|
await refreshLockfiles(ctx, active, summary, clones);
|
|
8380
8667
|
await bumpConsumerRefs(ctx, consumers, train);
|
|
@@ -8391,9 +8678,9 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
|
|
|
8391
8678
|
if (ctx.dryRun) {
|
|
8392
8679
|
ctx.say(`\u2192 ${site.rel}: copier update (preview from ${localPreset}@main)`);
|
|
8393
8680
|
repo = await cloneOf(consumer);
|
|
8394
|
-
target =
|
|
8395
|
-
const answers = await ctx.readFile(
|
|
8396
|
-
await ctx.stage(write(
|
|
8681
|
+
target = join8(repo, relative3(consumer.path, site.path));
|
|
8682
|
+
const answers = await ctx.readFile(join8(site.path, ANSWERS_FILE));
|
|
8683
|
+
await ctx.stage(write(join8(target, ANSWERS_FILE), answers.replace(/^_src_path: .*$/m, `_src_path: ${localPreset}`)));
|
|
8397
8684
|
await previewCommit(repo, `plx preview: ${site.rel} reads ${localPreset} from the local checkout`);
|
|
8398
8685
|
await ctx.stage(run("copier", ["update", "--skip-answered", "--trust", "--defaults", "--skip-tasks", "--vcs-ref", "main"], {
|
|
8399
8686
|
cwd: target
|
|
@@ -8412,7 +8699,7 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
|
|
|
8412
8699
|
let conflict = hasUnmergedEntries(status);
|
|
8413
8700
|
const changed = (await ctx.read("git", ["diff", "--name-only"], { cwd: repo })).split("\n").filter(Boolean);
|
|
8414
8701
|
for (const f of changed) {
|
|
8415
|
-
const text = await ctx.readFile(
|
|
8702
|
+
const text = await ctx.readFile(join8(repo, f)).catch(() => "");
|
|
8416
8703
|
if (hasConflictMarkers(text))
|
|
8417
8704
|
conflict = true;
|
|
8418
8705
|
}
|
|
@@ -8430,17 +8717,17 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
|
|
|
8430
8717
|
await previewCommit(repo, `plx preview: ${site.rel} updated`);
|
|
8431
8718
|
}
|
|
8432
8719
|
async function syncSiteMarker(ctx, site, target) {
|
|
8433
|
-
const marker = await ctx.readFile(
|
|
8720
|
+
const marker = await ctx.readFile(join8(target, MARKER_FILE)).catch(() => void 0);
|
|
8434
8721
|
if (marker === void 0)
|
|
8435
8722
|
return;
|
|
8436
|
-
const { commit } = parseAnswers(await ctx.readFile(
|
|
8723
|
+
const { commit } = parseAnswers(await ctx.readFile(join8(target, ANSWERS_FILE)));
|
|
8437
8724
|
if (!commit)
|
|
8438
8725
|
return;
|
|
8439
8726
|
const synced = syncMarker(marker, commit);
|
|
8440
8727
|
if (synced === marker)
|
|
8441
8728
|
return;
|
|
8442
8729
|
ctx.say(` marker: plexus \u2192 ${commit} in ${site.rel}/${MARKER_FILE}`);
|
|
8443
|
-
const step = write(
|
|
8730
|
+
const step = write(join8(target, MARKER_FILE), synced);
|
|
8444
8731
|
if (ctx.dryRun)
|
|
8445
8732
|
await ctx.stage(step);
|
|
8446
8733
|
else
|
|
@@ -8449,7 +8736,7 @@ async function syncSiteMarker(ctx, site, target) {
|
|
|
8449
8736
|
async function refreshLockfiles(ctx, consumers, summary, clones) {
|
|
8450
8737
|
const manifests = ["*package.json", "pnpm-workspace.yaml"];
|
|
8451
8738
|
for (const c of consumers) {
|
|
8452
|
-
if (await ctx.readFile(
|
|
8739
|
+
if (await ctx.readFile(join8(c.path, "pnpm-workspace.yaml")).catch(() => void 0) === void 0)
|
|
8453
8740
|
continue;
|
|
8454
8741
|
const repo = ctx.dryRun ? clones.get(c.path) : c.path;
|
|
8455
8742
|
if (!repo)
|
|
@@ -8469,7 +8756,7 @@ async function refreshLockfiles(ctx, consumers, summary, clones) {
|
|
|
8469
8756
|
}
|
|
8470
8757
|
async function bumpConsumerRefs(ctx, consumers, train) {
|
|
8471
8758
|
for (const c of consumers) {
|
|
8472
|
-
const dir =
|
|
8759
|
+
const dir = join8(c.path, ".github", "workflows");
|
|
8473
8760
|
const entries = await ctx.readDir(dir).catch(() => []);
|
|
8474
8761
|
const files = entries.filter((e) => !e.directory && /\.ya?ml$/.test(e.name)).map((e) => e.name).sort();
|
|
8475
8762
|
if (files.length === 0)
|
|
@@ -8479,14 +8766,14 @@ async function bumpConsumerRefs(ctx, consumers, train) {
|
|
|
8479
8766
|
continue;
|
|
8480
8767
|
}
|
|
8481
8768
|
for (const f of files) {
|
|
8482
|
-
const text = await ctx.readFile(
|
|
8769
|
+
const text = await ctx.readFile(join8(dir, f));
|
|
8483
8770
|
if (!text.includes(WORKFLOW_REF))
|
|
8484
8771
|
continue;
|
|
8485
8772
|
const bumped = bumpWorkflowRefs(text, train);
|
|
8486
8773
|
if (bumped === text)
|
|
8487
8774
|
continue;
|
|
8488
8775
|
ctx.say(`\u2192 ${c.rel}: ${f} ci-cd refs \u2192 @${train}`);
|
|
8489
|
-
await ctx.apply([write(
|
|
8776
|
+
await ctx.apply([write(join8(dir, f), bumped)]);
|
|
8490
8777
|
}
|
|
8491
8778
|
}
|
|
8492
8779
|
}
|
|
@@ -8497,7 +8784,7 @@ var detail = (error) => {
|
|
|
8497
8784
|
};
|
|
8498
8785
|
|
|
8499
8786
|
// ../lib/dist/requirements.js
|
|
8500
|
-
import { join as
|
|
8787
|
+
import { join as join9 } from "node:path";
|
|
8501
8788
|
var KEYWORD = /MUST|SHOULD|MAY/;
|
|
8502
8789
|
var hasKeyword = (line) => KEYWORD.test(line.replace(/MUSTs|SHOULDs|MAYs/g, ""));
|
|
8503
8790
|
var list = (value) => value.replace(/^\s*\[/, "").replace(/\].*$/, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -8590,7 +8877,7 @@ var MAP_PAGE = "essentials/standard.md";
|
|
|
8590
8877
|
async function modulePages(ctx, compendium) {
|
|
8591
8878
|
const pages = [];
|
|
8592
8879
|
for (const dir of Object.keys(CATALOG_DIRS)) {
|
|
8593
|
-
const entries = await ctx.readDir(
|
|
8880
|
+
const entries = await ctx.readDir(join9(compendium, dir));
|
|
8594
8881
|
for (const e of entries.filter((e2) => !e2.directory && e2.name.endsWith(".md") && e2.name !== "index.md").sort())
|
|
8595
8882
|
pages.push(`${dir}/${e.name}`);
|
|
8596
8883
|
}
|
|
@@ -8598,11 +8885,11 @@ async function modulePages(ctx, compendium) {
|
|
|
8598
8885
|
}
|
|
8599
8886
|
async function checkCompendium(ctx, compendium) {
|
|
8600
8887
|
const defects = [];
|
|
8601
|
-
defects.push(...scanPage(await ctx.readFile(
|
|
8888
|
+
defects.push(...scanPage(await ctx.readFile(join9(compendium, MAP_PAGE)), MAP_PAGE, "map").defects);
|
|
8602
8889
|
const pages = await modulePages(ctx, compendium);
|
|
8603
8890
|
const scans = /* @__PURE__ */ new Map();
|
|
8604
8891
|
for (const page of pages) {
|
|
8605
|
-
const scan = scanPage(await ctx.readFile(
|
|
8892
|
+
const scan = scanPage(await ctx.readFile(join9(compendium, page)), page, "module");
|
|
8606
8893
|
defects.push(...scan.defects);
|
|
8607
8894
|
scans.set(page, scan);
|
|
8608
8895
|
}
|
|
@@ -8649,9 +8936,9 @@ async function checkCompendium(ctx, compendium) {
|
|
|
8649
8936
|
}
|
|
8650
8937
|
|
|
8651
8938
|
// ../lib/dist/ship.js
|
|
8652
|
-
import { join as
|
|
8939
|
+
import { join as join10 } from "node:path";
|
|
8653
8940
|
async function ship(ctx, options) {
|
|
8654
|
-
const upstream =
|
|
8941
|
+
const upstream = join10(options.workspace, UPSTREAM);
|
|
8655
8942
|
const propagateOnly = options.propagateOnly ?? false;
|
|
8656
8943
|
const train = await readTrain(ctx, upstream);
|
|
8657
8944
|
if (!propagateOnly)
|
|
@@ -8702,7 +8989,7 @@ async function planBookkeeping(ctx, train, next, today2) {
|
|
|
8702
8989
|
return r;
|
|
8703
8990
|
};
|
|
8704
8991
|
const edit = async (r, rel, what, transform) => {
|
|
8705
|
-
const path3 =
|
|
8992
|
+
const path3 = join10(r.path, rel);
|
|
8706
8993
|
const text = await ctx.readFile(path3).catch(() => void 0);
|
|
8707
8994
|
if (text === void 0)
|
|
8708
8995
|
return;
|
|
@@ -8723,7 +9010,7 @@ async function planBookkeeping(ctx, train, next, today2) {
|
|
|
8723
9010
|
const comp = repo("compendium");
|
|
8724
9011
|
await edit(comp, MAP_PAGE, `version \u2192 ${next}, timestamp \u2192 ${today2}`, (t) => stampStandard(t, next, today2));
|
|
8725
9012
|
for (const page of await modulePages(ctx, comp.path)) {
|
|
8726
|
-
const text = await ctx.readFile(
|
|
9013
|
+
const text = await ctx.readFile(join10(comp.path, page));
|
|
8727
9014
|
if (frontmatterField(text, "maturity") !== "standard")
|
|
8728
9015
|
continue;
|
|
8729
9016
|
const previous = comp.tag ? await ctx.read("git", ["show", `${comp.tag}:${page}`], { cwd: comp.path }).catch(() => "") : "";
|
|
@@ -9686,13 +9973,13 @@ import { AsyncResource as AsyncResource3 } from "node:async_hooks";
|
|
|
9686
9973
|
|
|
9687
9974
|
// ../../node_modules/.pnpm/@inquirer+type@4.2.0_@types+node@26.6.5/node_modules/@inquirer/type/dist/utils.js
|
|
9688
9975
|
function withResolver() {
|
|
9689
|
-
let
|
|
9976
|
+
let resolve10;
|
|
9690
9977
|
let reject;
|
|
9691
9978
|
const promise = new Promise((res, rej) => {
|
|
9692
|
-
|
|
9979
|
+
resolve10 = res;
|
|
9693
9980
|
reject = rej;
|
|
9694
9981
|
});
|
|
9695
|
-
return { promise, resolve:
|
|
9982
|
+
return { promise, resolve: resolve10, reject };
|
|
9696
9983
|
}
|
|
9697
9984
|
|
|
9698
9985
|
// ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/create-prompt.js
|
|
@@ -10071,7 +10358,7 @@ function createPrompt(view) {
|
|
|
10071
10358
|
});
|
|
10072
10359
|
output.mute();
|
|
10073
10360
|
const screen = new ScreenManager(rl);
|
|
10074
|
-
const { promise, resolve:
|
|
10361
|
+
const { promise, resolve: resolve10, reject } = withResolver();
|
|
10075
10362
|
return withHooks(rl, (cycle) => {
|
|
10076
10363
|
const clearEffects = AsyncResource3.bind(() => effectScheduler.clearAll());
|
|
10077
10364
|
const settlePrompt = (settle) => {
|
|
@@ -10082,7 +10369,7 @@ function createPrompt(view) {
|
|
|
10082
10369
|
reject(error);
|
|
10083
10370
|
}
|
|
10084
10371
|
};
|
|
10085
|
-
const resolvePrompt = (value) => settlePrompt(() =>
|
|
10372
|
+
const resolvePrompt = (value) => settlePrompt(() => resolve10(value));
|
|
10086
10373
|
const rejectPrompt = (error) => settlePrompt(() => reject(error));
|
|
10087
10374
|
const promptPromise = Object.assign(promise.finally(() => {
|
|
10088
10375
|
cleanups.forEach((cleanup) => cleanup());
|
|
@@ -13601,12 +13888,22 @@ function useColor() {
|
|
|
13601
13888
|
// ../../node_modules/.pnpm/commander@15.0.0/node_modules/commander/index.js
|
|
13602
13889
|
var program = new Command();
|
|
13603
13890
|
|
|
13604
|
-
// src/commands/
|
|
13891
|
+
// src/commands/changed-apps.ts
|
|
13605
13892
|
import { resolve } from "node:path";
|
|
13606
13893
|
function register(program2) {
|
|
13894
|
+
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) => {
|
|
13895
|
+
const ctx = contextOf2(command);
|
|
13896
|
+
const result = await readChangedApps(ctx, resolve(opts.dir), from, to);
|
|
13897
|
+
ctx.result(result.apps);
|
|
13898
|
+
});
|
|
13899
|
+
}
|
|
13900
|
+
|
|
13901
|
+
// src/commands/changeset.ts
|
|
13902
|
+
import { resolve as resolve2 } from "node:path";
|
|
13903
|
+
function register2(program2) {
|
|
13607
13904
|
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
13905
|
const ctx = contextOf2(command);
|
|
13609
|
-
const changeset = await readChangeset(ctx,
|
|
13906
|
+
const changeset = await readChangeset(ctx, resolve2(dir), opts.base, opts.branch);
|
|
13610
13907
|
await ctx.apply(changeset.plan);
|
|
13611
13908
|
ctx.result(changeset, renderChangeset);
|
|
13612
13909
|
});
|
|
@@ -13617,9 +13914,49 @@ function renderChangeset(c) {
|
|
|
13617
13914
|
return [`\u2713 ${c.path}`, ...releases.map(([name, bump]) => ` ${name}: ${bump}`)].join("\n");
|
|
13618
13915
|
}
|
|
13619
13916
|
|
|
13917
|
+
// src/commands/deploy.ts
|
|
13918
|
+
import { resolve as resolve3 } from "node:path";
|
|
13919
|
+
function register3(program2) {
|
|
13920
|
+
program2.command("deploy").description(
|
|
13921
|
+
"render the app\u2019s compose file with the image pinned, stage it on the host, pull, migrate, swap, up, poll readiness, roll back on failure"
|
|
13922
|
+
).argument("<host>", "ssh target, deploy@host").argument("<tenant>", "tenant slug, the first segment of the app dir on the host").argument("<app>", "the app, apps/<app> in the checkout").option("--image <ref>", "the image ref to pin; absent for a third-party app whose compose.yaml pins its own").option(
|
|
13923
|
+
"--health-url <url>",
|
|
13924
|
+
"readiness URL polled on the host (default: /healthz, or the plexus.healthz label, on the loopback port)"
|
|
13925
|
+
).option("--retries <n>", "poll attempts, two seconds apart", "30").option("--app-dir <path>", "the app dir on the host (default: /opt/stacks/<tenant>/<app>)").option("--source <path>", "the app\u2019s dir in the checkout (default: apps/<app>)").option("-C, --dir <dir>", "the checkout to read", ".").action(async (host, tenant, app, opts, command) => {
|
|
13926
|
+
const ctx = contextOf2(command);
|
|
13927
|
+
const result = await deploy(ctx, {
|
|
13928
|
+
host,
|
|
13929
|
+
tenant,
|
|
13930
|
+
app,
|
|
13931
|
+
image: opts.image,
|
|
13932
|
+
healthUrl: opts.healthUrl,
|
|
13933
|
+
retries: Number(opts.retries),
|
|
13934
|
+
appDir: opts.appDir,
|
|
13935
|
+
source: opts.source,
|
|
13936
|
+
dir: resolve3(opts.dir)
|
|
13937
|
+
});
|
|
13938
|
+
ctx.result(result, renderDeploy);
|
|
13939
|
+
if (!result.healthy) process.exitCode = 1;
|
|
13940
|
+
});
|
|
13941
|
+
}
|
|
13942
|
+
function renderDeploy(d) {
|
|
13943
|
+
if (d.healthy) return d.live ?? "";
|
|
13944
|
+
return d.rolledBack ? `rolled back to ${d.live ?? "the previous compose.yaml"}` : "not healthy";
|
|
13945
|
+
}
|
|
13946
|
+
|
|
13947
|
+
// src/commands/deploy-targets.ts
|
|
13948
|
+
import { resolve as resolve4 } from "node:path";
|
|
13949
|
+
function register4(program2) {
|
|
13950
|
+
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) => {
|
|
13951
|
+
const ctx = contextOf2(command);
|
|
13952
|
+
const targets = await readDeployTargets(ctx, resolve4(inventory));
|
|
13953
|
+
ctx.result(targets, (t) => JSON.stringify(t));
|
|
13954
|
+
});
|
|
13955
|
+
}
|
|
13956
|
+
|
|
13620
13957
|
// src/commands/doctor.ts
|
|
13621
13958
|
import { existsSync } from "node:fs";
|
|
13622
|
-
import { join as
|
|
13959
|
+
import { join as join11, resolve as resolve5 } from "node:path";
|
|
13623
13960
|
|
|
13624
13961
|
// src/style.ts
|
|
13625
13962
|
function style(enabled) {
|
|
@@ -13630,15 +13967,15 @@ var colorByDefault = () => process.stdout.isTTY === true && !process.env.NO_COLO
|
|
|
13630
13967
|
|
|
13631
13968
|
// src/commands/doctor.ts
|
|
13632
13969
|
var WORKSPACE_COMPENDIUM = "plexus-ms/compendium";
|
|
13633
|
-
function
|
|
13970
|
+
function register5(program2) {
|
|
13634
13971
|
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(
|
|
13635
13972
|
"--compendium <path>",
|
|
13636
13973
|
`compendium checkout to read the modules from (default: <dir>/${WORKSPACE_COMPENDIUM})`
|
|
13637
13974
|
).option("--color", "colour the report even when piped").option("--no-color", "never colour the report").action(async (dir, opts, command) => {
|
|
13638
13975
|
const ctx = contextOf2(command);
|
|
13639
|
-
const root =
|
|
13640
|
-
const compendium =
|
|
13641
|
-
if (!existsSync(
|
|
13976
|
+
const root = resolve5(dir);
|
|
13977
|
+
const compendium = resolve5(opts.compendium ?? join11(root, WORKSPACE_COMPENDIUM));
|
|
13978
|
+
if (!existsSync(join11(compendium, "contracts")))
|
|
13642
13979
|
throw new Error(
|
|
13643
13980
|
`no compendium at ${compendium}; pass --compendium <path to a checkout of plexus-ms/compendium>`
|
|
13644
13981
|
);
|
|
@@ -13666,11 +14003,11 @@ function renderReport(report, s, showCurrent) {
|
|
|
13666
14003
|
}
|
|
13667
14004
|
|
|
13668
14005
|
// src/commands/overview.ts
|
|
13669
|
-
import { resolve as
|
|
13670
|
-
function
|
|
14006
|
+
import { resolve as resolve6 } from "node:path";
|
|
14007
|
+
function register6(program2) {
|
|
13671
14008
|
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) => {
|
|
13672
14009
|
const ctx = contextOf2(command);
|
|
13673
|
-
const tree = await overview(ctx,
|
|
14010
|
+
const tree = await overview(ctx, resolve6(dir));
|
|
13674
14011
|
const s = style(opts.color ?? colorByDefault());
|
|
13675
14012
|
ctx.result(tree, (t) => renderTree(t, s, ctx.verbose >= 1));
|
|
13676
14013
|
});
|
|
@@ -13713,14 +14050,14 @@ function annotate(dir, s, modules) {
|
|
|
13713
14050
|
}
|
|
13714
14051
|
|
|
13715
14052
|
// src/commands/propagate.ts
|
|
13716
|
-
import { resolve as
|
|
14053
|
+
import { resolve as resolve8 } from "node:path";
|
|
13717
14054
|
|
|
13718
14055
|
// src/commands/ship.ts
|
|
13719
|
-
import { resolve as
|
|
13720
|
-
function
|
|
14056
|
+
import { resolve as resolve7 } from "node:path";
|
|
14057
|
+
function register7(program2) {
|
|
13721
14058
|
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) => {
|
|
13722
14059
|
const ctx = contextOf2(command);
|
|
13723
|
-
const result = await ship(ctx, { workspace:
|
|
14060
|
+
const result = await ship(ctx, { workspace: resolve7(dir), train: opts.train });
|
|
13724
14061
|
ctx.result(result, (r) => renderSummary(r, `plx -y ship --train ${r.train} ${dir}`));
|
|
13725
14062
|
if (!isClean(result)) process.exitCode = 1;
|
|
13726
14063
|
});
|
|
@@ -13743,23 +14080,23 @@ function renderSummary(s, realCommand) {
|
|
|
13743
14080
|
}
|
|
13744
14081
|
|
|
13745
14082
|
// src/commands/propagate.ts
|
|
13746
|
-
function
|
|
14083
|
+
function register8(program2) {
|
|
13747
14084
|
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) => {
|
|
13748
14085
|
const ctx = contextOf2(command);
|
|
13749
|
-
const result = await ship(ctx, { workspace:
|
|
14086
|
+
const result = await ship(ctx, { workspace: resolve8(dir), train: opts.train, propagateOnly: true });
|
|
13750
14087
|
ctx.result(result, (r) => renderSummary(r, `plx -y propagate --train ${r.train} ${dir}`));
|
|
13751
14088
|
if (!isClean(result)) process.exitCode = 1;
|
|
13752
14089
|
});
|
|
13753
14090
|
}
|
|
13754
14091
|
|
|
13755
14092
|
// src/commands/requirements.ts
|
|
13756
|
-
import { join as
|
|
13757
|
-
function
|
|
14093
|
+
import { join as join12, resolve as resolve9 } from "node:path";
|
|
14094
|
+
function register9(program2) {
|
|
13758
14095
|
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) => {
|
|
13759
14096
|
const ctx = contextOf2(command);
|
|
13760
|
-
const compendium =
|
|
14097
|
+
const compendium = resolve9(dir);
|
|
13761
14098
|
if (opts.extract) {
|
|
13762
|
-
ctx.result(extractRequirements(await ctx.readFile(
|
|
14099
|
+
ctx.result(extractRequirements(await ctx.readFile(join12(compendium, opts.extract))));
|
|
13763
14100
|
return;
|
|
13764
14101
|
}
|
|
13765
14102
|
const check = await checkCompendium(ctx, compendium);
|
|
@@ -13773,7 +14110,17 @@ function renderCheck(c) {
|
|
|
13773
14110
|
}
|
|
13774
14111
|
|
|
13775
14112
|
// src/commands/index.ts
|
|
13776
|
-
var commands = [
|
|
14113
|
+
var commands = [
|
|
14114
|
+
register6,
|
|
14115
|
+
register5,
|
|
14116
|
+
register7,
|
|
14117
|
+
register8,
|
|
14118
|
+
register9,
|
|
14119
|
+
register2,
|
|
14120
|
+
register,
|
|
14121
|
+
register4,
|
|
14122
|
+
register3
|
|
14123
|
+
];
|
|
13777
14124
|
|
|
13778
14125
|
// src/program.ts
|
|
13779
14126
|
var { version, description } = createRequire(import.meta.url)("../package.json");
|
|
@@ -13782,7 +14129,7 @@ function buildProgram() {
|
|
|
13782
14129
|
program2.addOption(new Option("-n, --dry-run", "print every effect this run would have, perform none").env("PLX_DRY_RUN")).addOption(
|
|
13783
14130
|
new Option("-v, --verbose", "say more: once for reads and inputs, twice for command output").argParser((_value, previous) => previous + 1).default(0)
|
|
13784
14131
|
).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"));
|
|
13785
|
-
for (const
|
|
14132
|
+
for (const register10 of commands) register10(program2);
|
|
13786
14133
|
return program2;
|
|
13787
14134
|
}
|
|
13788
14135
|
function contextOf2(command) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@plexus-ms/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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,11 +20,11 @@
|
|
|
20
20
|
"access": "public"
|
|
21
21
|
},
|
|
22
22
|
"engines": {
|
|
23
|
-
"node": ">=24"
|
|
23
|
+
"node": ">=24.22.1"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@inquirer/prompts": "^8.7.4",
|
|
27
|
-
"@plexus-ms/lib": "0.
|
|
27
|
+
"@plexus-ms/lib": "0.6.0",
|
|
28
28
|
"commander": "^15.0.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|