Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit 4d523ddbd8b70965bd15143334a3fff5b0a8c57b
parent a587f4b3b7f0d8868bc24e1382df14858360ff37
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Wed, 23 Sep 2026 08:58:26 -0400

ops cli: deploy-site, and the preview alias printed where a person sees it

The alias is already in the response JSON, but JSON is what a pipe consumes and
the URL is what a person needs — and under --wait it scrolls off behind minutes
of build log. So it is reprinted last, on stderr, which keeps `| jq` seeing
nothing but the response. Usage says what a preview IS rather than only that a
key exists, because an operator reaching for one is about to decide whether to
touch production.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Mscripts/archilyzer-ops.mjs | 49+++++++++++++++++++++++++++++++++++++++++++++++--
Mscripts/archilyzer-ops.test.mjs | 71+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
2 files changed, 114 insertions(+), 6 deletions(-)

diff --git a/scripts/archilyzer-ops.mjs b/scripts/archilyzer-ops.mjs @@ -33,6 +33,7 @@ // pnpm ops relocate --json '{"slugs":["x"],"locationId":"platter"}' // pnpm ops build-site --json '{"siteId":"anilyzer"}' --wait // pnpm ops build-deploy --json '{"siteIds":["anilyzer","jeralyzer"]}' --wait +// pnpm ops deploy-site --json '{"siteId":"anilyzer","preview":"tags-exclude"}' --wait // pnpm ops get channel the-quartering // pnpm ops tags --json '{"op":"define","tag":{"id":"eva-collab","label":"Collab"}}' // pnpm ops tag-videos --file ids.json @@ -93,6 +94,10 @@ const ACTIONS = [ "build-index", "build-deploy", "build-site", + // Deploy the ALREADY-BUILT export/out — build-deploy's other half, and the + // one a preview is for: build once, look at the preview, then ship the same + // bundle to production without rebuilding it. + "deploy-site", "relocate", "relocate-back", "evict-clips", @@ -233,6 +238,32 @@ export function parseArgs(argv) { }; } +// The preview alias(es) a response carries, in job order and de-duplicated. +// +// WHY IT IS REPRINTED AT ALL. The alias is already in the JSON, but the JSON is +// what a pipe consumes and the URL is what a person needs, and with --wait it +// scrolls off behind minutes of build log. Printed on stderr, not stdout, so +// `pnpm ops … | jq` still sees nothing but the response — the same split the +// --wait log lines already use. +export function previewUrlsIn(payload) { + const urls = []; + if (payload && Array.isArray(payload.jobs)) { + for (const job of payload.jobs) { + if (job && typeof job.previewUrl === "string") urls.push(job.previewUrl); + } + } + // Only as a fallback: the single-job case repeats jobs[0].previewUrl at the + // top level, and printing it twice would read as two previews. + if (urls.length === 0 && payload && typeof payload.previewUrl === "string") { + urls.push(payload.previewUrl); + } + return [...new Set(urls)]; +} + +function printPreviewUrls(payload) { + for (const url of previewUrlsIn(payload)) console.error(`preview: ${url}`); +} + export function usage() { return [ "Usage: pnpm ops <action> [--json '<body>' | --file <path>] [--wait]", @@ -249,7 +280,14 @@ export function usage() { "--wait-timeout <seconds> gives up and exits 1 instead of waiting forever.", " Default: no timeout — the queue may legitimately hold a job for hours.", "", - 'build-site and build-deploy both take "siteId" (one) or "siteIds" (a list).', + 'build-site, build-deploy and deploy-site all take "siteId" (one) or', + ' "siteIds" (a list).', + "", + '"preview": "<branch>" on deploy-site or build-deploy makes it a Cloudflare', + " Pages PREVIEW instead of production: the same bundle goes to a branch", + " alias, https://<branch>.<project>.pages.dev, and the live site is left", + " alone. The alias is printed after the response. Lowercase letters,", + ' digits and dashes, up to 28 characters; "main" is refused.', "", "Env: ARCHILYZER_EDITOR_URL (default http://localhost:3001), WORKER_TOKEN,", " ARCHILYZER_AGENT (provenance of a tag write; default \"cli\")", @@ -459,7 +497,10 @@ async function main() { } console.log(JSON.stringify(payload, null, 2)); if (!res.ok || payload.ok === false) return 1; - if (!parsed.wait) return 0; + if (!parsed.wait) { + printPreviewUrls(payload); + return 0; + } // `jobIds` FIRST: a bulk fan-out (relocate, relocate-back) returns an array // and also a single `jobId` when it started exactly one, so reading `jobId` // first would follow one job out of five. Before `jobIds` existed those @@ -474,6 +515,7 @@ async function main() { : []; if (jobIds.length === 0) { // Not a job-starting action (or it queued nothing). --wait is satisfied. + printPreviewUrls(payload); return 0; } let worst = 0; @@ -484,6 +526,9 @@ async function main() { console.error(`[${jobId}] ${status}`); if (status !== "done") worst = 1; } + // LAST, after the logs: with --wait the response scrolled off minutes ago, + // and the alias is the one thing the operator came for. + printPreviewUrls(payload); return worst; } diff --git a/scripts/archilyzer-ops.test.mjs b/scripts/archilyzer-ops.test.mjs @@ -5,7 +5,12 @@ // Run with: pnpm test:scripts import assert from "node:assert/strict"; import test from "node:test"; -import { followJob, parseArgs, usage } from "./archilyzer-ops.mjs"; +import { + followJob, + parseArgs, + previewUrlsIn, + usage, +} from "./archilyzer-ops.mjs"; test("no arguments prints usage", () => { assert.equal(parseArgs([]).help, true); @@ -214,9 +219,67 @@ test("--wait-timeout gives up on a job that never ends", async () => { }); // The two build routes used to disagree about the spelling of their one -// argument, so the usage text is where a reader finds out they no longer do. -test("usage says both build routes take siteId or siteIds", () => { - assert.match(usage(), /build-site and build-deploy both take "siteId".*"siteIds"/); +// argument, so the usage text is where a reader finds out they no longer do — +// and deploy-site, added later, is in the same sentence rather than a footnote. +test("usage says all three site routes take siteId or siteIds", () => { + assert.match( + usage(), + /build-site, build-deploy and deploy-site all take "siteId"[\s\S]*"siteIds"/, + ); +}); + +// A preview is the feature an operator reaches for BEFORE a production deploy, +// so the usage text has to say what it is, not merely that a key exists. +test("usage explains preview deploys and names deploy-site", () => { + const u = usage(); + assert.match(u, /Actions:.*deploy-site/); + assert.match(u, /"preview": "<branch>"/); + assert.match(u, /https:\/\/<branch>\.<project>\.pages\.dev/); + assert.match(u, /"main" is refused/); +}); + +test("deploy-site is a POST to its own route", () => { + const parsed = parseArgs([ + "deploy-site", + "--json", + '{"siteId":"anilyzer","preview":"tags-exclude"}', + ]); + assert.equal(parsed.method, "POST"); + assert.equal(parsed.path, "/api/ops/deploy-site"); + assert.deepEqual(parsed.body, { siteId: "anilyzer", preview: "tags-exclude" }); +}); + +test("previewUrlsIn reads the alias off each job", () => { + assert.deepEqual( + previewUrlsIn({ + ok: true, + jobs: [ + { siteId: "a", jobId: "j1", previewUrl: "https://p.a.pages.dev" }, + { siteId: "b", jobId: "j2", previewUrl: "https://p.b.pages.dev" }, + ], + }), + ["https://p.a.pages.dev", "https://p.b.pages.dev"], + ); +}); + +test("previewUrlsIn prints the single-job alias once, not twice", () => { + // The route repeats jobs[0].previewUrl at the top level so a one-site caller + // never indexes into `jobs`; printing both would read as two previews. + assert.deepEqual( + previewUrlsIn({ + ok: true, + jobs: [{ siteId: "a", jobId: "j1", previewUrl: "https://p.a.pages.dev" }], + jobId: "j1", + previewUrl: "https://p.a.pages.dev", + }), + ["https://p.a.pages.dev"], + ); +}); + +test("previewUrlsIn is empty for a production deploy", () => { + assert.deepEqual(previewUrlsIn({ ok: true, jobs: [{ siteId: "a", jobId: "j1" }] }), []); + assert.deepEqual(previewUrlsIn({ ok: true, jobId: "j1" }), []); + assert.deepEqual(previewUrlsIn(null), []); }); test("a recovered log endpoint answering 'running' is not an outcome", async () => {