@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
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
// offline.mjs — local "offline mode" for Augur.
|
|
2
|
+
//
|
|
3
|
+
// npm run offline → start · Ctrl-C → stop
|
|
4
|
+
//
|
|
5
|
+
// What it does:
|
|
6
|
+
// 1. Builds dist once (node build.js).
|
|
7
|
+
// 2. Starts `wrangler dev` against a generated config (.wrangler/offline.toml) with
|
|
8
|
+
// src/entry.js as the entry — the SAME front door a deployed instance uses, running
|
|
9
|
+
// the real worker locally, so the per-user login gate is ON (same as live; sign in
|
|
10
|
+
// with an admin account — seed creds in src/identity.json) and every overlay API
|
|
11
|
+
// (comments / pins / status / names / piti) works. No deploy — a faithful local
|
|
12
|
+
// mirror of the live site.
|
|
13
|
+
// It used to be `wrangler pages dev dist`, and the difference is not cosmetic: a
|
|
14
|
+
// Worker serves a matching static asset BEFORE the worker runs unless the config says
|
|
15
|
+
// otherwise, and dist/__config/instance.json holds the roster with seed passwords.
|
|
16
|
+
// Running the deployed front door locally is how that difference gets found by a
|
|
17
|
+
// person. See scripts/lib/offline-wrangler.mjs.
|
|
18
|
+
// ⚠️ KV is LIVE/prod when .env.deploy holds Cloudflare creds: the worker reads/writes
|
|
19
|
+
// the REAL production KV via a REST shim (LIVE_KV below), so overlay edits made offline
|
|
20
|
+
// are live for everyone — intentional (offline editing, shared live overlay). Rename
|
|
21
|
+
// .env.deploy for a safe local-only KV sandbox (it then logs `KV: local`).
|
|
22
|
+
// Canvas realtime follows the same split: live KV + RT_SHARED_SECRET in .env.deploy
|
|
23
|
+
// → boards join the instance's real rooms; sandbox → realtime disabled outright
|
|
24
|
+
// (GV_RT_DISABLE), so a sandbox board can't broadcast into shared prod rooms.
|
|
25
|
+
// wrangler is supervised: a workerd crash respawns it (crash-loop cutoff in
|
|
26
|
+
// lib/offline-respawn.mjs) instead of killing the whole offline server.
|
|
27
|
+
// 3. Watches the build inputs (the design system, the workspace, build.js, the
|
|
28
|
+
// worker) and rebuilds on any change. Each build writes a fresh BUILD_ID into
|
|
29
|
+
// dist/__config/instance.json — it is runtime DATA, not something stamped into the
|
|
30
|
+
// worker's code — and the page's live-reload poller (which polls /__version every
|
|
31
|
+
// ~1s on localhost) refreshes open tabs within the worker's ~1.5s config tick.
|
|
32
|
+
//
|
|
33
|
+
// Zero new dependencies: Node's recursive fs.watch + npx wrangler (same as deploy).
|
|
34
|
+
|
|
35
|
+
import { spawn } from "node:child_process";
|
|
36
|
+
import { watch, existsSync, readFileSync, readdirSync, mkdirSync, writeFileSync } from "node:fs";
|
|
37
|
+
import path from "node:path";
|
|
38
|
+
import { parseEnvFile, derivePosture, postureVars, postureBanner } from "./lib/offline-posture.mjs";
|
|
39
|
+
import { offlineWranglerConfig } from "./lib/offline-wrangler.mjs";
|
|
40
|
+
import { fileURLToPath } from "node:url";
|
|
41
|
+
import { findShellDir } from "./lib/instance.mjs";
|
|
42
|
+
import { respawnDelay } from "./lib/offline-respawn.mjs";
|
|
43
|
+
|
|
44
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
45
|
+
const PORT = process.env.OFFLINE_PORT || "8788";
|
|
46
|
+
|
|
47
|
+
const log = (msg) => console.log(`\x1b[35m[offline]\x1b[0m ${msg}`);
|
|
48
|
+
|
|
49
|
+
// Optional "offline-live" mode: if augur/.env.deploy holds Cloudflare creds, the local
|
|
50
|
+
// worker talks to the REAL prod KV via the REST shim in _worker.js (kvFor) — so
|
|
51
|
+
// comments/pins/status/renames are the shared live layer while prototypes stay local.
|
|
52
|
+
// No creds → today's local KV, unchanged. Passed to the worker as
|
|
53
|
+
// --binding GV_KV_* (read by kvFor). Prototypes/assets are always local regardless.
|
|
54
|
+
// The live-vs-sandbox decision, and the bindings it implies, now live in
|
|
55
|
+
// scripts/lib/offline-posture.mjs — a pure function with a table of cases under it in
|
|
56
|
+
// test/offline-posture.test.mjs. It moved because it had NO coverage and it decides the
|
|
57
|
+
// one thing about local development that can hurt somebody else: whether this process
|
|
58
|
+
// reads and writes the LIVE KV namespace or a local sandbox. Getting it backwards is
|
|
59
|
+
// silent in both directions, so a refactor of exactly this code is how a live-vs-sandbox
|
|
60
|
+
// inversion ships green.
|
|
61
|
+
//
|
|
62
|
+
// `--sandbox` forces the safe posture whatever .env.deploy holds. There is deliberately
|
|
63
|
+
// no inverse flag: nothing may turn a sandbox into a live run except the credentials
|
|
64
|
+
// themselves being present.
|
|
65
|
+
const POSTURE = derivePosture(
|
|
66
|
+
parseEnvFile((() => { try { return readFileSync(path.join(ROOT, ".env.deploy"), "utf8"); } catch (e) { return ""; } })()),
|
|
67
|
+
{ forceSandbox: process.argv.includes("--sandbox") },
|
|
68
|
+
);
|
|
69
|
+
const POSTURE_VARS = postureVars(POSTURE);
|
|
70
|
+
|
|
71
|
+
// Build from the canonical EDIT-HERE clones, not the pinned nested submodules.
|
|
72
|
+
// One repo per space: a maintainer workspace puts each space repo — a self-contained
|
|
73
|
+
// bundle with that space's design system AND prototypes at its root — as a sibling of
|
|
74
|
+
// augur/ (../<space>, …), and that's where every agent edits. Building from them means
|
|
75
|
+
// a local preview reflects live edits with no pin bump, no matter which agent made the
|
|
76
|
+
// change. A sibling counts as a space iff it carries a space.json at its root — the same
|
|
77
|
+
// filter build.js's discoverSpaces() applies — so augur itself and scratch dirs are
|
|
78
|
+
// ignored. Fall back to the pinned submodules under augur/spaces/ if no sibling space
|
|
79
|
+
// exists (e.g. augur cloned on its own, outside a multi-space workspace).
|
|
80
|
+
const PARENT = path.join(ROOT, "..");
|
|
81
|
+
let siblingSpaces = [];
|
|
82
|
+
try {
|
|
83
|
+
siblingSpaces = readdirSync(PARENT, { withFileTypes: true })
|
|
84
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith(".")
|
|
85
|
+
&& existsSync(path.join(PARENT, e.name, "space.json")))
|
|
86
|
+
.map((e) => path.join(PARENT, e.name));
|
|
87
|
+
} catch {}
|
|
88
|
+
const usingSiblings = siblingSpaces.length > 0;
|
|
89
|
+
const SPACES_ROOT = usingSiblings ? PARENT : path.join(ROOT, "spaces");
|
|
90
|
+
// Passed to build.js so it composes every space from here (see GV_SPACES_ROOT there).
|
|
91
|
+
// GV_LOCAL says this build is only ever served from this machine, which is what lets a
|
|
92
|
+
// space's tracks/ music play here while staying out of everything that ships.
|
|
93
|
+
const BUILD_ENV = { ...process.env, GV_SPACES_ROOT: SPACES_ROOT, GV_LOCAL: "1" };
|
|
94
|
+
// Identity + deploy config: explicit env wins; else auto-detect a sibling DEPLOY SHELL
|
|
95
|
+
// by shape — any sibling dir with an identity.json at its root that is not a space
|
|
96
|
+
// (shell repo names vary per instance). The shell also contributes deploy.config.json
|
|
97
|
+
// when it has one. A raw engine clone with no shell falls back to the in-repo
|
|
98
|
+
// src/identity.json (an empty [] placeholder → gate stays open).
|
|
99
|
+
const SHELL_DIR = findShellDir(ROOT);
|
|
100
|
+
const IDENTITY_PATH = process.env.GV_IDENTITY_PATH
|
|
101
|
+
|| [SHELL_DIR && path.join(SHELL_DIR, "identity.json"), path.join(ROOT, "src", "identity.json")]
|
|
102
|
+
.filter(Boolean).find((p) => existsSync(p));
|
|
103
|
+
if (IDENTITY_PATH) BUILD_ENV.GV_IDENTITY_PATH = IDENTITY_PATH;
|
|
104
|
+
const DEPLOY_CONFIG_PATH = process.env.GV_DEPLOY_CONFIG_PATH
|
|
105
|
+
|| (SHELL_DIR && existsSync(path.join(SHELL_DIR, "deploy.config.json"))
|
|
106
|
+
? path.join(SHELL_DIR, "deploy.config.json") : null);
|
|
107
|
+
if (DEPLOY_CONFIG_PATH) BUILD_ENV.GV_DEPLOY_CONFIG_PATH = DEPLOY_CONFIG_PATH;
|
|
108
|
+
|
|
109
|
+
// Build inputs to watch — each space repo (its DS assets + prototypes) plus the
|
|
110
|
+
// augur-owned build inputs. Specific subtrees only — never node_modules, .git, dist,
|
|
111
|
+
// .wrangler, or space-specific bulk dirs (IGNORE below filters their events even
|
|
112
|
+
// inside a watched space root; extend via GV_SCAN_IGNORE, comma-separated).
|
|
113
|
+
const WATCH = [
|
|
114
|
+
path.join(ROOT, "build.js"),
|
|
115
|
+
path.join(ROOT, "src", "_worker.js"),
|
|
116
|
+
...(IDENTITY_PATH ? [IDENTITY_PATH] : []), // users + seed passwords → rebuild on change
|
|
117
|
+
...(DEPLOY_CONFIG_PATH ? [DEPLOY_CONFIG_PATH] : []), // instance config → rebuild on change
|
|
118
|
+
path.join(ROOT, "src", "canvas"), // the infinite-canvas engine (emitted to /__canvas)
|
|
119
|
+
path.join(ROOT, "src", "review"), // comment/annotation overlay (emitted to /__review)
|
|
120
|
+
path.join(ROOT, "pitis"), // augur-owned cursor-companion layer
|
|
121
|
+
...(usingSiblings ? siblingSpaces : [path.join(ROOT, "spaces")]),
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
// Paths whose changes are noise — ignore even if they live under a watched root.
|
|
125
|
+
const IGNORE = [
|
|
126
|
+
"node_modules", "/.git/", "/dist/", "/.wrangler/", ".DS_Store",
|
|
127
|
+
...(process.env.GV_SCAN_IGNORE || "").split(",").filter(Boolean),
|
|
128
|
+
];
|
|
129
|
+
// endsWith catches an event for the .git dir itself (no trailing slash) — the watch
|
|
130
|
+
// roots are now whole sibling repos, which contain a real .git.
|
|
131
|
+
const ignored = (abs) => IGNORE.some((frag) => abs.includes(frag)) || abs.endsWith("/.git");
|
|
132
|
+
|
|
133
|
+
// ── build (run-to-completion, with a queued-rebuild guard) ───────────────────
|
|
134
|
+
let building = false;
|
|
135
|
+
let pending = false;
|
|
136
|
+
|
|
137
|
+
function build(reason) {
|
|
138
|
+
if (building) { pending = true; return; }
|
|
139
|
+
building = true;
|
|
140
|
+
const started = Date.now();
|
|
141
|
+
log(`↻ building${reason ? ` (${reason})` : ""}…`);
|
|
142
|
+
const proc = spawn("node", ["build.js"], { cwd: ROOT, env: BUILD_ENV, stdio: ["ignore", "ignore", "pipe"] });
|
|
143
|
+
let err = "";
|
|
144
|
+
proc.stderr.on("data", (d) => { err += d; });
|
|
145
|
+
proc.on("close", (code) => {
|
|
146
|
+
building = false;
|
|
147
|
+
if (code === 0) log(`✓ built in ${((Date.now() - started) / 1000).toFixed(1)}s`);
|
|
148
|
+
else log(`✗ build failed (exit ${code})\n${err.trim()}`);
|
|
149
|
+
if (pending) { pending = false; build("queued change"); }
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ── initial build, then launch wrangler ──────────────────────────────────────
|
|
154
|
+
log(`starting offline mode — building from ${usingSiblings ? "canonical sibling space clones (edit-here)" : "nested submodules (pinned)"}`);
|
|
155
|
+
log(` spaces: ${usingSiblings ? siblingSpaces.join(", ") : path.join(ROOT, "spaces")}`);
|
|
156
|
+
await new Promise((resolve) => {
|
|
157
|
+
const proc = spawn("node", ["build.js"], { cwd: ROOT, env: BUILD_ENV, stdio: "inherit" });
|
|
158
|
+
proc.on("close", resolve);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
log(`serving on http://localhost:${PORT} (Ctrl-C to stop)`);
|
|
162
|
+
// The posture prints on EVERY run, because a posture nobody prints is a posture nobody
|
|
163
|
+
// checks. It carries no credential, which is the reason the banner takes the derived
|
|
164
|
+
// posture rather than the env file.
|
|
165
|
+
log(POSTURE.live
|
|
166
|
+
? `KV: \x1b[1mLIVE\x1b[0m\x1b[35m — ${POSTURE.reason} (prototypes stay local)`
|
|
167
|
+
: `KV: local — ${POSTURE.reason}`);
|
|
168
|
+
log({
|
|
169
|
+
joined: "realtime: LIVE — canvas boards join the instance's real rooms through /__rt",
|
|
170
|
+
solo: "realtime: \x1b[1mUNAVAILABLE\x1b[0m\x1b[35m — no RT_SHARED_SECRET in .env.deploy; canvas boards run SOLO (saves still land in prod KV)",
|
|
171
|
+
disabled: "realtime: disabled (sandbox) — canvas boards run solo against local KV",
|
|
172
|
+
}[POSTURE.realtime]);
|
|
173
|
+
let wrangler = null;
|
|
174
|
+
let stopping = false;
|
|
175
|
+
const crashTimes = [];
|
|
176
|
+
// The config wrangler runs from, rewritten on every start so it can never be stale, and
|
|
177
|
+
// written under .wrangler/ so a stray `npx wrangler` in this clone cannot pick it up.
|
|
178
|
+
// Regenerating it here rather than committing it is also what keeps the gate line
|
|
179
|
+
// (`run_worker_first`) impossible to lose to a local edit.
|
|
180
|
+
const WRANGLER_CONFIG = path.join(ROOT, ".wrangler", "offline.toml");
|
|
181
|
+
mkdirSync(path.dirname(WRANGLER_CONFIG), { recursive: true });
|
|
182
|
+
writeFileSync(WRANGLER_CONFIG, offlineWranglerConfig({ root: ROOT }));
|
|
183
|
+
|
|
184
|
+
function spawnWrangler() {
|
|
185
|
+
wrangler = spawn(
|
|
186
|
+
"npx",
|
|
187
|
+
// `wrangler dev`, not `wrangler pages dev` — the front door a deployed instance uses.
|
|
188
|
+
// `--local` is explicit rather than assumed: the COMMENTS binding here is a local
|
|
189
|
+
// namespace and must stay one. A live run reaches production KV through the REST shim
|
|
190
|
+
// inside the worker (GV_KV_* below), which is a different mechanism on purpose.
|
|
191
|
+
["--yes", "wrangler", "dev",
|
|
192
|
+
"-c", WRANGLER_CONFIG,
|
|
193
|
+
"--local",
|
|
194
|
+
...POSTURE_VARS,
|
|
195
|
+
"--port", PORT,
|
|
196
|
+
"--persist-to", ".wrangler/state"],
|
|
197
|
+
{ cwd: ROOT, stdio: "inherit" },
|
|
198
|
+
);
|
|
199
|
+
// A workerd crash must not take the whole offline server with it — every open tab
|
|
200
|
+
// would lose its save rail AND its room at once, and unsaved board edits would sit
|
|
201
|
+
// in browser memory until someone noticed the port was dead. Respawn instead, with
|
|
202
|
+
// a crash-loop cutoff (policy in lib/offline-respawn.mjs) so an unstartable worker
|
|
203
|
+
// (port taken, broken build) fails loudly rather than restart-storming.
|
|
204
|
+
wrangler.on("close", (code) => {
|
|
205
|
+
if (stopping) process.exit(code ?? 0);
|
|
206
|
+
const now = Date.now();
|
|
207
|
+
const delay = respawnDelay(crashTimes, now);
|
|
208
|
+
crashTimes.push(now);
|
|
209
|
+
if (delay === null) {
|
|
210
|
+
log(`wrangler exited (${code}) — crash loop, giving up. Fix the cause and restart.`);
|
|
211
|
+
process.exit(code ?? 1);
|
|
212
|
+
}
|
|
213
|
+
log(`wrangler exited (${code}) — respawning in ${delay / 1000}s`);
|
|
214
|
+
setTimeout(spawnWrangler, delay);
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
spawnWrangler();
|
|
218
|
+
|
|
219
|
+
// ── watch inputs, debounce, rebuild ──────────────────────────────────────────
|
|
220
|
+
let timer = null;
|
|
221
|
+
const onChange = (root) => (_evt, file) => {
|
|
222
|
+
// For a directory target, `file` is the path within it; for a file target, `file`
|
|
223
|
+
// is just that file's basename (so don't re-join it onto the already-full root).
|
|
224
|
+
const abs = file && !root.endsWith(file) ? path.join(root, file) : root;
|
|
225
|
+
if (ignored(abs)) return;
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
timer = setTimeout(() => build(path.relative(ROOT, abs)), 200); // debounce bursts of fs events
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
for (const target of WATCH) {
|
|
231
|
+
try {
|
|
232
|
+
watch(target, { recursive: true }, onChange(target));
|
|
233
|
+
} catch {
|
|
234
|
+
// A watched path may not exist in every checkout (e.g. an optional dir) — skip it.
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
log(`watching ${WATCH.length} input paths for changes`);
|
|
238
|
+
|
|
239
|
+
// ── teardown ─────────────────────────────────────────────────────────────────
|
|
240
|
+
function shutdown() {
|
|
241
|
+
log("stopping.");
|
|
242
|
+
stopping = true;
|
|
243
|
+
if (wrangler) wrangler.kill("SIGINT");
|
|
244
|
+
else process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
process.on("SIGINT", shutdown);
|
|
247
|
+
process.on("SIGTERM", shutdown);
|
package/scripts/og.mjs
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// og.mjs — compose a 1200×630 Open Graph card for a prototype: the prototype's
|
|
3
|
+
// poster screenshot framed in a browser chrome, beside the title + site wordmark,
|
|
4
|
+
// on the site's indigo shell background. Writes og.jpg into the folder.
|
|
5
|
+
//
|
|
6
|
+
// Run AFTER shoot.mjs (it reuses each folder's preview.webp as the screenshot).
|
|
7
|
+
//
|
|
8
|
+
// node scripts/og.mjs <path/to/folder> # one folder
|
|
9
|
+
// node scripts/og.mjs # every card target (same set as shoot)
|
|
10
|
+
//
|
|
11
|
+
// Requires: playwright (devDep).
|
|
12
|
+
|
|
13
|
+
import { chromium } from "playwright";
|
|
14
|
+
import { promises as fs } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
|
|
18
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
19
|
+
const OG = { width: 1200, height: 630 };
|
|
20
|
+
|
|
21
|
+
async function exists(p) { try { await fs.access(p); return true; } catch { return false; } }
|
|
22
|
+
async function isDir(p) { try { return (await fs.stat(p)).isDirectory(); } catch { return false; } }
|
|
23
|
+
|
|
24
|
+
async function entry(dir) {
|
|
25
|
+
if (await exists(path.join(dir, "index.html"))) return path.join(dir, "index.html");
|
|
26
|
+
const es = await fs.readdir(dir, { withFileTypes: true });
|
|
27
|
+
const h = es.find((e) => e.isFile() && e.name.endsWith(".html"));
|
|
28
|
+
return h ? path.join(dir, h.name) : null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Folders inside a space root that never hold card targets. Space discovery mirrors
|
|
32
|
+
// shoot.mjs: editable sibling clones (../<x> with space.json) first, pinned spaces/<id>
|
|
33
|
+
// mirrors as the fallback.
|
|
34
|
+
// Extend via GV_SCAN_IGNORE (comma-separated dir names) for space-specific bulk dirs.
|
|
35
|
+
const IGNORE = new Set(["node_modules", "skills", "scripts", "registry", ".git", ".github", ".claude",
|
|
36
|
+
...(process.env.GV_SCAN_IGNORE || "").split(",").filter(Boolean)]);
|
|
37
|
+
async function spaceRoots() {
|
|
38
|
+
const roots = [];
|
|
39
|
+
for (const base of [path.join(ROOT, ".."), path.join(ROOT, "spaces")]) {
|
|
40
|
+
try {
|
|
41
|
+
for (const e of await fs.readdir(base, { withFileTypes: true })) {
|
|
42
|
+
if (!e.isDirectory() || e.name.startsWith(".")) continue;
|
|
43
|
+
if (await exists(path.join(base, e.name, "space.json"))) roots.push(path.join(base, e.name));
|
|
44
|
+
}
|
|
45
|
+
} catch {}
|
|
46
|
+
if (roots.length) break;
|
|
47
|
+
}
|
|
48
|
+
return roots;
|
|
49
|
+
}
|
|
50
|
+
async function targets() {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const root of await spaceRoots()) {
|
|
53
|
+
for (const t of await fs.readdir(root, { withFileTypes: true })) {
|
|
54
|
+
if (!t.isDirectory() || IGNORE.has(t.name) || t.name.startsWith(".")) continue;
|
|
55
|
+
const pp = path.join(root, t.name, "prototypes");
|
|
56
|
+
if (await isDir(pp)) {
|
|
57
|
+
for (const p of await fs.readdir(pp, { withFileTypes: true }))
|
|
58
|
+
if (p.isDirectory()) out.push(path.join(pp, p.name));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
for (const group of ["pages", "components", "base", "patterns", "playground"]) {
|
|
62
|
+
const g = path.join(root, group);
|
|
63
|
+
if (await isDir(g)) {
|
|
64
|
+
for (const e of await fs.readdir(g, { withFileTypes: true }))
|
|
65
|
+
if (e.isDirectory() && !e.name.startsWith(".")) out.push(path.join(g, e.name));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Bare card: just the prototype preview, centered, bottom-flush, on the soft site
|
|
73
|
+
// shell. No logo/title/text — those live in the og:* meta tags, not the image.
|
|
74
|
+
function cardHTML({ posterDataUri }) {
|
|
75
|
+
return `<!doctype html><html><head><meta charset="utf-8">
|
|
76
|
+
<style>
|
|
77
|
+
* { box-sizing: border-box; margin: 0; padding: 0; }
|
|
78
|
+
html,body { width:1200px; height:630px; }
|
|
79
|
+
body { position:relative; overflow:hidden; background:#fbfbfd; }
|
|
80
|
+
body::before {
|
|
81
|
+
content:""; position:absolute; inset:0;
|
|
82
|
+
background:
|
|
83
|
+
radial-gradient(1000px 520px at 50% -22%, rgba(94,106,210,0.13), transparent 60%),
|
|
84
|
+
linear-gradient(180deg, #f3f4fb 0%, #fbfbfd 62%);
|
|
85
|
+
}
|
|
86
|
+
.shot {
|
|
87
|
+
position:absolute; left:50%; bottom:0; transform:translateX(-50%);
|
|
88
|
+
width:940px; display:block; border-radius:14px 14px 0 0;
|
|
89
|
+
box-shadow:0 34px 90px -30px rgba(16,24,40,0.46), 0 2px 8px rgba(16,24,40,0.06);
|
|
90
|
+
}
|
|
91
|
+
</style></head><body>
|
|
92
|
+
<img class="shot" src="${posterDataUri}">
|
|
93
|
+
</body></html>`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function makeCard(browser, dir) {
|
|
97
|
+
const file = await entry(dir);
|
|
98
|
+
const rel = path.relative(ROOT, dir);
|
|
99
|
+
if (!file) { console.log("· skip (no html):", rel); return false; }
|
|
100
|
+
const posterPath = path.join(dir, "preview.webp");
|
|
101
|
+
if (!(await exists(posterPath))) { console.log("· skip (no poster — run shoot):", rel); return false; }
|
|
102
|
+
|
|
103
|
+
const posterB64 = (await fs.readFile(posterPath)).toString("base64");
|
|
104
|
+
const posterDataUri = `data:image/webp;base64,${posterB64}`;
|
|
105
|
+
const out = path.join(dir, "og.jpg");
|
|
106
|
+
|
|
107
|
+
// Up to 2 attempts, with per-card failures caught so a single transient screenshot
|
|
108
|
+
// hiccup can't crash the run and abort `npm run deploy` — the existing committed
|
|
109
|
+
// card is left in place and the run continues. Mirrors shoot.mjs.
|
|
110
|
+
for (let attempt = 1; attempt <= 2; attempt++) {
|
|
111
|
+
// 1× = the OG spec size (1200×630). Unfurl thumbnails render ~500px wide, so a
|
|
112
|
+
// higher DPR only bloats the file (some bots time out on multi-MB images).
|
|
113
|
+
const page = await browser.newPage({ viewport: OG, deviceScaleFactor: 1 });
|
|
114
|
+
try {
|
|
115
|
+
await page.setContent(cardHTML({ posterDataUri }), { waitUntil: "load" });
|
|
116
|
+
await page.waitForTimeout(200); // let the poster decode
|
|
117
|
+
// JPEG q85 keeps each committed card ~80–120KB (vs ~300KB PNG) — invisible on an
|
|
118
|
+
// unfurl thumbnail, ~3× lighter in the repo as the prototype count grows.
|
|
119
|
+
await page.screenshot({ path: out, type: "jpeg", quality: 85, clip: { x: 0, y: 0, ...OG } });
|
|
120
|
+
const kb = Math.round((await fs.stat(out)).size / 1024);
|
|
121
|
+
console.log("✓", rel + "/og.jpg", kb + "KB");
|
|
122
|
+
return true;
|
|
123
|
+
} catch (e) {
|
|
124
|
+
const msg = e.message.split("\n")[0];
|
|
125
|
+
if (attempt < 2) { console.log("· retry", rel, "—", msg); continue; }
|
|
126
|
+
console.log("✗ FAIL", rel, "—", msg);
|
|
127
|
+
return false;
|
|
128
|
+
} finally {
|
|
129
|
+
await page.close().catch(() => {});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// A folder needs a fresh card when it has no og.jpg, or its poster/entry html is
|
|
136
|
+
// newer than the existing card. Mirrors shoot.mjs's --stale so deploys stay cheap.
|
|
137
|
+
async function needsCard(dir) {
|
|
138
|
+
const card = path.join(dir, "og.jpg");
|
|
139
|
+
if (!(await exists(card))) return true;
|
|
140
|
+
const cardM = (await fs.stat(card)).mtimeMs;
|
|
141
|
+
const poster = path.join(dir, "preview.webp");
|
|
142
|
+
const file = await entry(dir);
|
|
143
|
+
for (const p of [poster, file].filter(Boolean)) {
|
|
144
|
+
if (await exists(p) && (await fs.stat(p)).mtimeMs > cardM) return true;
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const argv = process.argv.slice(2);
|
|
150
|
+
const staleOnly = argv.includes("--stale");
|
|
151
|
+
const pathArg = argv.find((a) => !a.startsWith("--"));
|
|
152
|
+
let list = pathArg ? [path.resolve(pathArg)] : await targets();
|
|
153
|
+
|
|
154
|
+
if (staleOnly) {
|
|
155
|
+
const fresh = [];
|
|
156
|
+
for (const d of list) if (await needsCard(d)) fresh.push(d);
|
|
157
|
+
const skipped = list.length - fresh.length;
|
|
158
|
+
if (skipped) console.log(`stale: ${skipped} card(s) up to date · ${fresh.length} to (re)make`);
|
|
159
|
+
list = fresh;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (!list.length) {
|
|
163
|
+
console.log("nothing to make — all OG cards current");
|
|
164
|
+
} else {
|
|
165
|
+
const browser = await chromium.launch();
|
|
166
|
+
let ok = 0;
|
|
167
|
+
for (const d of list) if (await makeCard(browser, d)) ok++;
|
|
168
|
+
await browser.close();
|
|
169
|
+
console.log(`\nmade ${ok}/${list.length} OG cards`);
|
|
170
|
+
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// one-tenant-resolver — the seam stays a seam.
|
|
3
|
+
//
|
|
4
|
+
// WHY. `resolveTenant(request, env)` is the single place that decides which workspace a
|
|
5
|
+
// request belongs to. Its value is that it is SINGLE: today it answers statically, and
|
|
6
|
+
// swapping in a Host-header resolver is then a body change with nothing else to find.
|
|
7
|
+
// The way that value is lost is not a red test — it is one more `await resolveTenant(…)`
|
|
8
|
+
// added somewhere convenient, which in a single-workspace deployment returns the same
|
|
9
|
+
// answer as the first one and is therefore invisible to every other check. A second call
|
|
10
|
+
// site is also a second place to forget, and two callers with different answers is the
|
|
11
|
+
// bug the whole phase exists to remove.
|
|
12
|
+
//
|
|
13
|
+
// WHAT IS ENFORCED.
|
|
14
|
+
//
|
|
15
|
+
// ONE DECLARATION, with the pinned signature `async function resolveTenant(request, env)`.
|
|
16
|
+
// The signature is the contract the next implementation has to honour: a resolver that
|
|
17
|
+
// quietly grows a third parameter is a resolver its callers have to know something extra
|
|
18
|
+
// about.
|
|
19
|
+
//
|
|
20
|
+
// ONE CALL SITE, and it is inside the worker's `fetch()`, BEFORE the config load. After
|
|
21
|
+
// the load is too late — the config would have been read for whichever workspace the
|
|
22
|
+
// isolate happened to look at last, which is the failure being designed out.
|
|
23
|
+
//
|
|
24
|
+
// Comments do not count as calls: the header above names the function repeatedly, and so
|
|
25
|
+
// does the worker's own. Lines whose first non-space character starts a comment are
|
|
26
|
+
// skipped, which is enough here because the worker writes no code after a `//` on the
|
|
27
|
+
// same line and its embedded client scripts are all indented inside template literals.
|
|
28
|
+
//
|
|
29
|
+
// Usage: node scripts/one-tenant-resolver.mjs [--worker <path>] [--quiet]
|
|
30
|
+
// No config, no dependencies. Exit 1 on any failure.
|
|
31
|
+
|
|
32
|
+
import fs from "node:fs";
|
|
33
|
+
import path from "node:path";
|
|
34
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
35
|
+
|
|
36
|
+
const DECLARATION = /^async function resolveTenant\(request, env\)\s*\{/;
|
|
37
|
+
const CALL = /\bresolveTenant\s*\(/;
|
|
38
|
+
const FETCH_OPEN = /^\s*async fetch\(request, env, ctx\)\s*\{/;
|
|
39
|
+
const LOAD_CONFIG = /\bloadConfig\s*\(/;
|
|
40
|
+
|
|
41
|
+
const isComment = (line) => {
|
|
42
|
+
const t = line.trim();
|
|
43
|
+
return t.startsWith("//") || t.startsWith("*") || t.startsWith("/*");
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Returns every problem found, each as `{ kind, line, message }`. An empty list passes.
|
|
47
|
+
export function checkTenantResolver(source) {
|
|
48
|
+
const lines = source.split("\n");
|
|
49
|
+
const problems = [];
|
|
50
|
+
const at = (re, from = 0) => {
|
|
51
|
+
for (let i = from; i < lines.length; i++) if (!isComment(lines[i]) && re.test(lines[i])) return i;
|
|
52
|
+
return -1;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const declared = [];
|
|
56
|
+
const calls = [];
|
|
57
|
+
for (let i = 0; i < lines.length; i++) {
|
|
58
|
+
const line = lines[i];
|
|
59
|
+
if (isComment(line)) continue;
|
|
60
|
+
if (DECLARATION.test(line)) { declared.push(i + 1); continue; }
|
|
61
|
+
if (/^\s*(async\s+)?function resolveTenant\b/.test(line) || /^\s*(const|let|var)\s+resolveTenant\b/.test(line)) {
|
|
62
|
+
problems.push({
|
|
63
|
+
kind: "signature", line: i + 1,
|
|
64
|
+
message: "resolveTenant is declared with a different shape — the pinned contract is " +
|
|
65
|
+
"`async function resolveTenant(request, env)` at module scope",
|
|
66
|
+
});
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (CALL.test(line)) calls.push(i + 1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (declared.length !== 1) {
|
|
73
|
+
problems.push({
|
|
74
|
+
kind: "declaration", line: declared[1] || 0,
|
|
75
|
+
message: `expected exactly 1 declaration of resolveTenant, found ${declared.length}`,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (calls.length !== 1) {
|
|
80
|
+
problems.push({
|
|
81
|
+
kind: "call-sites", line: calls[1] || 0,
|
|
82
|
+
message: `expected exactly 1 call site, found ${calls.length}` +
|
|
83
|
+
(calls.length ? ` (lines ${calls.join(", ")})` : "") +
|
|
84
|
+
" — the seam's whole value is that there is one place to change",
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const fetchOpen = at(FETCH_OPEN);
|
|
89
|
+
if (fetchOpen < 0) {
|
|
90
|
+
problems.push({
|
|
91
|
+
kind: "fetch", line: 0,
|
|
92
|
+
message: "could not find `async fetch(request, env, ctx) {` — this checker is reading the wrong file, " +
|
|
93
|
+
"or the worker's entry point was renamed and this check needs updating with it",
|
|
94
|
+
});
|
|
95
|
+
} else if (calls.length === 1) {
|
|
96
|
+
const call = calls[0];
|
|
97
|
+
const loadConfig = at(LOAD_CONFIG, fetchOpen) + 1;
|
|
98
|
+
if (call <= fetchOpen) {
|
|
99
|
+
problems.push({
|
|
100
|
+
kind: "placement", line: call,
|
|
101
|
+
message: `the call site is at line ${call}, outside fetch() (which opens at ${fetchOpen + 1}) — ` +
|
|
102
|
+
"the tenant is a property of the request and must be resolved where the request arrives",
|
|
103
|
+
});
|
|
104
|
+
} else if (loadConfig > 0 && call > loadConfig) {
|
|
105
|
+
problems.push({
|
|
106
|
+
kind: "placement", line: call,
|
|
107
|
+
message: `the call site (line ${call}) comes AFTER the config load (line ${loadConfig}) — ` +
|
|
108
|
+
"config must be loaded FOR a tenant, so the tenant has to be known first",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return { declared, calls, problems };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- CLI ----------------------------------------------------------------------------
|
|
117
|
+
|
|
118
|
+
function main(argv) {
|
|
119
|
+
const opt = (flag, fallback) => {
|
|
120
|
+
const i = argv.indexOf(flag);
|
|
121
|
+
return i > -1 && argv[i + 1] ? argv[i + 1] : fallback;
|
|
122
|
+
};
|
|
123
|
+
const quiet = argv.includes("--quiet");
|
|
124
|
+
const root = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
|
|
125
|
+
const worker = path.resolve(opt("--worker", path.join(root, "src", "_worker.js")));
|
|
126
|
+
|
|
127
|
+
const from = path.relative(process.cwd(), worker);
|
|
128
|
+
const rel = !from || from.startsWith("..") ? worker : from;
|
|
129
|
+
const { calls, problems } = checkTenantResolver(fs.readFileSync(worker, "utf8"));
|
|
130
|
+
|
|
131
|
+
if (problems.length) {
|
|
132
|
+
for (const p of problems) {
|
|
133
|
+
const where = p.line ? `${rel}:${p.line}` : rel;
|
|
134
|
+
console.error(`${where}: resolveTenant: ${p.message}`);
|
|
135
|
+
}
|
|
136
|
+
console.error(`\n${problems.length} problem(s) — see the header of scripts/one-tenant-resolver.mjs`);
|
|
137
|
+
return 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!quiet) console.log(`${rel}: resolveTenant declared once, called once (line ${calls[0]}), before the config load`);
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
|
145
|
+
process.exit(main(process.argv.slice(2)));
|
|
146
|
+
}
|
package/scripts/open.mjs
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// augur open <prototype> [--new] [--dir <folder>] [--session <label>]
|
|
3
|
+
//
|
|
4
|
+
// Open one prototype into a folder of its own, as a draft that is live at once at its own
|
|
5
|
+
// address. Prints who else is drafting it. The folder holds only that prototype's files and
|
|
6
|
+
// a `.augur/draft.json`; nothing else on this machine is shared with any other session.
|
|
7
|
+
// See docs/drafts-that-land.md §4.
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { target, buildStamp } from "./lib/store.mjs";
|
|
11
|
+
import { markPathFor } from "./lib/marks.mjs";
|
|
12
|
+
import { unitClient, doOpen } from "./lib/draft.mjs";
|
|
13
|
+
import { installAdapters } from "./lib/adapters.mjs";
|
|
14
|
+
import { normUnit } from "../src/unit-core.mjs";
|
|
15
|
+
|
|
16
|
+
const log = (m) => console.error(`\x1b[35m[open]\x1b[0m ${m}`);
|
|
17
|
+
const die = (m) => { console.error(`\x1b[31m[open]\x1b[0m ${m}`); process.exit(1); };
|
|
18
|
+
const argv = process.argv.slice(2);
|
|
19
|
+
const opt = (n) => { const i = argv.indexOf(n); return i > -1 ? argv[i + 1] : null; };
|
|
20
|
+
const positional = argv.filter((a, i) => !a.startsWith("--") && !(i > 0 && argv[i - 1].startsWith("--")));
|
|
21
|
+
|
|
22
|
+
const raw = positional[0];
|
|
23
|
+
if (!raw) die("name a prototype: `augur open <opportunity>/<prototype>` (a folder path works too).");
|
|
24
|
+
const unit = normUnit(markPathFor(raw) || raw);
|
|
25
|
+
if (!unit) die(`"${raw}" is not a prototype path.`);
|
|
26
|
+
let origin, token;
|
|
27
|
+
try { ({ origin, token } = target({ needToken: true })); } catch (e) { die(e.message); }
|
|
28
|
+
// The space id addresses blob uploads. A folder with a space.json names it; otherwise the
|
|
29
|
+
// instance's own build stamp does (one workspace serves one space).
|
|
30
|
+
let space = null;
|
|
31
|
+
try { space = JSON.parse(fs.readFileSync("space.json", "utf8")).id; } catch (e) { /* not in a space folder */ }
|
|
32
|
+
if (!space) { try { space = Object.keys((await buildStamp(origin)).spaces || {})[0] || null; } catch (e) { /* stamp unreachable */ } }
|
|
33
|
+
if (!space) die("could not tell which space this instance serves — run from a folder with space.json, or set AUGUR_ORIGIN.");
|
|
34
|
+
const session = process.env.AUGUR_SESSION || opt("--session") || `session-${process.pid}`;
|
|
35
|
+
const dir = path.resolve(opt("--dir") || unit.split("/").filter(Boolean).pop());
|
|
36
|
+
|
|
37
|
+
const client = unitClient({ origin, token, space, session });
|
|
38
|
+
const isNew = argv.includes("--new");
|
|
39
|
+
const r = await doOpen({ client, unit, dir, origin, space, session, now: new Date().toISOString(), isNew });
|
|
40
|
+
if (!r.ok) {
|
|
41
|
+
if (r.error === "folder-not-empty") die(`${r.dir} is not empty — pick another folder with --dir.`);
|
|
42
|
+
if (r.error === "unknown-unit") die(`${unit} does not exist here. To create it: \`augur open --new ${unit.replace(/^\/|\/$/g, "")}\`.`);
|
|
43
|
+
if (r.error === "unit-exists") die(`${unit} exists already — open it without --new.`);
|
|
44
|
+
if (r.error === "units-not-configured") die("this instance does not serve drafts yet (no unit store bound).");
|
|
45
|
+
if (r.error === "bad-unit" && r.reason === "reserved-folder") die(`${unit} sits under a folder the engine reserves — a prototype lives at <opportunity>/<prototype>.`);
|
|
46
|
+
if (r.error === "bad-unit" && r.reason === "not-a-prototype-folder") die(`${unit} is not a prototype folder — name one as <opportunity>/<prototype>.`);
|
|
47
|
+
die(`could not open: ${r.error || r.status}${r.reason ? ` (${r.reason})` : ""}`);
|
|
48
|
+
}
|
|
49
|
+
log(r.isNew ? `draft ${r.draftId} on ${unit} — a NEW prototype; ${dir} is empty, write its index.html there` : `draft ${r.draftId} on ${unit} — ${r.files} file(s) in ${dir}`);
|
|
50
|
+
// The agent tool's hooks, installed for this machine the first time a draft is opened
|
|
51
|
+
// here (idempotent; `AUGUR_NO_ADAPTERS=1` skips it — the suite and CI set it).
|
|
52
|
+
if (!process.env.AUGUR_NO_ADAPTERS) {
|
|
53
|
+
for (const a of installAdapters()) {
|
|
54
|
+
if (a.result === "installed") log(`${a.name}: editor hooks installed (${a.path}) — edits in a draft folder save on their own; edits to a shared checkout's prototypes are refused.`);
|
|
55
|
+
else if (a.result === "updated") log(`${a.name}: editor hooks updated (${a.path}).`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (r.others.length) {
|
|
59
|
+
log("also drafting this prototype right now:");
|
|
60
|
+
for (const o of r.others) log(` ${o.session || "someone"} (${o.active ? "active" : "idle"})`);
|
|
61
|
+
log("nothing here stops you; if you both land, the second one syncs first.");
|
|
62
|
+
}
|
|
63
|
+
console.log(r.address);
|