@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.
Files changed (190) hide show
  1. package/CANVAS.md +858 -0
  2. package/INSTALL.md +603 -0
  3. package/LICENSE +27 -0
  4. package/README.md +150 -0
  5. package/TRADEMARKS.md +72 -0
  6. package/agents/README.md +65 -0
  7. package/agents/canon-extract.md +117 -0
  8. package/agents/canon.md +151 -0
  9. package/agents/canvas.md +88 -0
  10. package/agents/currency.md +74 -0
  11. package/agents/drafts.md +92 -0
  12. package/agents/identity.md +148 -0
  13. package/agents/prototype-contract.md +69 -0
  14. package/agents/publishing.md +324 -0
  15. package/agents/review-feedback.md +76 -0
  16. package/agents/space-json.md +132 -0
  17. package/agents/ui-skill.md +101 -0
  18. package/agents/working-marks.md +86 -0
  19. package/brand/augur-eye.svg +3 -0
  20. package/brand/augur-icon.svg +41 -0
  21. package/brand/augur-mark.svg +5 -0
  22. package/brand/lentianova-bulky.otf +0 -0
  23. package/brand/lentianova-median.otf +0 -0
  24. package/brand/lentianova.woff2 +0 -0
  25. package/build.js +8409 -0
  26. package/changelog.md +133 -0
  27. package/fonts/inter-latin-wght-normal.woff2 +0 -0
  28. package/package.json +57 -0
  29. package/pitis/PETS.md +121 -0
  30. package/pitis/README.md +117 -0
  31. package/pitis/index.html +266 -0
  32. package/pitis/piti.build.js +81 -0
  33. package/pitis/piti.js +929 -0
  34. package/pitis/roast-agent.md +185 -0
  35. package/realtime/src/index.js +119 -0
  36. package/realtime/wrangler.example.toml +44 -0
  37. package/scripts/adopt.mjs +106 -0
  38. package/scripts/board-snapshot.mjs +197 -0
  39. package/scripts/build-seed-pack.mjs +33 -0
  40. package/scripts/bundle-rekey.mjs +112 -0
  41. package/scripts/bundle-tenancy-rehearsal.mjs +781 -0
  42. package/scripts/canon-extract.mjs +578 -0
  43. package/scripts/canon.mjs +599 -0
  44. package/scripts/canvas-screen.mjs +333 -0
  45. package/scripts/check-local.mjs +136 -0
  46. package/scripts/clawd-art.mjs +75 -0
  47. package/scripts/clawd-canvas.mjs +741 -0
  48. package/scripts/cli.mjs +100 -0
  49. package/scripts/clone.mjs +422 -0
  50. package/scripts/close.mjs +30 -0
  51. package/scripts/connect.mjs +103 -0
  52. package/scripts/deploy.mjs +157 -0
  53. package/scripts/dev.mjs +54 -0
  54. package/scripts/doc-lint.mjs +143 -0
  55. package/scripts/export.mjs +300 -0
  56. package/scripts/fork.mjs +64 -0
  57. package/scripts/freeze.mjs +74 -0
  58. package/scripts/frontdoor-parity.mjs +180 -0
  59. package/scripts/hook.mjs +67 -0
  60. package/scripts/identity-rekey.mjs +112 -0
  61. package/scripts/init.mjs +122 -0
  62. package/scripts/land.mjs +40 -0
  63. package/scripts/lib/adapters.mjs +156 -0
  64. package/scripts/lib/board-snapshot.mjs +511 -0
  65. package/scripts/lib/draft.mjs +385 -0
  66. package/scripts/lib/graduate.mjs +276 -0
  67. package/scripts/lib/instance.mjs +74 -0
  68. package/scripts/lib/marks.mjs +107 -0
  69. package/scripts/lib/materialize.mjs +138 -0
  70. package/scripts/lib/merge3.mjs +124 -0
  71. package/scripts/lib/offline-posture.mjs +120 -0
  72. package/scripts/lib/offline-respawn.mjs +18 -0
  73. package/scripts/lib/offline-wrangler.mjs +78 -0
  74. package/scripts/lib/publish-compose.mjs +12 -0
  75. package/scripts/lib/publish-conflict.mjs +78 -0
  76. package/scripts/lib/publish-evidence.mjs +144 -0
  77. package/scripts/lib/refine-compare.mjs +140 -0
  78. package/scripts/lib/refine-ledger.mjs +150 -0
  79. package/scripts/lib/refine-png.mjs +149 -0
  80. package/scripts/lib/seed-pack-build.mjs +141 -0
  81. package/scripts/lib/state-compare.mjs +68 -0
  82. package/scripts/lib/store.mjs +122 -0
  83. package/scripts/login.mjs +84 -0
  84. package/scripts/mark.mjs +112 -0
  85. package/scripts/migrate-board-keys.mjs +164 -0
  86. package/scripts/migrate.mjs +265 -0
  87. package/scripts/no-foreign-vocabulary.mjs +216 -0
  88. package/scripts/no-product-names.mjs +157 -0
  89. package/scripts/no-tenant-globals.mjs +1134 -0
  90. package/scripts/offline.mjs +247 -0
  91. package/scripts/og.mjs +170 -0
  92. package/scripts/one-tenant-resolver.mjs +146 -0
  93. package/scripts/open.mjs +63 -0
  94. package/scripts/publish.mjs +1041 -0
  95. package/scripts/read.mjs +31 -0
  96. package/scripts/refine.mjs +455 -0
  97. package/scripts/release-drift.mjs +138 -0
  98. package/scripts/restore.mjs +322 -0
  99. package/scripts/review.mjs +125 -0
  100. package/scripts/save.mjs +21 -0
  101. package/scripts/shell-lint.mjs +189 -0
  102. package/scripts/ship.mjs +460 -0
  103. package/scripts/shoot.mjs +264 -0
  104. package/scripts/smoke-invite.mjs +113 -0
  105. package/scripts/state-inventory.mjs +223 -0
  106. package/scripts/status.mjs +177 -0
  107. package/scripts/sync.mjs +27 -0
  108. package/scripts/tenant-do-rehearsal.mjs +1314 -0
  109. package/scripts/ui-copy-lint.mjs +202 -0
  110. package/scripts/watch.mjs +32 -0
  111. package/scripts/wrangler-preflight.mjs +246 -0
  112. package/seed/CANON.md +43 -0
  113. package/seed/README.md +119 -0
  114. package/seed/prototype-status.json +9 -0
  115. package/seed/registry.json +88 -0
  116. package/seed/skills/starter-ui/SKILL.md +52 -0
  117. package/seed/skills/starter-ui/skill.json +11 -0
  118. package/seed/skills/starter-ui/starter-tokens.css +73 -0
  119. package/seed/skills/starter-ui/starter-ui.css +318 -0
  120. package/seed/skills/starter-ui/starter-ui.js +50 -0
  121. package/seed/space.json +5 -0
  122. package/seed/start-here/prototypes/connect-your-terminal/index.html +428 -0
  123. package/seed/start-here/prototypes/connect-your-terminal/preview.webp +0 -0
  124. package/seed/start-here/prototypes/sample-with-comments/index.html +172 -0
  125. package/seed/start-here/prototypes/sample-with-comments/preview.webp +0 -0
  126. package/seed/start-here/prototypes/set-up-your-design-system/index.html +353 -0
  127. package/seed/start-here/prototypes/set-up-your-design-system/preview.webp +0 -0
  128. package/seed/threads.json +52 -0
  129. package/seed/worked-examples/prototypes/field-readings/index.html +273 -0
  130. package/seed/worked-examples/prototypes/field-readings/preview.webp +0 -0
  131. package/seed/worked-examples/prototypes/slide-deck/index.html +214 -0
  132. package/seed/worked-examples/prototypes/slide-deck/preview.webp +0 -0
  133. package/seed/worked-examples/prototypes/specimen-viewer/index.html +203 -0
  134. package/seed/worked-examples/prototypes/specimen-viewer/preview.webp +0 -0
  135. package/src/_worker.js +12668 -0
  136. package/src/board-key.mjs +57 -0
  137. package/src/board-room.mjs +881 -0
  138. package/src/bundle-keys.mjs +138 -0
  139. package/src/canon/collect-in-browser.js +215 -0
  140. package/src/canon/emit.mjs +232 -0
  141. package/src/canon/observe.mjs +260 -0
  142. package/src/canon/schema.mjs +451 -0
  143. package/src/canvas/DSEG-LICENSE.txt +95 -0
  144. package/src/canvas/DSEG7Classic-Bold.woff2 +0 -0
  145. package/src/canvas/canvas.css +885 -0
  146. package/src/canvas/canvas.js +5780 -0
  147. package/src/canvas/capture.js +485 -0
  148. package/src/chrome/appchrome.mjs +568 -0
  149. package/src/currency.mjs +203 -0
  150. package/src/delete-confirmation.mjs +254 -0
  151. package/src/drafts/drafts.js +326 -0
  152. package/src/entry.js +58 -0
  153. package/src/galleries.mjs +400 -0
  154. package/src/health-cron.mjs +173 -0
  155. package/src/identity.json +1 -0
  156. package/src/kv-codec.mjs +135 -0
  157. package/src/kv-identity.mjs +259 -0
  158. package/src/mail.mjs +599 -0
  159. package/src/provenance.mjs +74 -0
  160. package/src/publish-compose.mjs +363 -0
  161. package/src/publish-fork.mjs +221 -0
  162. package/src/publish-units.mjs +44 -0
  163. package/src/purge.mjs +81 -0
  164. package/src/review/COMMENTING-UX.md +306 -0
  165. package/src/review/cat.png +0 -0
  166. package/src/review/comment-cursor.svg +7 -0
  167. package/src/review/comments.js +1738 -0
  168. package/src/room-ticket.mjs +96 -0
  169. package/src/roster-chip.mjs +32 -0
  170. package/src/seed-pack.mjs +261 -0
  171. package/src/state-inventory.mjs +249 -0
  172. package/src/sw-logic.mjs +48 -0
  173. package/src/tenant-cache.mjs +141 -0
  174. package/src/tenant-context.mjs +386 -0
  175. package/src/tenant-do.js +2940 -0
  176. package/src/tenant-host.mjs +144 -0
  177. package/src/tenant-quotas.mjs +140 -0
  178. package/src/unit-core.mjs +103 -0
  179. package/src/unit-object.mjs +338 -0
  180. package/templates/README.md +67 -0
  181. package/templates/shell/deploy.yml +145 -0
  182. package/templates/shell/engine-bump.yml +142 -0
  183. package/templates/shell/health.yml +233 -0
  184. package/templates/shell/kv-backup.yml +201 -0
  185. package/templates/shell/roster-update.yml +110 -0
  186. package/templates/shell/space-preflight.yml +35 -0
  187. package/templates/shell/space-rebake.yml +79 -0
  188. package/templates/shell/store-backup.yml +152 -0
  189. package/templates/shell/wrangler.example.toml +266 -0
  190. package/templates/space/publish.yml +91 -0
@@ -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();
@@ -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");
@@ -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\`.`);
@@ -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);