@augurworks/augur 0.15.1
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/CANVAS.md +858 -0
- package/INSTALL.md +603 -0
- package/LICENSE +27 -0
- package/README.md +150 -0
- package/TRADEMARKS.md +72 -0
- package/agents/README.md +65 -0
- package/agents/canon-extract.md +117 -0
- package/agents/canon.md +151 -0
- package/agents/canvas.md +88 -0
- package/agents/currency.md +74 -0
- package/agents/drafts.md +92 -0
- package/agents/identity.md +148 -0
- package/agents/prototype-contract.md +69 -0
- package/agents/publishing.md +324 -0
- package/agents/review-feedback.md +76 -0
- package/agents/space-json.md +132 -0
- package/agents/ui-skill.md +101 -0
- package/agents/working-marks.md +86 -0
- package/brand/augur-eye.svg +3 -0
- package/brand/augur-icon.svg +41 -0
- package/brand/augur-mark.svg +5 -0
- package/brand/lentianova-bulky.otf +0 -0
- package/brand/lentianova-median.otf +0 -0
- package/brand/lentianova.woff2 +0 -0
- package/build.js +8409 -0
- package/changelog.md +133 -0
- package/fonts/inter-latin-wght-normal.woff2 +0 -0
- package/package.json +57 -0
- package/pitis/PETS.md +121 -0
- package/pitis/README.md +117 -0
- package/pitis/index.html +266 -0
- package/pitis/piti.build.js +81 -0
- package/pitis/piti.js +929 -0
- package/pitis/roast-agent.md +185 -0
- package/realtime/src/index.js +119 -0
- package/realtime/wrangler.example.toml +44 -0
- package/scripts/adopt.mjs +106 -0
- package/scripts/board-snapshot.mjs +197 -0
- package/scripts/build-seed-pack.mjs +33 -0
- package/scripts/bundle-rekey.mjs +112 -0
- package/scripts/bundle-tenancy-rehearsal.mjs +781 -0
- package/scripts/canon-extract.mjs +578 -0
- package/scripts/canon.mjs +599 -0
- package/scripts/canvas-screen.mjs +333 -0
- package/scripts/check-local.mjs +136 -0
- package/scripts/clawd-art.mjs +75 -0
- package/scripts/clawd-canvas.mjs +741 -0
- package/scripts/cli.mjs +100 -0
- package/scripts/clone.mjs +422 -0
- package/scripts/close.mjs +30 -0
- package/scripts/connect.mjs +103 -0
- package/scripts/deploy.mjs +157 -0
- package/scripts/dev.mjs +54 -0
- package/scripts/doc-lint.mjs +143 -0
- package/scripts/export.mjs +300 -0
- package/scripts/fork.mjs +64 -0
- package/scripts/freeze.mjs +74 -0
- package/scripts/frontdoor-parity.mjs +180 -0
- package/scripts/hook.mjs +67 -0
- package/scripts/identity-rekey.mjs +112 -0
- package/scripts/init.mjs +122 -0
- package/scripts/land.mjs +40 -0
- package/scripts/lib/adapters.mjs +156 -0
- package/scripts/lib/board-snapshot.mjs +511 -0
- package/scripts/lib/draft.mjs +385 -0
- package/scripts/lib/graduate.mjs +276 -0
- package/scripts/lib/instance.mjs +74 -0
- package/scripts/lib/marks.mjs +107 -0
- package/scripts/lib/materialize.mjs +138 -0
- package/scripts/lib/merge3.mjs +124 -0
- package/scripts/lib/offline-posture.mjs +120 -0
- package/scripts/lib/offline-respawn.mjs +18 -0
- package/scripts/lib/offline-wrangler.mjs +78 -0
- package/scripts/lib/publish-compose.mjs +12 -0
- package/scripts/lib/publish-conflict.mjs +78 -0
- package/scripts/lib/publish-evidence.mjs +144 -0
- package/scripts/lib/refine-compare.mjs +140 -0
- package/scripts/lib/refine-ledger.mjs +150 -0
- package/scripts/lib/refine-png.mjs +149 -0
- package/scripts/lib/seed-pack-build.mjs +141 -0
- package/scripts/lib/state-compare.mjs +68 -0
- package/scripts/lib/store.mjs +122 -0
- package/scripts/login.mjs +84 -0
- package/scripts/mark.mjs +112 -0
- package/scripts/migrate-board-keys.mjs +164 -0
- package/scripts/migrate.mjs +265 -0
- package/scripts/no-foreign-vocabulary.mjs +216 -0
- package/scripts/no-product-names.mjs +157 -0
- package/scripts/no-tenant-globals.mjs +1134 -0
- package/scripts/offline.mjs +247 -0
- package/scripts/og.mjs +170 -0
- package/scripts/one-tenant-resolver.mjs +146 -0
- package/scripts/open.mjs +63 -0
- package/scripts/publish.mjs +1041 -0
- package/scripts/read.mjs +31 -0
- package/scripts/refine.mjs +455 -0
- package/scripts/release-drift.mjs +138 -0
- package/scripts/restore.mjs +322 -0
- package/scripts/review.mjs +125 -0
- package/scripts/save.mjs +21 -0
- package/scripts/shell-lint.mjs +189 -0
- package/scripts/ship.mjs +460 -0
- package/scripts/shoot.mjs +264 -0
- package/scripts/smoke-invite.mjs +113 -0
- package/scripts/state-inventory.mjs +223 -0
- package/scripts/status.mjs +177 -0
- package/scripts/sync.mjs +27 -0
- package/scripts/tenant-do-rehearsal.mjs +1314 -0
- package/scripts/ui-copy-lint.mjs +202 -0
- package/scripts/watch.mjs +32 -0
- package/scripts/wrangler-preflight.mjs +246 -0
- package/seed/CANON.md +43 -0
- package/seed/README.md +119 -0
- package/seed/prototype-status.json +9 -0
- package/seed/registry.json +88 -0
- package/seed/skills/starter-ui/SKILL.md +52 -0
- package/seed/skills/starter-ui/skill.json +11 -0
- package/seed/skills/starter-ui/starter-tokens.css +73 -0
- package/seed/skills/starter-ui/starter-ui.css +318 -0
- package/seed/skills/starter-ui/starter-ui.js +50 -0
- package/seed/space.json +5 -0
- package/seed/start-here/prototypes/connect-your-terminal/index.html +428 -0
- package/seed/start-here/prototypes/connect-your-terminal/preview.webp +0 -0
- package/seed/start-here/prototypes/sample-with-comments/index.html +172 -0
- package/seed/start-here/prototypes/sample-with-comments/preview.webp +0 -0
- package/seed/start-here/prototypes/set-up-your-design-system/index.html +353 -0
- package/seed/start-here/prototypes/set-up-your-design-system/preview.webp +0 -0
- package/seed/threads.json +52 -0
- package/seed/worked-examples/prototypes/field-readings/index.html +273 -0
- package/seed/worked-examples/prototypes/field-readings/preview.webp +0 -0
- package/seed/worked-examples/prototypes/slide-deck/index.html +214 -0
- package/seed/worked-examples/prototypes/slide-deck/preview.webp +0 -0
- package/seed/worked-examples/prototypes/specimen-viewer/index.html +203 -0
- package/seed/worked-examples/prototypes/specimen-viewer/preview.webp +0 -0
- package/src/_worker.js +12668 -0
- package/src/board-key.mjs +57 -0
- package/src/board-room.mjs +881 -0
- package/src/bundle-keys.mjs +138 -0
- package/src/canon/collect-in-browser.js +215 -0
- package/src/canon/emit.mjs +232 -0
- package/src/canon/observe.mjs +260 -0
- package/src/canon/schema.mjs +451 -0
- package/src/canvas/DSEG-LICENSE.txt +95 -0
- package/src/canvas/DSEG7Classic-Bold.woff2 +0 -0
- package/src/canvas/canvas.css +885 -0
- package/src/canvas/canvas.js +5780 -0
- package/src/canvas/capture.js +485 -0
- package/src/chrome/appchrome.mjs +568 -0
- package/src/currency.mjs +203 -0
- package/src/delete-confirmation.mjs +254 -0
- package/src/drafts/drafts.js +326 -0
- package/src/entry.js +58 -0
- package/src/galleries.mjs +400 -0
- package/src/health-cron.mjs +173 -0
- package/src/identity.json +1 -0
- package/src/kv-codec.mjs +135 -0
- package/src/kv-identity.mjs +259 -0
- package/src/mail.mjs +599 -0
- package/src/provenance.mjs +74 -0
- package/src/publish-compose.mjs +363 -0
- package/src/publish-fork.mjs +221 -0
- package/src/publish-units.mjs +44 -0
- package/src/purge.mjs +81 -0
- package/src/review/COMMENTING-UX.md +306 -0
- package/src/review/cat.png +0 -0
- package/src/review/comment-cursor.svg +7 -0
- package/src/review/comments.js +1738 -0
- package/src/room-ticket.mjs +96 -0
- package/src/roster-chip.mjs +32 -0
- package/src/seed-pack.mjs +261 -0
- package/src/state-inventory.mjs +249 -0
- package/src/sw-logic.mjs +48 -0
- package/src/tenant-cache.mjs +141 -0
- package/src/tenant-context.mjs +386 -0
- package/src/tenant-do.js +2940 -0
- package/src/tenant-host.mjs +144 -0
- package/src/tenant-quotas.mjs +140 -0
- package/src/unit-core.mjs +103 -0
- package/src/unit-object.mjs +338 -0
- package/templates/README.md +67 -0
- package/templates/shell/deploy.yml +145 -0
- package/templates/shell/engine-bump.yml +142 -0
- package/templates/shell/health.yml +233 -0
- package/templates/shell/kv-backup.yml +201 -0
- package/templates/shell/roster-update.yml +110 -0
- package/templates/shell/space-preflight.yml +35 -0
- package/templates/shell/space-rebake.yml +79 -0
- package/templates/shell/store-backup.yml +152 -0
- package/templates/shell/wrangler.example.toml +266 -0
- package/templates/space/publish.yml +91 -0
package/scripts/fork.mjs
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* augur fork — take your own copy of a published artifact, at a new URL.
|
|
4
|
+
*
|
|
5
|
+
* augur fork /toolkit/map/ /toolkit/map-mine/
|
|
6
|
+
* augur fork /toolkit/map/ /toolkit/map-mine/ --space alpha
|
|
7
|
+
*
|
|
8
|
+
* `F-fork-verb`. Divergence-then-convergence needs fork as a deliberate verb rather than as
|
|
9
|
+
* conflict cleanup or as "copy the folder and republish it". The copy is made in the
|
|
10
|
+
* MANIFEST — the blobs already exist, so a hundred-file prototype forks without uploading,
|
|
11
|
+
* downloading or hashing a byte — and the store remembers where it came from.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ IT NEEDS NO TREE, and that is the point. Every other publishing command starts from a
|
|
14
|
+
* folder on disk; this one is two paths and a token, so a workspace that has never had a
|
|
15
|
+
* repo can still say "give me my own copy of this". Nothing is written to your working
|
|
16
|
+
* directory, and nothing is read from it.
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ THE COPY IS YOURS. It is stamped with you as its owner rather than inheriting the
|
|
19
|
+
* original's, so forking to get an editable copy cannot hand the fork the same restriction
|
|
20
|
+
* it was forked to escape. Lineage rides along as `forkedFrom {path, version}` naming a
|
|
21
|
+
* manifest version the store still holds — versions are never pruned, so the parent stays
|
|
22
|
+
* reachable even after the source moves on.
|
|
23
|
+
*/
|
|
24
|
+
import { target, apiClient, buildStamp, idsFromStamp } from "./lib/store.mjs";
|
|
25
|
+
|
|
26
|
+
const log = (m) => console.error(`\x1b[36m[fork]\x1b[0m ${m}`);
|
|
27
|
+
const die = (m) => { log(m); process.exit(1); };
|
|
28
|
+
|
|
29
|
+
const args = process.argv.slice(2);
|
|
30
|
+
const opt = (f) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : null; };
|
|
31
|
+
const positional = args.filter((a, i) => !a.startsWith("--") && args[i - 1] !== "--space");
|
|
32
|
+
const [FROM, TO] = positional;
|
|
33
|
+
if (!FROM || !TO) die("usage: augur fork <from-path> <to-path> [--space <id>]");
|
|
34
|
+
|
|
35
|
+
let origin, token;
|
|
36
|
+
try { ({ origin, token } = target()); } catch (e) { die(e.message); }
|
|
37
|
+
const req = apiClient(origin, token);
|
|
38
|
+
|
|
39
|
+
// Which workspace. Named, or the one the instance serves — asked of the public build stamp
|
|
40
|
+
// rather than of a folder, because a forking publisher may have no folder.
|
|
41
|
+
let space = opt("--space");
|
|
42
|
+
if (!space) {
|
|
43
|
+
const ids = idsFromStamp(await buildStamp(origin)).filter((id) => id !== "_engine");
|
|
44
|
+
if (ids.length !== 1) die(`name the workspace: --space <${ids.join("|") || "id"}>`);
|
|
45
|
+
space = ids[0];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let res;
|
|
49
|
+
try {
|
|
50
|
+
res = await (await req(`${space}/fork`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "content-type": "application/json" },
|
|
53
|
+
body: JSON.stringify({ from: FROM, to: TO }),
|
|
54
|
+
})).json();
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// The store's refusals are specific on purpose, so pass its words through rather than
|
|
57
|
+
// flattening them into "fork failed".
|
|
58
|
+
die(e.message.replace(/^POST \S+ → /, ""));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
log(`\x1b[32mforked\x1b[0m ${res.from} → ${res.to} (v${res.version})`);
|
|
62
|
+
console.log(`${origin}${res.to}`);
|
|
63
|
+
console.log(`\x1b[2m${res.files} file(s) aliased, ${res.blobsUploaded} blob(s) uploaded — `
|
|
64
|
+
+ `forked from ${res.forkedFrom.path} at v${res.forkedFrom.version}\x1b[0m`);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* augur freeze — make a workspace read-only while it is being moved.
|
|
4
|
+
*
|
|
5
|
+
* augur freeze [--reason "…"] stop accepting writes
|
|
6
|
+
* augur freeze --status is it frozen, and since when
|
|
7
|
+
* augur thaw accept writes again, and print how long it lasted
|
|
8
|
+
*
|
|
9
|
+
* `MIG-cutover-freeze`. Moving a workspace is export → verify → cut the hostname over, and
|
|
10
|
+
* anything written to the OLD instance inside that window goes to a copy nobody will ever
|
|
11
|
+
* read again. Not lost noisily — lost the way a comment is lost when somebody posts it,
|
|
12
|
+
* watches it appear, and comes back tomorrow to a page that never had it.
|
|
13
|
+
*
|
|
14
|
+
* ONLY WRITES STOP. The other way to do this is pulling the route or the DNS record, which
|
|
15
|
+
* is simpler and takes READS down too: on a real workspace the copy and the verification
|
|
16
|
+
* are minutes, and minutes of dark site looks like an outage to everybody who is not
|
|
17
|
+
* migrating. Here the site stays up, a reader sees what was there, and somebody who tries
|
|
18
|
+
* to change something is told rather than quietly ignored. Signing in still works, because
|
|
19
|
+
* whoever is watching the migration has to be able to get in and look.
|
|
20
|
+
*
|
|
21
|
+
* THE DURATION IS THE POINT OF `thaw` PRINTING IT. A migration has to publish how long its
|
|
22
|
+
* window actually was — an instance being moved for somebody else has people planning
|
|
23
|
+
* around that number, and "about ten minutes" from memory is not a number.
|
|
24
|
+
*/
|
|
25
|
+
import { target, apiClient } from "./lib/store.mjs";
|
|
26
|
+
|
|
27
|
+
const log = (m) => console.error(`\x1b[36m[freeze]\x1b[0m ${m}`);
|
|
28
|
+
const die = (m) => { log(m); process.exit(1); };
|
|
29
|
+
|
|
30
|
+
const args = process.argv.slice(2);
|
|
31
|
+
const flag = (f) => args.includes(f);
|
|
32
|
+
const opt = (f) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : null; };
|
|
33
|
+
// `augur thaw` and `augur freeze --thaw` are the same thing; the CLI exposes both because
|
|
34
|
+
// one of them is what somebody types under pressure.
|
|
35
|
+
const THAW = flag("--thaw") || /thaw/.test(process.env.AUGUR_CMD || "") || args[0] === "thaw";
|
|
36
|
+
const STATUS = flag("--status");
|
|
37
|
+
|
|
38
|
+
let origin, token;
|
|
39
|
+
try { ({ origin, token } = target()); } catch (e) { die(e.message); }
|
|
40
|
+
const req = apiClient(origin, token);
|
|
41
|
+
|
|
42
|
+
const fmt = (ms) => {
|
|
43
|
+
if (ms == null) return "unknown";
|
|
44
|
+
const s = Math.round(ms / 1000);
|
|
45
|
+
return s < 90 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
if (STATUS) {
|
|
49
|
+
const { freeze } = await (await req("_state/freeze")).json();
|
|
50
|
+
if (!freeze) { console.log(`${origin} accepting writes`); process.exit(0); }
|
|
51
|
+
console.log(`${origin} FROZEN since ${freeze.at} — ${freeze.reason}`
|
|
52
|
+
+ ` (${fmt(Date.now() - Date.parse(freeze.at))} so far)`);
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const res = await (await req("_state/freeze", {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "content-type": "application/json" },
|
|
59
|
+
body: JSON.stringify(THAW
|
|
60
|
+
? { thaw: true }
|
|
61
|
+
: { reason: opt("--reason") || "being moved to a new home" }),
|
|
62
|
+
})).json();
|
|
63
|
+
|
|
64
|
+
if (!res.ok) die(`${origin} refused: ${res.reason}`);
|
|
65
|
+
|
|
66
|
+
if (THAW) {
|
|
67
|
+
log(`\x1b[32mthawed\x1b[0m — writes are accepted again`);
|
|
68
|
+
console.log(`${origin} frozen for ${fmt(res.durationMs)}`);
|
|
69
|
+
console.log("\x1b[2mpublish that number with the migration — somebody planned around it\x1b[0m");
|
|
70
|
+
} else {
|
|
71
|
+
log(`\x1b[33mfrozen\x1b[0m since ${res.since} — ${res.reason}`);
|
|
72
|
+
console.log(`${origin} reads unaffected; every write is refused with a 503 that says why`);
|
|
73
|
+
console.log("\x1b[2mrun `augur thaw` when the cutover is done — nothing lifts this on its own\x1b[0m");
|
|
74
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* frontdoor-parity — ask a RUNNING deployment whether a gated path is gated.
|
|
4
|
+
*
|
|
5
|
+
* WHY. Nothing in this engine or any shell checks that. The shells' health canary curls
|
|
6
|
+
* /_build.json, which is a static file: a bare host with no worker running at all answers
|
|
7
|
+
* it correctly and reports "healthy". Every test in the suite drives the worker in
|
|
8
|
+
* process, so none of them can observe how the platform in front of it routes.
|
|
9
|
+
*
|
|
10
|
+
* That gap is survivable on Pages, where the worker runs first for every request by
|
|
11
|
+
* construction. It stops being survivable the moment an instance moves to a plain Worker,
|
|
12
|
+
* because there the platform serves a matching static asset FIRST unless
|
|
13
|
+
* `run_worker_first = true` — and the asset directory holds __config/instance.json, which
|
|
14
|
+
* carries the roster including seed passwords.
|
|
15
|
+
*
|
|
16
|
+
* THE DISCRIMINATOR, and it is the whole idea: ask for a file that EXISTS in the asset
|
|
17
|
+
* directory and is not meant to be public, then check the response is not that file. On a
|
|
18
|
+
* healthy instance /tenant-context.mjs answers with the gate's HTML, because the worker
|
|
19
|
+
* ran and decided. On a broken one it answers with `export const …`, because the platform
|
|
20
|
+
* served the file before the worker was ever invoked. Status codes cannot tell those
|
|
21
|
+
* apart — both are 200.
|
|
22
|
+
*
|
|
23
|
+
* WHAT IT CANNOT DO. It is a black-box prober: it can only see what a stranger sees. It
|
|
24
|
+
* says which checks it could not discriminate rather than counting them as passes — a
|
|
25
|
+
* gated instance answers every unknown path with the login page at 200, so the
|
|
26
|
+
* not-found check has nothing to compare against there and says so.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* node scripts/frontdoor-parity.mjs <origin> assert the matrix
|
|
30
|
+
* node scripts/frontdoor-parity.mjs <origin-a> <origin-b> assert both, then diff them
|
|
31
|
+
*
|
|
32
|
+
* Exit 1 on any failure. Exit 2 on a usage or network error, which is not a pass.
|
|
33
|
+
*/
|
|
34
|
+
import crypto from "node:crypto";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Bodies are not byte-stable across two requests for the same page, and the reason is not
|
|
38
|
+
* in this repo: Cloudflare's Email Address Obfuscation rewrites any address it finds in
|
|
39
|
+
* HTML into `<a data-cfemail="…">`, with a FRESH random key each response. Comparing raw
|
|
40
|
+
* bytes therefore reported every HTML path as a difference — including two fetches of one
|
|
41
|
+
* origin — which would have made the parity check pure noise on its first real use.
|
|
42
|
+
*
|
|
43
|
+
* Normalise it out, and nothing else. A comparator that strips more than it must is a
|
|
44
|
+
* comparator that stops noticing the difference it exists to find.
|
|
45
|
+
*/
|
|
46
|
+
function normalise(body) {
|
|
47
|
+
return body
|
|
48
|
+
.replace(/data-cfemail="[0-9a-f]+"/gi, 'data-cfemail=""')
|
|
49
|
+
.replace(/\/cdn-cgi\/l\/email-protection#[0-9a-f]+/gi, "/cdn-cgi/l/email-protection#");
|
|
50
|
+
}
|
|
51
|
+
const sha = (s) => crypto.createHash("sha256").update(normalise(s)).digest("hex").slice(0, 12);
|
|
52
|
+
const isHtml = (b) => /^\s*<!doctype html/i.test(b);
|
|
53
|
+
const looksLikeLoginPage = (b) => isHtml(b) && /type=["']password["']/i.test(b);
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* SEALED: the path exists as a file in dist/ and must never come back as that file.
|
|
57
|
+
* `leaks` returns a reason when the body IS the file, i.e. when the worker did not run.
|
|
58
|
+
*/
|
|
59
|
+
const SEALED = [
|
|
60
|
+
["/__config/instance.json", (b) => /"users"\s*:/.test(b) && "the instance roster (this file carries seed passwords)"],
|
|
61
|
+
["/__config/routing.json", (b) => /"publicPrefixes"\s*:/.test(b) && "the routing document"],
|
|
62
|
+
["/__manifests/_engine.json", (b) => /"files"\s*:/.test(b) && "an engine manifest"],
|
|
63
|
+
["/_worker.js", (b) => /export\s+default|addEventListener\s*\(/.test(b) && "the worker source"],
|
|
64
|
+
["/tenant-context.mjs", (b) => /export\s+(const|function)/.test(b) && "the tenant-context module"],
|
|
65
|
+
["/tenant-cache.mjs", (b) => /export\s+(const|function)/.test(b) && "the tenant-cache module"],
|
|
66
|
+
["/kv-codec.mjs", (b) => /export\s+(const|function)/.test(b) && "the KV codec module"],
|
|
67
|
+
["/mail.mjs", (b) => /export\s+(const|function)/.test(b) && "the mail transport"],
|
|
68
|
+
["/chrome/appchrome.mjs", (b) => /export\s+(const|function)/.test(b) && "the chrome renderer"],
|
|
69
|
+
["/.assetsignore", (b) => /^_worker\.js/m.test(b) && "the deploy ignore list"],
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* ALIVE: must be the real thing. Without these the matrix would pass on a deployment that
|
|
74
|
+
* answered everything with a 404 — sealed, and also completely broken.
|
|
75
|
+
*/
|
|
76
|
+
const ALIVE = [
|
|
77
|
+
["/_build.json", (b) => { try { return !!JSON.parse(b).engine.sha; } catch { return false; } }, "a build stamp naming an engine sha"],
|
|
78
|
+
["/sw.js", (b) => /service worker/i.test(b) || /addEventListener/.test(b), "the service worker script"],
|
|
79
|
+
["/", (b) => isHtml(b), "an HTML page"],
|
|
80
|
+
];
|
|
81
|
+
|
|
82
|
+
const BOGUS = "/__frontdoor-parity-probe-no-such-path";
|
|
83
|
+
|
|
84
|
+
async function get(origin, path, method = "GET") {
|
|
85
|
+
const url = origin.replace(/\/$/, "") + path;
|
|
86
|
+
const res = await fetch(url, { method, redirect: "manual", headers: { "user-agent": "augur-frontdoor-parity" } });
|
|
87
|
+
const body = method === "HEAD" ? "" : await res.text();
|
|
88
|
+
return { status: res.status, ct: (res.headers.get("content-type") || "").split(";")[0].trim(), body, url };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function probe(origin) {
|
|
92
|
+
const results = [];
|
|
93
|
+
const fail = (name, detail) => results.push({ ok: false, name, detail });
|
|
94
|
+
const pass = (name, detail) => results.push({ ok: true, name, detail });
|
|
95
|
+
const skip = (name, detail) => results.push({ skip: true, name, detail });
|
|
96
|
+
|
|
97
|
+
for (const [path, leaks] of SEALED) {
|
|
98
|
+
const r = await get(origin, path);
|
|
99
|
+
const why = leaks(r.body);
|
|
100
|
+
if (why) fail(`sealed ${path}`, `answered with ${why} (${r.status} ${r.ct}, ${r.body.length}B). The worker did not decide this request.`);
|
|
101
|
+
else pass(`sealed ${path}`, `${r.status} ${r.ct || "-"} — not the file`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
for (const [path, ok, expected] of ALIVE) {
|
|
105
|
+
const r = await get(origin, path);
|
|
106
|
+
if (r.status !== 200 || !ok(r.body)) fail(`alive ${path}`, `expected ${expected}, got ${r.status} ${r.ct} (${r.body.length}B)`);
|
|
107
|
+
else pass(`alive ${path}`, `${r.status} ${r.ct || "-"}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// not_found_handling. A gated instance answers every unknown path with the login page,
|
|
111
|
+
// and its root is that same page, so there is nothing here to discriminate — say so
|
|
112
|
+
// rather than counting it.
|
|
113
|
+
const root = await get(origin, "/");
|
|
114
|
+
const bogus = await get(origin, BOGUS);
|
|
115
|
+
if (looksLikeLoginPage(root.body)) {
|
|
116
|
+
skip("not-found", "this instance gates its root, so an unknown path and the root are the same page by design");
|
|
117
|
+
} else if (bogus.status === 200 && sha(bogus.body) === sha(root.body)) {
|
|
118
|
+
fail("not-found", `an unknown path returned the ROOT PAGE at 200. That is not_found_handling = "single-page-application"; every typo and every unpublished URL now looks real.`);
|
|
119
|
+
} else {
|
|
120
|
+
pass("not-found", `${bogus.status} and not the root page`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// A HEAD must agree with the GET about whether the thing exists.
|
|
124
|
+
const headBuild = await get(origin, "/_build.json", "HEAD");
|
|
125
|
+
if (headBuild.status !== 200) fail("HEAD /_build.json", `${headBuild.status}, but GET answers 200`);
|
|
126
|
+
else pass("HEAD /_build.json", "200");
|
|
127
|
+
|
|
128
|
+
return results;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function main() {
|
|
132
|
+
const origins = process.argv.slice(2).filter((a) => !a.startsWith("-"));
|
|
133
|
+
if (!origins.length) {
|
|
134
|
+
console.error("usage: node scripts/frontdoor-parity.mjs <origin> [origin-b]");
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let failures = 0;
|
|
139
|
+
const bodies = [];
|
|
140
|
+
for (const origin of origins) {
|
|
141
|
+
console.log(`\n${origin}`);
|
|
142
|
+
let results;
|
|
143
|
+
try { results = await probe(origin); }
|
|
144
|
+
catch (e) { console.log(` ERROR ${e.message}`); process.exit(2); }
|
|
145
|
+
for (const r of results) {
|
|
146
|
+
if (r.skip) console.log(` skip ${r.name} — ${r.detail}`);
|
|
147
|
+
else if (r.ok) console.log(` ok ${r.name} — ${r.detail}`);
|
|
148
|
+
else { console.log(` FAIL ${r.name}`); console.log(` ${r.detail}`); failures++; }
|
|
149
|
+
}
|
|
150
|
+
bodies.push({ origin, results });
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Parity. Two front doors onto ONE instance must answer identically; that is what makes
|
|
154
|
+
// a cutover checkable rather than hoped-for.
|
|
155
|
+
if (origins.length === 2) {
|
|
156
|
+
console.log(`\nparity ${origins[0]} vs ${origins[1]}`);
|
|
157
|
+
const paths = ["/_build.json", "/sw.js", "/", ...SEALED.map(([p]) => p)];
|
|
158
|
+
for (const p of paths) {
|
|
159
|
+
const [a, b] = await Promise.all(origins.map((o) => get(o, p)));
|
|
160
|
+
// /_build.json carries a timestamp, so compare the engine sha rather than the bytes.
|
|
161
|
+
if (p === "/_build.json") {
|
|
162
|
+
const s = (x) => { try { return JSON.parse(x.body).engine.sha; } catch { return null; } };
|
|
163
|
+
if (s(a) !== s(b)) { console.log(` FAIL ${p} engine sha differs: ${s(a)} vs ${s(b)}`); failures++; }
|
|
164
|
+
else console.log(` ok ${p} same engine sha`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (a.status !== b.status || sha(a.body) !== sha(b.body)) {
|
|
168
|
+
console.log(` FAIL ${p} ${a.status}/${sha(a.body)} vs ${b.status}/${sha(b.body)}`);
|
|
169
|
+
failures++;
|
|
170
|
+
} else console.log(` ok ${p} ${a.status} identical`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
console.log(failures
|
|
175
|
+
? `\nfrontdoor-parity: ${failures} FAILURE(S). A 'sealed' failure means the platform answered before the worker ran.`
|
|
176
|
+
: `\nfrontdoor-parity: OK — every sealed path was decided by the worker, and the site is actually serving`);
|
|
177
|
+
process.exit(failures ? 1 : 0);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
main();
|
package/scripts/hook.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// augur hook <pre|post|install|remove|status>
|
|
3
|
+
//
|
|
4
|
+
// `pre` and `post` are what an agent tool runs around every file edit, with the tool's own
|
|
5
|
+
// JSON on stdin (docs/drafts-that-land.md §7). Both answer with an exit code: 0 lets the
|
|
6
|
+
// edit through, 2 refuses it and puts one sentence on stderr for the agent to read in its
|
|
7
|
+
// next tool result. NOTHING ELSE EXITS NON-ZERO: an unreadable payload, a missing
|
|
8
|
+
// registry, an exception — every accident exits 0 and says nothing, because the hook is
|
|
9
|
+
// installed machine-wide and a bug here would take every editor down with it.
|
|
10
|
+
//
|
|
11
|
+
// pre refuse a write into a read-only copy, or into a shared checkout's prototype
|
|
12
|
+
// post save the draft the edited file belongs to; refused saves fail with the reason
|
|
13
|
+
//
|
|
14
|
+
// `install|remove|status` manage the adapters — `augur open` installs them once.
|
|
15
|
+
import fs from "node:fs";
|
|
16
|
+
import { ADAPTERS, denyDecision, saveDecision, installAdapters, removeAdapters } from "./lib/adapters.mjs";
|
|
17
|
+
import { registryList, readState, unitClient, doSave } from "./lib/draft.mjs";
|
|
18
|
+
import { resolveToken } from "./lib/store.mjs";
|
|
19
|
+
|
|
20
|
+
const event = process.argv[2];
|
|
21
|
+
const refuse = (m) => { process.stderr.write(m + "\n"); process.exit(2); };
|
|
22
|
+
|
|
23
|
+
if (event === "install" || event === "remove" || event === "status") {
|
|
24
|
+
const rows = event === "install" ? installAdapters() : event === "remove" ? removeAdapters() : installAdapters({ dryRun: true });
|
|
25
|
+
for (const r of rows) {
|
|
26
|
+
const verdict = event === "status" ? (r.result === "unchanged" ? "installed" : r.result === "absent" ? "absent" : "not installed") : r.result;
|
|
27
|
+
console.log(`${r.name}: ${verdict}${r.path ? ` (${r.path})` : ""}`);
|
|
28
|
+
}
|
|
29
|
+
process.exit(0);
|
|
30
|
+
}
|
|
31
|
+
if (event !== "pre" && event !== "post") {
|
|
32
|
+
console.error("usage: augur hook <pre|post|install|remove|status>");
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
if (process.env.AUGUR_HOOKS_OFF) process.exit(0);
|
|
36
|
+
|
|
37
|
+
let payload = null;
|
|
38
|
+
try { payload = JSON.parse(fs.readFileSync(0, "utf8")); } catch (e) { process.exit(0); }
|
|
39
|
+
const adapter = ADAPTERS.find((a) => a.id === (process.env.AUGUR_ADAPTER || ADAPTERS[0].id)) || ADAPTERS[0];
|
|
40
|
+
let p;
|
|
41
|
+
try { p = adapter.payload(payload); } catch (e) { process.exit(0); }
|
|
42
|
+
let registry = [];
|
|
43
|
+
try { registry = registryList(); } catch (e) { registry = []; }
|
|
44
|
+
|
|
45
|
+
if (event === "pre") {
|
|
46
|
+
let d;
|
|
47
|
+
try { d = denyDecision({ filePath: p.filePath, cwd: p.cwd, registry }); } catch (e) { process.exit(0); }
|
|
48
|
+
if (d && d.deny) refuse(d.reason);
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// post
|
|
53
|
+
let target;
|
|
54
|
+
try { target = saveDecision({ filePath: p.filePath, cwd: p.cwd, registry }); } catch (e) { process.exit(0); }
|
|
55
|
+
if (!target) process.exit(0);
|
|
56
|
+
const st = readState(target.dir);
|
|
57
|
+
if (!st) process.exit(0);
|
|
58
|
+
const origin = st.origin;
|
|
59
|
+
// `AUGUR_TOKEN` set — even to nothing — is the answer; otherwise the saved tokens.
|
|
60
|
+
const token = process.env.AUGUR_TOKEN !== undefined ? process.env.AUGUR_TOKEN : resolveToken(origin);
|
|
61
|
+
if (!token) refuse(`draft ${st.draftId} not saved: no publish token for ${origin} — run augur connect once, then save with augur save.`);
|
|
62
|
+
const client = unitClient({ origin, token, space: st.space, session: st.session });
|
|
63
|
+
const r = await doSave({ client, dir: target.dir });
|
|
64
|
+
if (r.ok) process.exit(0);
|
|
65
|
+
if (r.error === "stale-draft" || r.error === "stale-draft-revision") refuse(`draft ${st.draftId} not saved: it moved under you (another process saved to it) — run augur sync, then augur save.`);
|
|
66
|
+
if (r.error === "network") refuse(`draft ${st.draftId} not saved: ${origin} is unreachable (${r.message}). Nothing is lost — the next save carries every change since.`);
|
|
67
|
+
refuse(`draft ${st.draftId} not saved: ${r.error || r.status}${r.reason ? ` (${r.reason})` : ""}. Fix it and run augur save.`);
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* augur identity-rekey — move a live workspace's IDENTITY documents onto the workspace
|
|
4
|
+
* segment, without an account credential.
|
|
5
|
+
*
|
|
6
|
+
* augur identity-rekey what WOULD move (a dry run; the default)
|
|
7
|
+
* augur identity-rekey --confirm <workspace> move it
|
|
8
|
+
* augur identity-rekey --confirm <ws> --families roster,publishTokens
|
|
9
|
+
*
|
|
10
|
+
* `B-identity-kv-write-segmentation`. The roster, the roles, the display names, the
|
|
11
|
+
* avatars, the icons, the invites, the last-seen stamps and the publish tokens now carry
|
|
12
|
+
* the workspace on a deployment that resolves one from the Host (`identityKey` in
|
|
13
|
+
* src/_worker.js). A deployment that starts WRITING `t/<workspace>/users:roster` does not
|
|
14
|
+
* thereby start reading what it wrote yesterday, and there is deliberately no read-through
|
|
15
|
+
* to the unsegmented key: it belongs to whichever workspace this deployment served before
|
|
16
|
+
* the segment existed, and nothing in the key says which. So a live workspace is MOVED,
|
|
17
|
+
* and this is the command that moves it.
|
|
18
|
+
*
|
|
19
|
+
* ⚠️ THE TWIN OF `augur bundle-rekey`, AND A SEPARATE COMMAND ON PURPOSE. That one asks
|
|
20
|
+
* "is my published content where I read it"; this one asks "is my roster". An operator
|
|
21
|
+
* moving content must not silently move the login gate's documents in the same breath,
|
|
22
|
+
* and the two have different things to check afterwards.
|
|
23
|
+
*
|
|
24
|
+
* ⚠️ IT IS A COPY AND NEVER A CUT, and here that matters more than it does for content:
|
|
25
|
+
* the unsegmented documents are what a per-family revert READS. Flip one word in
|
|
26
|
+
* `IDENTITY_TENANCY` back with the sources deleted and the revert is a rollback to the day
|
|
27
|
+
* of the cut, with everybody minted since gone.
|
|
28
|
+
*
|
|
29
|
+
* ⛔ `users:secrets` DOES NOT MOVE, because it is not segmented. A credential is
|
|
30
|
+
* account-level — one address, one password, several workspaces — and it belongs to
|
|
31
|
+
* `B-cross-workspace-signin`.
|
|
32
|
+
*
|
|
33
|
+
* ⚠️ IT IS CORRECT FOR EXACTLY ONE WORKSPACE PER DEPLOYMENT, and the worker refuses rather
|
|
34
|
+
* than trusting this script about it: a deployment already holding a second workspace
|
|
35
|
+
* answers `not-the-only-workspace`.
|
|
36
|
+
*
|
|
37
|
+
* RUN IT BEHIND A FREEZE (`augur freeze`). The `_state/*` routes are exempt from the
|
|
38
|
+
* freeze, so the move runs with no identity write racing it; reads and sign-in are
|
|
39
|
+
* unaffected, so the site stays up.
|
|
40
|
+
*
|
|
41
|
+
* ⚠️ AFTERWARDS, GIVE KV A MINUTE BEFORE BELIEVING A READ. A `kv.get` through the worker
|
|
42
|
+
* is cached for up to sixty seconds, so a key written by this command can answer as
|
|
43
|
+
* missing for that long — which reads exactly like a broken move. Re-check past the minute
|
|
44
|
+
* before concluding anything, or read the namespace directly.
|
|
45
|
+
*
|
|
46
|
+
* Exit 0 when the move is complete (or was already), 1 on a refusal.
|
|
47
|
+
*/
|
|
48
|
+
import { target, apiClient } from "./lib/store.mjs";
|
|
49
|
+
|
|
50
|
+
const log = (m) => console.error(`\x1b[36m[identity-rekey]\x1b[0m ${m}`);
|
|
51
|
+
const die = (m) => { log(m); process.exit(1); };
|
|
52
|
+
|
|
53
|
+
const args = process.argv.slice(2);
|
|
54
|
+
const opt = (f) => { const i = args.indexOf(f); return i >= 0 ? args[i + 1] : null; };
|
|
55
|
+
const CONFIRM = opt("--confirm");
|
|
56
|
+
const FAMILIES = (opt("--families") || "").split(",").map((s) => s.trim()).filter(Boolean);
|
|
57
|
+
const MAX_PAGES = Number(opt("--max-pages")) > 0 ? Number(opt("--max-pages")) : 200;
|
|
58
|
+
|
|
59
|
+
let origin, token;
|
|
60
|
+
try { ({ origin, token } = target()); } catch (e) { die(e.message); }
|
|
61
|
+
const req = apiClient(origin, token);
|
|
62
|
+
|
|
63
|
+
const call = async (body) => {
|
|
64
|
+
const res = await req("_state/identity-rekey", {
|
|
65
|
+
method: "POST",
|
|
66
|
+
headers: { "content-type": "application/json" },
|
|
67
|
+
body: JSON.stringify(body),
|
|
68
|
+
});
|
|
69
|
+
let out;
|
|
70
|
+
try { out = await res.json(); } catch (e) { die(`${origin} answered ${res.status} with something that is not JSON`); }
|
|
71
|
+
if (!out.ok) {
|
|
72
|
+
if (out.reason === "not-the-only-workspace") {
|
|
73
|
+
die(`${origin} refused: this deployment already holds another workspace (${(out.others || []).join(", ")}).`
|
|
74
|
+
+ ` An unsegmented identity document there belongs to nobody in particular, so nothing may claim it.`);
|
|
75
|
+
}
|
|
76
|
+
if (out.reason === "confirm-mismatch") die(`${origin} refused: --confirm must be "${out.expected}"`);
|
|
77
|
+
if (out.reason === "unknown-family") die(`${origin} refused: no such family (${(out.unknown || []).join(", ")})`);
|
|
78
|
+
die(`${origin} refused: ${out.reason}`);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const base = { ...(FAMILIES.length ? { families: FAMILIES } : {}) };
|
|
84
|
+
|
|
85
|
+
const plan = await call(base);
|
|
86
|
+
if (plan.reason === "no-segment") {
|
|
87
|
+
log(`${origin} serves one workspace and writes no segment — there is nothing to move.`);
|
|
88
|
+
process.exit(0);
|
|
89
|
+
}
|
|
90
|
+
log(`${origin} — workspace ${plan.workspace} → ${plan.segment}`);
|
|
91
|
+
log(`families ${plan.families.join(", ")}; ${plan.considered} document(s) considered, ${plan.skipped} already there`);
|
|
92
|
+
|
|
93
|
+
if (!CONFIRM) {
|
|
94
|
+
const left = plan.copied + plan.pending;
|
|
95
|
+
if (!left) { log("\x1b[32mnothing to move\x1b[0m — this workspace is already on the segment"); process.exit(0); }
|
|
96
|
+
log(`\x1b[33mwould copy ${left} document(s)\x1b[0m`);
|
|
97
|
+
for (const k of plan.keys) console.log(` ${k}`);
|
|
98
|
+
if (plan.keys.length < left) console.log(` … and ${left - plan.keys.length} more`);
|
|
99
|
+
console.log("\x1b[2mre-run with --confirm <workspace> to move them; nothing is ever deleted\x1b[0m");
|
|
100
|
+
process.exit(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
let copied = 0, bytes = 0, pages = 0;
|
|
104
|
+
for (;;) {
|
|
105
|
+
const out = await call({ ...base, confirm: CONFIRM });
|
|
106
|
+
copied += out.copied; bytes += out.bytes; pages++;
|
|
107
|
+
if (out.copied) log(`page ${pages}: ${out.copied} copied (${(bytes / 1e3).toFixed(1)} kB so far)`);
|
|
108
|
+
if (out.done) break;
|
|
109
|
+
if (pages >= MAX_PAGES) die(`stopped after ${MAX_PAGES} pages with work left — re-run to continue`);
|
|
110
|
+
}
|
|
111
|
+
log(`\x1b[32mdone\x1b[0m — ${copied} document(s) copied, ${(bytes / 1e3).toFixed(1)} kB`);
|
|
112
|
+
console.log("\x1b[2mthe originals are untouched: this is a copy, and it is what the per-family revert reads\x1b[0m");
|
package/scripts/init.mjs
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// init.mjs — `augur init`: scaffold a new space in the current directory.
|
|
2
|
+
//
|
|
3
|
+
// augur init [--id <id>] [--name "<name>"] [--origin https://…]
|
|
4
|
+
// [--project <folder>] [--prototype <name>]
|
|
5
|
+
//
|
|
6
|
+
// Writes the two files that make a directory a space the engine can build:
|
|
7
|
+
// a `space.json` satisfying agents/space-json.md, and one starter prototype at
|
|
8
|
+
// `<project>/prototypes/<name>/index.html`. Nothing else — a space needs no CI,
|
|
9
|
+
// no secrets and no submodule mount (see templates/README.md).
|
|
10
|
+
//
|
|
11
|
+
// That nesting is not decoration: the build only finds prototypes one level down,
|
|
12
|
+
// inside a top-level project folder (`<opportunity>/prototypes/<name>/` in
|
|
13
|
+
// agents/prototype-contract.md). A `prototypes/` directory at the space root
|
|
14
|
+
// discovers as zero prototypes and the build still reports success, which is a
|
|
15
|
+
// quiet way to lose an afternoon — so the scaffold puts it where it is seen.
|
|
16
|
+
//
|
|
17
|
+
// Defaults come from the directory you run it in, so the common case is a bare
|
|
18
|
+
// `augur init`. It refuses to overwrite an existing space.json or prototype:
|
|
19
|
+
// re-running after filling one in is a no-op you can trust.
|
|
20
|
+
|
|
21
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import { NOTE as CANON_NOTE } from "./canon.mjs";
|
|
24
|
+
|
|
25
|
+
const log = (msg) => console.error(`\x1b[32m[init]\x1b[0m ${msg}`);
|
|
26
|
+
const die = (msg) => { console.error(`\x1b[31m[init]\x1b[0m ${msg}`); process.exit(1); };
|
|
27
|
+
const opt = (f) => { const i = process.argv.indexOf(f); return i > 0 ? process.argv[i + 1] : null; };
|
|
28
|
+
|
|
29
|
+
const CWD = process.cwd();
|
|
30
|
+
|
|
31
|
+
// `id` is the URL prefix and the manifest key, so it is the one field the build
|
|
32
|
+
// constrains: lowercase [a-z0-9-]. Derive a legal one from the folder name rather
|
|
33
|
+
// than making the common case ask a question.
|
|
34
|
+
const slug = (s) => String(s || "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^-+|-+$/g, "");
|
|
35
|
+
const title = (s) => s.split(/[-_\s]+/).filter(Boolean).map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
36
|
+
|
|
37
|
+
const id = slug(opt("--id") || path.basename(CWD));
|
|
38
|
+
if (!id) die(`could not derive a space id from "${path.basename(CWD)}" — pass --id <id> (lowercase letters, digits, hyphens).`);
|
|
39
|
+
|
|
40
|
+
const name = opt("--name") || title(id);
|
|
41
|
+
const origin = (opt("--origin") || process.env.AUGUR_ORIGIN || "").replace(/\/+$/, "");
|
|
42
|
+
const proto = slug(opt("--prototype") || "hello");
|
|
43
|
+
const project = slug(opt("--project") || "hello");
|
|
44
|
+
|
|
45
|
+
const spaceJsonPath = path.join(CWD, "space.json");
|
|
46
|
+
const rel = path.join(project, "prototypes", proto, "index.html");
|
|
47
|
+
const protoDir = path.join(CWD, project, "prototypes", proto);
|
|
48
|
+
const protoPath = path.join(protoDir, "index.html");
|
|
49
|
+
|
|
50
|
+
if (existsSync(spaceJsonPath)) die(`${path.relative(CWD, spaceJsonPath) || "space.json"} already exists — this is already a space. Nothing written.`);
|
|
51
|
+
if (existsSync(protoPath)) die(`${rel} already exists. Pass --prototype <name> for a different starter.`);
|
|
52
|
+
|
|
53
|
+
// Only the fields a new space actually needs. Every other key in
|
|
54
|
+
// agents/space-json.md is optional and better added when it is wanted than
|
|
55
|
+
// scaffolded as noise — `default: true` because a fresh space is a one-space
|
|
56
|
+
// site until something else mounts beside it.
|
|
57
|
+
const space = { id, name, default: true };
|
|
58
|
+
// siteOrigin is what lets `augur connect`/`augur login` and `augur publish` work from a bare
|
|
59
|
+
// clone with no deploy shell around it, so write it when we know it.
|
|
60
|
+
if (origin) space.siteOrigin = origin;
|
|
61
|
+
|
|
62
|
+
writeFileSync(spaceJsonPath, JSON.stringify(space, null, 2) + "\n");
|
|
63
|
+
|
|
64
|
+
// The starter has to open over file:// with no build step (agents/prototype-contract.md),
|
|
65
|
+
// so: no imports, no absolute paths, no fonts off the network. The description meta is
|
|
66
|
+
// what the gallery card, the OG preview and the canvas insert-picker all read.
|
|
67
|
+
const html = `<!doctype html>
|
|
68
|
+
<html lang="en">
|
|
69
|
+
<head>
|
|
70
|
+
<meta charset="utf-8">
|
|
71
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
72
|
+
<title>${name}</title>
|
|
73
|
+
<meta name="description" content="Starter prototype for the ${name} space — replace this with one sentence saying what it shows.">
|
|
74
|
+
<style>
|
|
75
|
+
:root { color-scheme: light dark; }
|
|
76
|
+
body {
|
|
77
|
+
margin: 0; min-height: 100vh; display: grid; place-items: center;
|
|
78
|
+
font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
79
|
+
background: #f4efe6; color: #2c2150;
|
|
80
|
+
}
|
|
81
|
+
main { max-width: 34rem; padding: 2rem; }
|
|
82
|
+
h1 { font-size: 2rem; margin: 0 0 .5rem; letter-spacing: -.02em; }
|
|
83
|
+
p { margin: 0 0 1rem; }
|
|
84
|
+
code { background: rgba(44, 33, 80, .08); padding: .15em .4em; border-radius: 4px; }
|
|
85
|
+
@media (prefers-color-scheme: dark) {
|
|
86
|
+
body { background: #17141f; color: #ece7dd; }
|
|
87
|
+
code { background: rgba(236, 231, 221, .12); }
|
|
88
|
+
}
|
|
89
|
+
</style>
|
|
90
|
+
</head>
|
|
91
|
+
<body>
|
|
92
|
+
<main>
|
|
93
|
+
<h1>${name}</h1>
|
|
94
|
+
<p>This file is <code>${rel}</code>. Edit it — it is
|
|
95
|
+
self-contained static HTML with no build step, so it opens straight from disk
|
|
96
|
+
and ships exactly as written.</p>
|
|
97
|
+
<p>When it works, ship it: on a workspace that serves drafts,
|
|
98
|
+
<code>augur open ${project}/${proto}</code> then <code>augur land</code>; otherwise
|
|
99
|
+
<code>augur ship</code>. Either way you get the live URL.</p>
|
|
100
|
+
</main>
|
|
101
|
+
</body>
|
|
102
|
+
</html>
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
mkdirSync(protoDir, { recursive: true });
|
|
106
|
+
writeFileSync(protoPath, html);
|
|
107
|
+
|
|
108
|
+
// The naming scheme travels WITH the workspace, because the agent that needs it is the one
|
|
109
|
+
// that arrives cold — often with no engine clone beside it to read agents/canon.md from.
|
|
110
|
+
// Written at birth rather than at the first promotion: an affordance nobody can see is one
|
|
111
|
+
// nobody uses, and a workspace that has been worked in for a month is exactly the one whose
|
|
112
|
+
// names have already drifted. Never overwritten — same rule as space.json.
|
|
113
|
+
const canonPath = path.join(CWD, "CANON.md");
|
|
114
|
+
const wroteCanon = !existsSync(canonPath);
|
|
115
|
+
if (wroteCanon) writeFileSync(canonPath, CANON_NOTE);
|
|
116
|
+
|
|
117
|
+
log(`space "${id}" scaffolded:`);
|
|
118
|
+
log(` space.json`);
|
|
119
|
+
log(` ${rel}`);
|
|
120
|
+
if (wroteCanon) log(` CANON.md`);
|
|
121
|
+
if (!origin) log(`no siteOrigin set — add one to space.json (or pass --origin) so connect/login/publish work from this clone.`);
|
|
122
|
+
log(`next: edit the prototype, then \`augur land\` (a workspace that serves drafts) or \`augur ship\`.`);
|
package/scripts/land.mjs
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// augur land [-m "note"] — replace the prototype's main with this draft. The real URL moves;
|
|
3
|
+
// its address is the last line of stdout. Refused when main moved since the draft opened:
|
|
4
|
+
// then `augur sync`, check the draft address, and land again. See docs/drafts-that-land.md.
|
|
5
|
+
import { resolveOrigin, resolveToken } from "./lib/store.mjs";
|
|
6
|
+
import { readState, unitClient, doLand } from "./lib/draft.mjs";
|
|
7
|
+
|
|
8
|
+
const log = (m) => console.error(`\x1b[35m[land]\x1b[0m ${m}`);
|
|
9
|
+
const die = (m) => { console.error(`\x1b[31m[land]\x1b[0m ${m}`); process.exit(1); };
|
|
10
|
+
const argv = process.argv.slice(2);
|
|
11
|
+
const i = argv.indexOf("-m");
|
|
12
|
+
const note = i > -1 ? argv[i + 1] || "" : "";
|
|
13
|
+
const dir = process.cwd();
|
|
14
|
+
const st = readState(dir);
|
|
15
|
+
if (!st) die("not a draft folder — run `augur open <prototype>` first.");
|
|
16
|
+
const origin = st.origin || resolveOrigin();
|
|
17
|
+
const token = resolveToken(origin);
|
|
18
|
+
if (!token) die("no publish token — run `augur connect` once.");
|
|
19
|
+
const client = unitClient({ origin, token, space: st.space, session: st.session });
|
|
20
|
+
const r = await doLand({ client, dir, note });
|
|
21
|
+
if (!r.ok) {
|
|
22
|
+
if (r.error === "main-moved") {
|
|
23
|
+
log(`main moved since this draft opened (now revision ${r.mainRevision}):`);
|
|
24
|
+
for (const c of r.changed || []) log(` changed ${c.path}${c.by ? ` by ${c.by}` : ""}`);
|
|
25
|
+
for (const p of r.removed || []) log(` removed ${p}`);
|
|
26
|
+
die("run `augur sync` to fold those in, check the draft address, then `augur land` again.");
|
|
27
|
+
}
|
|
28
|
+
if (r.error === "landing-in-progress") die("somebody is landing this prototype right now — try again in a few seconds.");
|
|
29
|
+
die(`land refused: ${r.error || r.status}`);
|
|
30
|
+
}
|
|
31
|
+
// The bytes are live; when `recorded` is false only the history entry is missing. Said out
|
|
32
|
+
// loud because the next call adopts that landing as the instance's own, and nobody would
|
|
33
|
+
// otherwise know a landing of theirs is not in the record.
|
|
34
|
+
if (r.recorded === false) {
|
|
35
|
+
log(`landed (publish v${r.version})`);
|
|
36
|
+
log("this landing is live, but its history entry is missing — the next call adopts it without an author.");
|
|
37
|
+
} else {
|
|
38
|
+
log(`landed as revision ${r.revision} (publish v${r.version})`);
|
|
39
|
+
}
|
|
40
|
+
console.log(r.url);
|