@plexus-ms/cli 0.5.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 +259 -67
- package/package.json +2 -2
package/dist/plx.js
CHANGED
|
@@ -7754,7 +7754,7 @@ function realIo(prompt) {
|
|
|
7754
7754
|
stdout: process.stdout,
|
|
7755
7755
|
stderr: process.stderr,
|
|
7756
7756
|
prompt: prompt && isInteractive() ? prompt : void 0,
|
|
7757
|
-
exec: (command, args, options = {}) => new Promise((
|
|
7757
|
+
exec: (command, args, options = {}) => new Promise((resolve10, reject) => {
|
|
7758
7758
|
const child = spawn(command, args, {
|
|
7759
7759
|
cwd: options.cwd,
|
|
7760
7760
|
env: options.env,
|
|
@@ -7777,7 +7777,7 @@ function realIo(prompt) {
|
|
|
7777
7777
|
child.on("error", reject);
|
|
7778
7778
|
child.on("close", (code) => {
|
|
7779
7779
|
if (code === 0)
|
|
7780
|
-
|
|
7780
|
+
resolve10(stdout);
|
|
7781
7781
|
else
|
|
7782
7782
|
reject(new ExecError(render(command, args), code, stderr));
|
|
7783
7783
|
});
|
|
@@ -7808,8 +7808,6 @@ function describe(step) {
|
|
|
7808
7808
|
switch (step.kind) {
|
|
7809
7809
|
case "run":
|
|
7810
7810
|
return `$ ${step.cwd ? `(cd ${step.cwd} && ` : ""}${render(step.command, step.args)}${step.cwd ? ")" : ""}`;
|
|
7811
|
-
case "ssh":
|
|
7812
|
-
return `$ ${render("ssh", [step.host, "bash", "-s"])} <<'EOF'`;
|
|
7813
7811
|
case "write":
|
|
7814
7812
|
return `> ${step.path} (${Buffer.byteLength(step.content)} bytes)`;
|
|
7815
7813
|
}
|
|
@@ -7818,9 +7816,6 @@ function body(step) {
|
|
|
7818
7816
|
switch (step.kind) {
|
|
7819
7817
|
case "run":
|
|
7820
7818
|
return step.input;
|
|
7821
|
-
case "ssh":
|
|
7822
|
-
return `${step.script.trimEnd()}
|
|
7823
|
-
EOF`;
|
|
7824
7819
|
case "write":
|
|
7825
7820
|
return step.content;
|
|
7826
7821
|
}
|
|
@@ -8110,8 +8105,6 @@ var Context = class {
|
|
|
8110
8105
|
switch (step.kind) {
|
|
8111
8106
|
case "run":
|
|
8112
8107
|
return this.io.exec(step.command, step.args, { cwd: step.cwd, input: step.input, inherit });
|
|
8113
|
-
case "ssh":
|
|
8114
|
-
return this.io.exec("ssh", [step.host, "bash", "-s"], { input: step.script, inherit });
|
|
8115
8108
|
case "write":
|
|
8116
8109
|
await this.io.writeFile(step.path, step.content);
|
|
8117
8110
|
return void 0;
|
|
@@ -8145,8 +8138,9 @@ function parseAnswers(text) {
|
|
|
8145
8138
|
};
|
|
8146
8139
|
}
|
|
8147
8140
|
|
|
8148
|
-
// ../lib/dist/
|
|
8149
|
-
|
|
8141
|
+
// ../lib/dist/deploy.js
|
|
8142
|
+
import { randomBytes } from "node:crypto";
|
|
8143
|
+
import { tmpdir } from "node:os";
|
|
8150
8144
|
import { join as join5 } from "node:path";
|
|
8151
8145
|
|
|
8152
8146
|
// ../lib/dist/train.js
|
|
@@ -8289,7 +8283,174 @@ pass the version explicitly (--train ${train.last}) to finish the half-tagged tr
|
|
|
8289
8283
|
var UsageError = class extends Error {
|
|
8290
8284
|
};
|
|
8291
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
|
+
|
|
8292
8451
|
// ../lib/dist/deployTargets.js
|
|
8452
|
+
var import_yaml4 = __toESM(require_dist(), 1);
|
|
8453
|
+
import { join as join6 } from "node:path";
|
|
8293
8454
|
var VARS_DIRS = ["group_vars", "host_vars"];
|
|
8294
8455
|
function parseInventoryFile(path3, text) {
|
|
8295
8456
|
const data = (0, import_yaml4.parse)(text);
|
|
@@ -8318,7 +8479,7 @@ async function readDeployTargets(ctx, inventory) {
|
|
|
8318
8479
|
const files = [];
|
|
8319
8480
|
let found = false;
|
|
8320
8481
|
for (const sub of VARS_DIRS) {
|
|
8321
|
-
const dir =
|
|
8482
|
+
const dir = join6(inventory, sub);
|
|
8322
8483
|
const entries = await ctx.readDir(dir).catch(() => void 0);
|
|
8323
8484
|
if (entries === void 0)
|
|
8324
8485
|
continue;
|
|
@@ -8326,7 +8487,7 @@ async function readDeployTargets(ctx, inventory) {
|
|
|
8326
8487
|
for (const e of entries) {
|
|
8327
8488
|
if (e.directory || !/\.ya?ml$/.test(e.name))
|
|
8328
8489
|
continue;
|
|
8329
|
-
const path3 =
|
|
8490
|
+
const path3 = join6(dir, e.name);
|
|
8330
8491
|
files.push(parseInventoryFile(path3, await ctx.readFile(path3)));
|
|
8331
8492
|
}
|
|
8332
8493
|
}
|
|
@@ -8356,7 +8517,7 @@ function isNotable(dir) {
|
|
|
8356
8517
|
}
|
|
8357
8518
|
|
|
8358
8519
|
// ../lib/dist/overview.js
|
|
8359
|
-
import { join as
|
|
8520
|
+
import { join as join7 } from "node:path";
|
|
8360
8521
|
var PRUNED = /* @__PURE__ */ new Set([
|
|
8361
8522
|
"node_modules",
|
|
8362
8523
|
".hg",
|
|
@@ -8404,7 +8565,7 @@ async function walk(ctx, path3, name, options) {
|
|
|
8404
8565
|
}
|
|
8405
8566
|
const subdirs = entries.filter((e) => e.directory && e.name !== ".git" && !PRUNED.has(e.name)).map((e) => e.name).sort();
|
|
8406
8567
|
for (const sub of subdirs) {
|
|
8407
|
-
const child = await walk(ctx,
|
|
8568
|
+
const child = await walk(ctx, join7(path3, sub), sub, options);
|
|
8408
8569
|
if (isNotable(child) || child.children.length > 0)
|
|
8409
8570
|
dir.children.push(child);
|
|
8410
8571
|
}
|
|
@@ -8412,7 +8573,7 @@ async function walk(ctx, path3, name, options) {
|
|
|
8412
8573
|
}
|
|
8413
8574
|
async function facet(ctx, dir, file, read) {
|
|
8414
8575
|
try {
|
|
8415
|
-
read(await ctx.readFile(
|
|
8576
|
+
read(await ctx.readFile(join7(dir.path, file)));
|
|
8416
8577
|
} catch (error) {
|
|
8417
8578
|
dir.problems ??= [];
|
|
8418
8579
|
dir.problems.push(`${file}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -8451,12 +8612,12 @@ async function templateFacet(ctx, cwd) {
|
|
|
8451
8612
|
}
|
|
8452
8613
|
|
|
8453
8614
|
// ../lib/dist/propagate.js
|
|
8454
|
-
import { join as
|
|
8615
|
+
import { join as join8, relative as relative3 } from "node:path";
|
|
8455
8616
|
var isClean = (s) => s.conflicted.length === 0 && s.failed.length === 0 && s.skipped.length === 0;
|
|
8456
8617
|
var PREVIEW_BASE = "ship-base";
|
|
8457
8618
|
async function propagate(ctx, workspace, train) {
|
|
8458
8619
|
const summary = { train, preview: ctx.dryRun, clean: [], conflicted: [], failed: [], skipped: [] };
|
|
8459
|
-
const upstream =
|
|
8620
|
+
const upstream = join8(workspace, UPSTREAM);
|
|
8460
8621
|
const tree = await overview(ctx, workspace);
|
|
8461
8622
|
const upstreamDir = tree.children.find((c) => c.path === upstream);
|
|
8462
8623
|
const presets = TRAIN_REPOS.filter((r) => upstreamDir?.children.some((c) => c.name === r && c.template));
|
|
@@ -8473,7 +8634,7 @@ async function propagate(ctx, workspace, train) {
|
|
|
8473
8634
|
const cloneOf = async (consumer) => {
|
|
8474
8635
|
let clone = clones.get(consumer.path);
|
|
8475
8636
|
if (!clone) {
|
|
8476
|
-
clone =
|
|
8637
|
+
clone = join8(scratch ?? "", consumer.rel);
|
|
8477
8638
|
await ctx.stage(run("git", ["clone", "--quiet", consumer.path, clone]));
|
|
8478
8639
|
await ctx.stage(run("git", ["tag", PREVIEW_BASE], { cwd: clone }));
|
|
8479
8640
|
clones.set(consumer.path, clone);
|
|
@@ -8500,7 +8661,7 @@ async function propagate(ctx, workspace, train) {
|
|
|
8500
8661
|
if (sites.length === 0)
|
|
8501
8662
|
ctx.say(`\u2022 no consumers of ${presetSource(preset)} found under ${workspace}`);
|
|
8502
8663
|
for (const [consumer, site] of sites)
|
|
8503
|
-
await updateSite(ctx, consumer, site,
|
|
8664
|
+
await updateSite(ctx, consumer, site, join8(upstream, preset), summary, cloneOf, previewCommit);
|
|
8504
8665
|
}
|
|
8505
8666
|
await refreshLockfiles(ctx, active, summary, clones);
|
|
8506
8667
|
await bumpConsumerRefs(ctx, consumers, train);
|
|
@@ -8517,9 +8678,9 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
|
|
|
8517
8678
|
if (ctx.dryRun) {
|
|
8518
8679
|
ctx.say(`\u2192 ${site.rel}: copier update (preview from ${localPreset}@main)`);
|
|
8519
8680
|
repo = await cloneOf(consumer);
|
|
8520
|
-
target =
|
|
8521
|
-
const answers = await ctx.readFile(
|
|
8522
|
-
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}`)));
|
|
8523
8684
|
await previewCommit(repo, `plx preview: ${site.rel} reads ${localPreset} from the local checkout`);
|
|
8524
8685
|
await ctx.stage(run("copier", ["update", "--skip-answered", "--trust", "--defaults", "--skip-tasks", "--vcs-ref", "main"], {
|
|
8525
8686
|
cwd: target
|
|
@@ -8538,7 +8699,7 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
|
|
|
8538
8699
|
let conflict = hasUnmergedEntries(status);
|
|
8539
8700
|
const changed = (await ctx.read("git", ["diff", "--name-only"], { cwd: repo })).split("\n").filter(Boolean);
|
|
8540
8701
|
for (const f of changed) {
|
|
8541
|
-
const text = await ctx.readFile(
|
|
8702
|
+
const text = await ctx.readFile(join8(repo, f)).catch(() => "");
|
|
8542
8703
|
if (hasConflictMarkers(text))
|
|
8543
8704
|
conflict = true;
|
|
8544
8705
|
}
|
|
@@ -8556,17 +8717,17 @@ async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, pr
|
|
|
8556
8717
|
await previewCommit(repo, `plx preview: ${site.rel} updated`);
|
|
8557
8718
|
}
|
|
8558
8719
|
async function syncSiteMarker(ctx, site, target) {
|
|
8559
|
-
const marker = await ctx.readFile(
|
|
8720
|
+
const marker = await ctx.readFile(join8(target, MARKER_FILE)).catch(() => void 0);
|
|
8560
8721
|
if (marker === void 0)
|
|
8561
8722
|
return;
|
|
8562
|
-
const { commit } = parseAnswers(await ctx.readFile(
|
|
8723
|
+
const { commit } = parseAnswers(await ctx.readFile(join8(target, ANSWERS_FILE)));
|
|
8563
8724
|
if (!commit)
|
|
8564
8725
|
return;
|
|
8565
8726
|
const synced = syncMarker(marker, commit);
|
|
8566
8727
|
if (synced === marker)
|
|
8567
8728
|
return;
|
|
8568
8729
|
ctx.say(` marker: plexus \u2192 ${commit} in ${site.rel}/${MARKER_FILE}`);
|
|
8569
|
-
const step = write(
|
|
8730
|
+
const step = write(join8(target, MARKER_FILE), synced);
|
|
8570
8731
|
if (ctx.dryRun)
|
|
8571
8732
|
await ctx.stage(step);
|
|
8572
8733
|
else
|
|
@@ -8575,7 +8736,7 @@ async function syncSiteMarker(ctx, site, target) {
|
|
|
8575
8736
|
async function refreshLockfiles(ctx, consumers, summary, clones) {
|
|
8576
8737
|
const manifests = ["*package.json", "pnpm-workspace.yaml"];
|
|
8577
8738
|
for (const c of consumers) {
|
|
8578
|
-
if (await ctx.readFile(
|
|
8739
|
+
if (await ctx.readFile(join8(c.path, "pnpm-workspace.yaml")).catch(() => void 0) === void 0)
|
|
8579
8740
|
continue;
|
|
8580
8741
|
const repo = ctx.dryRun ? clones.get(c.path) : c.path;
|
|
8581
8742
|
if (!repo)
|
|
@@ -8595,7 +8756,7 @@ async function refreshLockfiles(ctx, consumers, summary, clones) {
|
|
|
8595
8756
|
}
|
|
8596
8757
|
async function bumpConsumerRefs(ctx, consumers, train) {
|
|
8597
8758
|
for (const c of consumers) {
|
|
8598
|
-
const dir =
|
|
8759
|
+
const dir = join8(c.path, ".github", "workflows");
|
|
8599
8760
|
const entries = await ctx.readDir(dir).catch(() => []);
|
|
8600
8761
|
const files = entries.filter((e) => !e.directory && /\.ya?ml$/.test(e.name)).map((e) => e.name).sort();
|
|
8601
8762
|
if (files.length === 0)
|
|
@@ -8605,14 +8766,14 @@ async function bumpConsumerRefs(ctx, consumers, train) {
|
|
|
8605
8766
|
continue;
|
|
8606
8767
|
}
|
|
8607
8768
|
for (const f of files) {
|
|
8608
|
-
const text = await ctx.readFile(
|
|
8769
|
+
const text = await ctx.readFile(join8(dir, f));
|
|
8609
8770
|
if (!text.includes(WORKFLOW_REF))
|
|
8610
8771
|
continue;
|
|
8611
8772
|
const bumped = bumpWorkflowRefs(text, train);
|
|
8612
8773
|
if (bumped === text)
|
|
8613
8774
|
continue;
|
|
8614
8775
|
ctx.say(`\u2192 ${c.rel}: ${f} ci-cd refs \u2192 @${train}`);
|
|
8615
|
-
await ctx.apply([write(
|
|
8776
|
+
await ctx.apply([write(join8(dir, f), bumped)]);
|
|
8616
8777
|
}
|
|
8617
8778
|
}
|
|
8618
8779
|
}
|
|
@@ -8623,7 +8784,7 @@ var detail = (error) => {
|
|
|
8623
8784
|
};
|
|
8624
8785
|
|
|
8625
8786
|
// ../lib/dist/requirements.js
|
|
8626
|
-
import { join as
|
|
8787
|
+
import { join as join9 } from "node:path";
|
|
8627
8788
|
var KEYWORD = /MUST|SHOULD|MAY/;
|
|
8628
8789
|
var hasKeyword = (line) => KEYWORD.test(line.replace(/MUSTs|SHOULDs|MAYs/g, ""));
|
|
8629
8790
|
var list = (value) => value.replace(/^\s*\[/, "").replace(/\].*$/, "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
@@ -8716,7 +8877,7 @@ var MAP_PAGE = "essentials/standard.md";
|
|
|
8716
8877
|
async function modulePages(ctx, compendium) {
|
|
8717
8878
|
const pages = [];
|
|
8718
8879
|
for (const dir of Object.keys(CATALOG_DIRS)) {
|
|
8719
|
-
const entries = await ctx.readDir(
|
|
8880
|
+
const entries = await ctx.readDir(join9(compendium, dir));
|
|
8720
8881
|
for (const e of entries.filter((e2) => !e2.directory && e2.name.endsWith(".md") && e2.name !== "index.md").sort())
|
|
8721
8882
|
pages.push(`${dir}/${e.name}`);
|
|
8722
8883
|
}
|
|
@@ -8724,11 +8885,11 @@ async function modulePages(ctx, compendium) {
|
|
|
8724
8885
|
}
|
|
8725
8886
|
async function checkCompendium(ctx, compendium) {
|
|
8726
8887
|
const defects = [];
|
|
8727
|
-
defects.push(...scanPage(await ctx.readFile(
|
|
8888
|
+
defects.push(...scanPage(await ctx.readFile(join9(compendium, MAP_PAGE)), MAP_PAGE, "map").defects);
|
|
8728
8889
|
const pages = await modulePages(ctx, compendium);
|
|
8729
8890
|
const scans = /* @__PURE__ */ new Map();
|
|
8730
8891
|
for (const page of pages) {
|
|
8731
|
-
const scan = scanPage(await ctx.readFile(
|
|
8892
|
+
const scan = scanPage(await ctx.readFile(join9(compendium, page)), page, "module");
|
|
8732
8893
|
defects.push(...scan.defects);
|
|
8733
8894
|
scans.set(page, scan);
|
|
8734
8895
|
}
|
|
@@ -8775,9 +8936,9 @@ async function checkCompendium(ctx, compendium) {
|
|
|
8775
8936
|
}
|
|
8776
8937
|
|
|
8777
8938
|
// ../lib/dist/ship.js
|
|
8778
|
-
import { join as
|
|
8939
|
+
import { join as join10 } from "node:path";
|
|
8779
8940
|
async function ship(ctx, options) {
|
|
8780
|
-
const upstream =
|
|
8941
|
+
const upstream = join10(options.workspace, UPSTREAM);
|
|
8781
8942
|
const propagateOnly = options.propagateOnly ?? false;
|
|
8782
8943
|
const train = await readTrain(ctx, upstream);
|
|
8783
8944
|
if (!propagateOnly)
|
|
@@ -8828,7 +8989,7 @@ async function planBookkeeping(ctx, train, next, today2) {
|
|
|
8828
8989
|
return r;
|
|
8829
8990
|
};
|
|
8830
8991
|
const edit = async (r, rel, what, transform) => {
|
|
8831
|
-
const path3 =
|
|
8992
|
+
const path3 = join10(r.path, rel);
|
|
8832
8993
|
const text = await ctx.readFile(path3).catch(() => void 0);
|
|
8833
8994
|
if (text === void 0)
|
|
8834
8995
|
return;
|
|
@@ -8849,7 +9010,7 @@ async function planBookkeeping(ctx, train, next, today2) {
|
|
|
8849
9010
|
const comp = repo("compendium");
|
|
8850
9011
|
await edit(comp, MAP_PAGE, `version \u2192 ${next}, timestamp \u2192 ${today2}`, (t) => stampStandard(t, next, today2));
|
|
8851
9012
|
for (const page of await modulePages(ctx, comp.path)) {
|
|
8852
|
-
const text = await ctx.readFile(
|
|
9013
|
+
const text = await ctx.readFile(join10(comp.path, page));
|
|
8853
9014
|
if (frontmatterField(text, "maturity") !== "standard")
|
|
8854
9015
|
continue;
|
|
8855
9016
|
const previous = comp.tag ? await ctx.read("git", ["show", `${comp.tag}:${page}`], { cwd: comp.path }).catch(() => "") : "";
|
|
@@ -9812,13 +9973,13 @@ import { AsyncResource as AsyncResource3 } from "node:async_hooks";
|
|
|
9812
9973
|
|
|
9813
9974
|
// ../../node_modules/.pnpm/@inquirer+type@4.2.0_@types+node@26.6.5/node_modules/@inquirer/type/dist/utils.js
|
|
9814
9975
|
function withResolver() {
|
|
9815
|
-
let
|
|
9976
|
+
let resolve10;
|
|
9816
9977
|
let reject;
|
|
9817
9978
|
const promise = new Promise((res, rej) => {
|
|
9818
|
-
|
|
9979
|
+
resolve10 = res;
|
|
9819
9980
|
reject = rej;
|
|
9820
9981
|
});
|
|
9821
|
-
return { promise, resolve:
|
|
9982
|
+
return { promise, resolve: resolve10, reject };
|
|
9822
9983
|
}
|
|
9823
9984
|
|
|
9824
9985
|
// ../../node_modules/.pnpm/@inquirer+core@12.0.5_@types+node@26.6.5/node_modules/@inquirer/core/dist/lib/create-prompt.js
|
|
@@ -10197,7 +10358,7 @@ function createPrompt(view) {
|
|
|
10197
10358
|
});
|
|
10198
10359
|
output.mute();
|
|
10199
10360
|
const screen = new ScreenManager(rl);
|
|
10200
|
-
const { promise, resolve:
|
|
10361
|
+
const { promise, resolve: resolve10, reject } = withResolver();
|
|
10201
10362
|
return withHooks(rl, (cycle) => {
|
|
10202
10363
|
const clearEffects = AsyncResource3.bind(() => effectScheduler.clearAll());
|
|
10203
10364
|
const settlePrompt = (settle) => {
|
|
@@ -10208,7 +10369,7 @@ function createPrompt(view) {
|
|
|
10208
10369
|
reject(error);
|
|
10209
10370
|
}
|
|
10210
10371
|
};
|
|
10211
|
-
const resolvePrompt = (value) => settlePrompt(() =>
|
|
10372
|
+
const resolvePrompt = (value) => settlePrompt(() => resolve10(value));
|
|
10212
10373
|
const rejectPrompt = (error) => settlePrompt(() => reject(error));
|
|
10213
10374
|
const promptPromise = Object.assign(promise.finally(() => {
|
|
10214
10375
|
cleanups.forEach((cleanup) => cleanup());
|
|
@@ -13753,19 +13914,49 @@ function renderChangeset(c) {
|
|
|
13753
13914
|
return [`\u2713 ${c.path}`, ...releases.map(([name, bump]) => ` ${name}: ${bump}`)].join("\n");
|
|
13754
13915
|
}
|
|
13755
13916
|
|
|
13756
|
-
// src/commands/deploy
|
|
13917
|
+
// src/commands/deploy.ts
|
|
13757
13918
|
import { resolve as resolve3 } from "node:path";
|
|
13758
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) {
|
|
13759
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) => {
|
|
13760
13951
|
const ctx = contextOf2(command);
|
|
13761
|
-
const targets = await readDeployTargets(ctx,
|
|
13952
|
+
const targets = await readDeployTargets(ctx, resolve4(inventory));
|
|
13762
13953
|
ctx.result(targets, (t) => JSON.stringify(t));
|
|
13763
13954
|
});
|
|
13764
13955
|
}
|
|
13765
13956
|
|
|
13766
13957
|
// src/commands/doctor.ts
|
|
13767
13958
|
import { existsSync } from "node:fs";
|
|
13768
|
-
import { join as
|
|
13959
|
+
import { join as join11, resolve as resolve5 } from "node:path";
|
|
13769
13960
|
|
|
13770
13961
|
// src/style.ts
|
|
13771
13962
|
function style(enabled) {
|
|
@@ -13776,15 +13967,15 @@ var colorByDefault = () => process.stdout.isTTY === true && !process.env.NO_COLO
|
|
|
13776
13967
|
|
|
13777
13968
|
// src/commands/doctor.ts
|
|
13778
13969
|
var WORKSPACE_COMPENDIUM = "plexus-ms/compendium";
|
|
13779
|
-
function
|
|
13970
|
+
function register5(program2) {
|
|
13780
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(
|
|
13781
13972
|
"--compendium <path>",
|
|
13782
13973
|
`compendium checkout to read the modules from (default: <dir>/${WORKSPACE_COMPENDIUM})`
|
|
13783
13974
|
).option("--color", "colour the report even when piped").option("--no-color", "never colour the report").action(async (dir, opts, command) => {
|
|
13784
13975
|
const ctx = contextOf2(command);
|
|
13785
|
-
const root =
|
|
13786
|
-
const compendium =
|
|
13787
|
-
if (!existsSync(
|
|
13976
|
+
const root = resolve5(dir);
|
|
13977
|
+
const compendium = resolve5(opts.compendium ?? join11(root, WORKSPACE_COMPENDIUM));
|
|
13978
|
+
if (!existsSync(join11(compendium, "contracts")))
|
|
13788
13979
|
throw new Error(
|
|
13789
13980
|
`no compendium at ${compendium}; pass --compendium <path to a checkout of plexus-ms/compendium>`
|
|
13790
13981
|
);
|
|
@@ -13812,11 +14003,11 @@ function renderReport(report, s, showCurrent) {
|
|
|
13812
14003
|
}
|
|
13813
14004
|
|
|
13814
14005
|
// src/commands/overview.ts
|
|
13815
|
-
import { resolve as
|
|
13816
|
-
function
|
|
14006
|
+
import { resolve as resolve6 } from "node:path";
|
|
14007
|
+
function register6(program2) {
|
|
13817
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) => {
|
|
13818
14009
|
const ctx = contextOf2(command);
|
|
13819
|
-
const tree = await overview(ctx,
|
|
14010
|
+
const tree = await overview(ctx, resolve6(dir));
|
|
13820
14011
|
const s = style(opts.color ?? colorByDefault());
|
|
13821
14012
|
ctx.result(tree, (t) => renderTree(t, s, ctx.verbose >= 1));
|
|
13822
14013
|
});
|
|
@@ -13859,14 +14050,14 @@ function annotate(dir, s, modules) {
|
|
|
13859
14050
|
}
|
|
13860
14051
|
|
|
13861
14052
|
// src/commands/propagate.ts
|
|
13862
|
-
import { resolve as
|
|
14053
|
+
import { resolve as resolve8 } from "node:path";
|
|
13863
14054
|
|
|
13864
14055
|
// src/commands/ship.ts
|
|
13865
|
-
import { resolve as
|
|
13866
|
-
function
|
|
14056
|
+
import { resolve as resolve7 } from "node:path";
|
|
14057
|
+
function register7(program2) {
|
|
13867
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) => {
|
|
13868
14059
|
const ctx = contextOf2(command);
|
|
13869
|
-
const result = await ship(ctx, { workspace:
|
|
14060
|
+
const result = await ship(ctx, { workspace: resolve7(dir), train: opts.train });
|
|
13870
14061
|
ctx.result(result, (r) => renderSummary(r, `plx -y ship --train ${r.train} ${dir}`));
|
|
13871
14062
|
if (!isClean(result)) process.exitCode = 1;
|
|
13872
14063
|
});
|
|
@@ -13889,23 +14080,23 @@ function renderSummary(s, realCommand) {
|
|
|
13889
14080
|
}
|
|
13890
14081
|
|
|
13891
14082
|
// src/commands/propagate.ts
|
|
13892
|
-
function
|
|
14083
|
+
function register8(program2) {
|
|
13893
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) => {
|
|
13894
14085
|
const ctx = contextOf2(command);
|
|
13895
|
-
const result = await ship(ctx, { workspace:
|
|
14086
|
+
const result = await ship(ctx, { workspace: resolve8(dir), train: opts.train, propagateOnly: true });
|
|
13896
14087
|
ctx.result(result, (r) => renderSummary(r, `plx -y propagate --train ${r.train} ${dir}`));
|
|
13897
14088
|
if (!isClean(result)) process.exitCode = 1;
|
|
13898
14089
|
});
|
|
13899
14090
|
}
|
|
13900
14091
|
|
|
13901
14092
|
// src/commands/requirements.ts
|
|
13902
|
-
import { join as
|
|
13903
|
-
function
|
|
14093
|
+
import { join as join12, resolve as resolve9 } from "node:path";
|
|
14094
|
+
function register9(program2) {
|
|
13904
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) => {
|
|
13905
14096
|
const ctx = contextOf2(command);
|
|
13906
|
-
const compendium =
|
|
14097
|
+
const compendium = resolve9(dir);
|
|
13907
14098
|
if (opts.extract) {
|
|
13908
|
-
ctx.result(extractRequirements(await ctx.readFile(
|
|
14099
|
+
ctx.result(extractRequirements(await ctx.readFile(join12(compendium, opts.extract))));
|
|
13909
14100
|
return;
|
|
13910
14101
|
}
|
|
13911
14102
|
const check = await checkCompendium(ctx, compendium);
|
|
@@ -13920,13 +14111,14 @@ function renderCheck(c) {
|
|
|
13920
14111
|
|
|
13921
14112
|
// src/commands/index.ts
|
|
13922
14113
|
var commands = [
|
|
13923
|
-
register5,
|
|
13924
|
-
register4,
|
|
13925
14114
|
register6,
|
|
14115
|
+
register5,
|
|
13926
14116
|
register7,
|
|
13927
14117
|
register8,
|
|
14118
|
+
register9,
|
|
13928
14119
|
register2,
|
|
13929
14120
|
register,
|
|
14121
|
+
register4,
|
|
13930
14122
|
register3
|
|
13931
14123
|
];
|
|
13932
14124
|
|
|
@@ -13937,7 +14129,7 @@ function buildProgram() {
|
|
|
13937
14129
|
program2.addOption(new Option("-n, --dry-run", "print every effect this run would have, perform none").env("PLX_DRY_RUN")).addOption(
|
|
13938
14130
|
new Option("-v, --verbose", "say more: once for reads and inputs, twice for command output").argParser((_value, previous) => previous + 1).default(0)
|
|
13939
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"));
|
|
13940
|
-
for (const
|
|
14132
|
+
for (const register10 of commands) register10(program2);
|
|
13941
14133
|
return program2;
|
|
13942
14134
|
}
|
|
13943
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",
|
|
@@ -24,7 +24,7 @@
|
|
|
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": {
|