@augurworks/augur 0.15.1 → 0.15.2

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/agents/README.md CHANGED
@@ -21,7 +21,7 @@ prototypes needs a **publish token**, and an agent gets one by device pairing, n
21
21
  asking anyone for a password:
22
22
 
23
23
  ```
24
- npx augur connect --origin https://<the workspace>
24
+ npx @augurworks/augur connect --origin https://<the workspace>
25
25
  ```
26
26
 
27
27
  It prints one line to relay — *ask the owner of this workspace to open `<link>` and
@@ -31,7 +31,7 @@ signed in to and types the code; the token lands in `~/.config/augur/tokens.json
31
31
  it finds no token, so inside a workspace tree nothing has to be done first. On a workspace
32
32
  that serves drafts (`/.well-known/augur.json` says `drafts.enabled`), the everyday verbs
33
33
  are `augur open` and `augur land` instead — see [drafts.md](./drafts.md). To get the
34
- tree from a hostname, pair first, then `npx augur clone --space <id>` — it reads the
34
+ tree from a hostname, pair first, then `npx @augurworks/augur clone --space <id>` — it reads the
35
35
  origin from the pairing.
36
36
 
37
37
  Not on npm yet? The engine clone sits next to every workspace that publishes:
package/agents/drafts.md CHANGED
@@ -16,12 +16,6 @@ augur sync # only when land was refused: fold main
16
16
  That is it. There is no commit, no push, no build. The URL `land` prints is what you hand
17
17
  the person you are working for — never a localhost, never a `file://` path.
18
18
 
19
- A prototype that does not exist yet: `augur open --new <opportunity>/<name>` gives you an
20
- empty folder and an empty draft; write its `index.html` and land. The same works for a
21
- library demo (`components/<name>`, `base/…`, `patterns/…`, `pages/…`) and for the
22
- workspace's design system (`skills/<prefix>-ui`), which is one unit like any other.
23
- Without `--new`, a name that does not exist is refused rather than guessed at.
24
-
25
19
  ## What a draft is
26
20
 
27
21
  Your own live copy of one prototype, at its own address: the prototype's URL with `@` and
@@ -3,7 +3,7 @@
3
3
  ## Getting in, if you are an agent
4
4
 
5
5
  You do not need a login. Publishing needs a **publish token**, and the way to get one
6
- is device pairing: `npx augur connect --origin <the workspace>` prints a link and a
6
+ is device pairing: `npx @augurworks/augur connect --origin <the workspace>` prints a link and a
7
7
  code, the owner enters the code in a browser they are already signed in to, and the
8
8
  token lands on this machine. Nobody's password is asked for, typed or stored. The
9
9
  instance says the same at `GET /llms.txt`. The full shape is in [README.md](./README.md)
package/changelog.md CHANGED
@@ -20,6 +20,11 @@ The date is the day the update went live. The page works out "Today / 3 days ago
20
20
 
21
21
  ---
22
22
 
23
+ ## 2026-09-06 — Augur installs from npm
24
+ Your agent can now fetch the Augur tool with one line, `npx @augurworks/augur`, on any
25
+ machine with Node. Nothing to clone first: the workspace's own front door prints that
26
+ line, and the rest is the code you approve in your browser.
27
+
23
28
  ## 2026-09-03 — Your agent finds the door on its own
24
29
  Point an agent at a workspace address and it now learns how to get in without
25
30
  asking you for a password: the site tells it to run `augur connect`, you approve a
package/package.json CHANGED
@@ -1,20 +1,6 @@
1
1
  {
2
2
  "name": "@augurworks/augur",
3
- "version": "0.15.1",
4
- "description": "A prototype and research repository for product teams. Real, clickable prototypes and the design system they are built from, on one site with login, comments and live boards on top. Underneath it is all git and static HTML.",
5
- "license": "MIT",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/andratwiro/augur.git"
9
- },
10
- "homepage": "https://github.com/andratwiro/augur#readme",
11
- "publishConfig": {
12
- "access": "public"
13
- },
14
- "type": "module",
15
- "bin": {
16
- "augur": "scripts/cli.mjs"
17
- },
3
+ "version": "0.15.2",
18
4
  "files": [
19
5
  "scripts/",
20
6
  "build.js",
@@ -31,6 +17,12 @@
31
17
  "TRADEMARKS.md",
32
18
  "changelog.md"
33
19
  ],
20
+ "license": "MIT",
21
+ "description": "A prototype and research repository for product teams. Real, clickable prototypes and the design system they are built from, on one site with login, comments and live boards on top. Underneath it is all git and static HTML.",
22
+ "type": "module",
23
+ "bin": {
24
+ "augur": "scripts/cli.mjs"
25
+ },
34
26
  "scripts": {
35
27
  "test": "node --test \"test/*.test.mjs\"",
36
28
  "smoke": "node scripts/smoke-invite.mjs",
@@ -53,5 +45,13 @@
53
45
  },
54
46
  "devDependencies": {
55
47
  "playwright": "^1.60.0"
56
- }
48
+ },
49
+ "publishConfig": {
50
+ "access": "public"
51
+ },
52
+ "repository": {
53
+ "type": "git",
54
+ "url": "git+https://github.com/andratwiro/augur.git"
55
+ },
56
+ "homepage": "https://github.com/andratwiro/augur#readme"
57
57
  }
@@ -7,7 +7,7 @@
7
7
  * transcript. This asks for neither: it prints a code, you type that code into a browser
8
8
  * that already has your session, and the token approval mints comes back here.
9
9
  *
10
- * npx augur connect [--origin https://your.site]
10
+ * npx @augurworks/augur connect [--origin https://your.site]
11
11
  *
12
12
  * `augur login` stays for CI and scripts, where there is no browser to type into.
13
13
  *
@@ -150,19 +150,11 @@ async function materialise(client, unit, table, dir) {
150
150
  }
151
151
  }
152
152
 
153
- async function doOpenImpl({ client, unit, dir, origin, space, session, now, isNew = false }) {
153
+ async function doOpenImpl({ client, unit, dir, origin, space, session, now }) {
154
154
  if (fs.existsSync(dir) && fs.readdirSync(dir).length) return { ok: false, error: "folder-not-empty", dir };
155
155
  const createdFolder = !fs.existsSync(dir);
156
156
  const o = await client.open({ unit });
157
157
  if (o.status) return { ok: false, ...o };
158
- // A unit with no files is one that does not exist yet. Creating one is a decision the
159
- // caller states with `isNew`; without it, a typo would quietly open an empty draft on a
160
- // prototype that was never there. Either mismatch hands the draft straight back.
161
- const exists = Object.keys(o.table || {}).length > 0;
162
- if (exists !== !isNew) {
163
- try { await client.discard({ unit, draftId: o.draftId }); } catch (e) { /* best-effort */ }
164
- return { ok: false, error: exists ? "unit-exists" : "unknown-unit", unit };
165
- }
166
158
  // From here the server-side draft exists, so any failure below must both undo what we
167
159
  // wrote to disk and tell the server to drop the orphan — otherwise a retry finds a
168
160
  // half-materialised folder (`folder-not-empty`) and the draft it opened is never freed.
@@ -185,7 +177,7 @@ async function doOpenImpl({ client, unit, dir, origin, space, session, now, isNe
185
177
  throw err;
186
178
  }
187
179
  const others = (o.presence || []).filter((d) => d.id !== o.draftId);
188
- return { ok: true, draftId: o.draftId, address: `${origin}${o.address}`, files: Object.keys(o.table).length, others, isNew: !exists };
180
+ return { ok: true, draftId: o.draftId, address: `${origin}${o.address}`, files: Object.keys(o.table).length, others };
189
181
  }
190
182
 
191
183
  async function doSaveImpl({ client, dir, baseRevision, baseTable }) {
@@ -224,7 +224,6 @@ const ALLOWLIST = {
224
224
  "mcpHostAllowlist",
225
225
  "CANVAS_REGISTRY",
226
226
  "PITI_REMARKS",
227
- "DERIVED", // the two store reads a derived gallery needs, per manifest version — the status baseline and the design-system catalog
228
227
  "ROSTER_OVERLAY",
229
228
  "FREEZE_STATE",
230
229
  "SUSPENSION_STATE",
@@ -282,21 +281,6 @@ const ALLOWLIST = {
282
281
  },
283
282
  },
284
283
 
285
- // The derived pages: pure renderers over the live store. Every table here is a fact
286
- // about the engine's vocabulary — tier names, status words and glyphs, the emoji pool a
287
- // card's leading glyph is picked from — the same for every workspace.
288
- "src/galleries.mjs": {
289
- frozen: [
290
- "TIERS", // the four library tiers whose demos are units
291
- "TIER_TITLE", // their titles
292
- "TIER_COPY", // their hints and empty states
293
- "STATUS_META", // status word → label + class
294
- "STATUS_ICONS", // status word → glyph
295
- "STATUS_RANK", // the card order a status implies
296
- "EMOJI_POOL", // the leading-emoji pool, picked by slug hash
297
- ],
298
- },
299
-
300
284
  "src/bundle-keys.mjs": {
301
285
  frozen: [
302
286
  "BUNDLE_TENANCY", // which bundle-store families carry a workspace segment; one word per family is the revert, and a deploy-wide fact — moved here from the worker so the workspace object shares the key shape
package/scripts/open.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- // augur open <prototype> [--new] [--dir <folder>] [--session <label>]
2
+ // augur open <prototype> [--dir <folder>] [--session <label>]
3
3
  //
4
4
  // Open one prototype into a folder of its own, as a draft that is live at once at its own
5
5
  // address. Prints who else is drafting it. The folder holds only that prototype's files and
@@ -35,18 +35,15 @@ const session = process.env.AUGUR_SESSION || opt("--session") || `session-${proc
35
35
  const dir = path.resolve(opt("--dir") || unit.split("/").filter(Boolean).pop());
36
36
 
37
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 });
38
+ const r = await doOpen({ client, unit, dir, origin, space, session, now: new Date().toISOString() });
40
39
  if (!r.ok) {
41
40
  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
41
  if (r.error === "units-not-configured") die("this instance does not serve drafts yet (no unit store bound).");
45
42
  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
43
  if (r.error === "bad-unit" && r.reason === "not-a-prototype-folder") die(`${unit} is not a prototype folder — name one as <opportunity>/<prototype>.`);
47
44
  die(`could not open: ${r.error || r.status}${r.reason ? ` (${r.reason})` : ""}`);
48
45
  }
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}`);
46
+ log(`draft ${r.draftId} on ${unit} — ${r.files} file(s) in ${dir}`);
50
47
  // The agent tool's hooks, installed for this machine the first time a draft is opened
51
48
  // here (idempotent; `AUGUR_NO_ADAPTERS=1` skips it — the suite and CI set it).
52
49
  if (!process.env.AUGUR_NO_ADAPTERS) {
package/seed/README.md CHANGED
@@ -59,7 +59,7 @@ Three things are substituted on the way in:
59
59
 
60
60
  | What | Where | Substituted with |
61
61
  | --- | --- | --- |
62
- | The connect command | `CONNECT_COMMAND` in `start-here/prototypes/connect-your-terminal/index.html` | `npx augur connect --origin https://<label><suffix>`, the workspace's real address, filled the moment the page is published |
62
+ | The connect command | `CONNECT_COMMAND` in `start-here/prototypes/connect-your-terminal/index.html` | `npx @augurworks/augur connect --origin https://<label><suffix>`, the workspace's real address, filled the moment the page is published |
63
63
  | Comment timestamps | `at` in `threads.json` | the provisioning time, so day-one threads do not read as months old |
64
64
  | File timestamps | `editedAt` on every file in the manifest | the same provisioning time, all of them |
65
65
 
@@ -91,7 +91,7 @@
91
91
  <h2 class="s-h2">Run this where your agent runs</h2>
92
92
  <p class="s-prose">In the folder you want this workspace to live in, run:</p>
93
93
  <div class="s-cmd">
94
- <code class="s-cmd__text" id="cmd-connect">npx augur connect your-workspace</code>
94
+ <code class="s-cmd__text" id="cmd-connect">npx @augurworks/augur connect your-workspace</code>
95
95
  <button type="button" class="s-cmd__btn" data-s-copy="#cmd-connect">Copy</button>
96
96
  </div>
97
97
  <p class="s-prose" style="margin-top: var(--starter-s4)">A browser tab opens and asks you to
@@ -105,7 +105,7 @@
105
105
  <div>
106
106
  <p class="ask__why">Paste this into your agent instead of running the command yourself.</p>
107
107
  <div class="s-cmd s-cmd--paste">
108
- <code class="s-cmd__text s-cmd__text--plain" id="ask-agent">Run `npx augur connect your-workspace` in this folder. It will open a browser tab for me to approve — tell me when it is waiting, and tell me exactly what it prints when it finishes.</code>
108
+ <code class="s-cmd__text s-cmd__text--plain" id="ask-agent">Run `npx @augurworks/augur connect your-workspace` in this folder. It will open a browser tab for me to approve — tell me when it is waiting, and tell me exactly what it prints when it finishes.</code>
109
109
  <button type="button" class="s-cmd__btn" data-s-copy="#ask-agent">Copy</button>
110
110
  </div>
111
111
  </div>
@@ -311,7 +311,7 @@
311
311
  <p class="s-prose">Last one. Ask the agent to connect, in plain language — it runs the
312
312
  command for you:</p>
313
313
  <div class="s-cmd s-cmd--paste">
314
- <code class="s-cmd__text s-cmd__text--plain" id="ask-5-do">Run `npx augur connect your-workspace` in this folder. It will open a browser tab for me to approve — tell me when it is waiting, and tell me exactly what it prints when it finishes.</code>
314
+ <code class="s-cmd__text s-cmd__text--plain" id="ask-5-do">Run `npx @augurworks/augur connect your-workspace` in this folder. It will open a browser tab for me to approve — tell me when it is waiting, and tell me exactly what it prints when it finishes.</code>
315
315
  <button type="button" class="s-cmd__btn" data-s-copy="#ask-5-do">Copy</button>
316
316
  </div>
317
317
  <p class="s-prose">Approve the tab that opens. The terminal now has a key of its own, so
@@ -369,7 +369,7 @@
369
369
  var host = location.hostname || "";
370
370
  var label = host.split(".")[0];
371
371
  var named = host.indexOf(".") > 0 && label && label !== "localhost" && !/^\d+$/.test(label);
372
- return named ? "npx augur connect --origin https://" + host : "npx augur connect your-workspace";
372
+ return named ? "npx @augurworks/augur connect --origin https://" + host : "npx @augurworks/augur connect your-workspace";
373
373
  }
374
374
 
375
375
  var cmd = connectCommand();
@@ -377,7 +377,7 @@
377
377
  if (cmdEl) cmdEl.textContent = cmd;
378
378
  ["ask-agent", "ask-5-do"].forEach(function (id) {
379
379
  var el = document.getElementById(id);
380
- if (el) el.textContent = el.textContent.replace(/npx augur connect [\w-]+/, cmd);
380
+ if (el) el.textContent = el.textContent.replace(/npx @augurworks/augur connect [\w-]+/, cmd);
381
381
  });
382
382
 
383
383
  /* ── Which path you are on ────────────────────────────────────────────────
package/src/_worker.js CHANGED
@@ -104,10 +104,6 @@ import { PURGED_AUTHOR, purgeThreads, idCollisions } from "./purge.mjs";
104
104
  import { authoredUnits, unitOfPath, unitPaths } from "./publish-units.mjs";
105
105
  import { composePublish, forkLanded } from "./publish-compose.mjs";
106
106
  import { normUnit, splitDraftPath, unitTable, draftAddress, DRAFT_ID_RE } from "./unit-core.mjs";
107
- import {
108
- siteModel, derivedPathKind, renderRootIndex, renderOpportunityIndex, renderPlaygroundIndex,
109
- renderTierIndex, renderComponentsIndex, searchIndex, catalogFrom,
110
- } from "./galleries.mjs";
111
107
  // `F-fork-verb`. Fork as a deliberate verb — one unit aliased to a new path, zero bytes
112
108
  // moved — plus the rule that keeps a fork's lineage and owner alive across every later
113
109
  // publish. Both are pure, and both are the CLI's too if it ever needs them.
@@ -4337,7 +4333,7 @@ function resolveBundlePath(manifests, pathname) {
4337
4333
  // against. It is the first argument for the same reason `loadTenantContext` takes it
4338
4334
  // first: everything below reads one workspace's store, and a function that had to guess
4339
4335
  // would resolve a path against whatever the isolate happened to hold.
4340
- async function assetFetch(tenantId, env, request, opts = {}) {
4336
+ async function assetFetch(tenantId, env, request) {
4341
4337
  if (!bundleMode(env)) return env.ASSETS.fetch(request);
4342
4338
  const url = new URL(request.url);
4343
4339
  // ── a draft address: the draft's table, not the manifest ─────────────────────────
@@ -4356,22 +4352,6 @@ async function assetFetch(tenantId, env, request, opts = {}) {
4356
4352
  return blobResponse(env, request, f);
4357
4353
  }
4358
4354
  const manifests = await loadManifests(tenantId, env);
4359
- // ── a design-system draft, across the site (`?ds=<draft>`, §5) ──────────────────
4360
- // A path under the workspace's declared skill folder resolves from THAT draft's table
4361
- // when the request carries one, so a shared change can be looked at on every prototype
4362
- // before it lands. Only the skill's own paths: a prototype's files never come from it,
4363
- // and a draft that does not hold the file falls through to main.
4364
- if (opts.dsDraft && DRAFT_ID_RE.test(opts.dsDraft) && decoded) {
4365
- const skill = Object.values(manifests)
4366
- .flatMap((m) => ((m && m.routing) || {}).publicSkillPrefixes || [])
4367
- .map(normUnit).find((p) => p && decoded.startsWith(p));
4368
- const stub = skill ? unitStub(env, tenantId, skill) : null;
4369
- if (stub) {
4370
- const dr = await unitCall(stub, `/draft/${opts.dsDraft}`, null, "GET");
4371
- const f = dr.status === 200 && dr.body.table ? dr.body.table[decoded] : null;
4372
- if (f) return blobResponse(env, request, f);
4373
- }
4374
- }
4375
4355
  const r = resolveBundlePath(manifests, url.pathname);
4376
4356
  if (r.redirect) return Response.redirect(new URL(r.redirect + url.search, url).toString(), 308);
4377
4357
  if (r.miss) return new Response("Not Found", { status: 404 });
@@ -5049,23 +5029,21 @@ const UNIT_CT_RE = /^[\w.+-]+\/[\w.+-]+(; ?charset=[\w-]+)?$/i;
5049
5029
  // to anonymous visitors, from a draft on a folder nobody edits.
5050
5030
  const isNewUnitPath = (unit) => unit.split("/").filter(Boolean).length === 2;
5051
5031
  // Top-level folders a NEW unit may not be created under. A landing replaces a unit's
5052
- // folder wholesale, so the folders whose contents are GENERATED the tokens page, the
5053
- // primitives gallery, the changelog, the search index, fonts, engine chrome (`_`, `__`) —
5054
- // are never a unit. The four library tiers are NOT here: `/components/button/` is one
5055
- // demo folder, a unit like any prototype, and the tier's own index is a sibling file the
5056
- // derived renderer owns. `skills` stays reserved with ONE exception, decided where the
5057
- // workspace's declared design system is known (`unitApi`): the skill folder the space
5058
- // names is the design-system unit. Stateless and the same on every workspace. A unit the
5059
- // manifest ALREADY declares is never held to this: the manifest is the authority.
5032
+ // folder wholesale, so a draft opened on `/components/button/` or `/skills/x-ui/` would
5033
+ // have replaced the design system's or the gallery's own files with whatever one draft
5034
+ // held. These are the folders a site builds from and every prototype reads: the design
5035
+ // system, the gallery pages, the changelog, the search index, engine chrome (`_`, `__`).
5036
+ // Stateless and the same on every workspace an opportunity folder is anything else,
5037
+ // `/playground/<name>/` included. A unit the manifest ALREADY declares is never held to
5038
+ // this: the manifest is the authority on what exists.
5060
5039
  const RESERVED_UNIT_FOLDERS = Object.freeze([
5061
- "skills", "tokens", "primitives", "fonts", "tracks", "admin", "changelog", "search",
5040
+ "base", "components", "pages", "patterns", "skills", "tokens", "fonts",
5041
+ "admin", "changelog", "search",
5062
5042
  ]);
5063
5043
  const isReservedUnitFolder = (unit) => {
5064
5044
  const first = unit.split("/").filter(Boolean)[0] || "";
5065
5045
  return first.startsWith("_") || RESERVED_UNIT_FOLDERS.includes(first);
5066
5046
  };
5067
- /** The one unit allowed under `skills/`: the design system the workspace declares. */
5068
- const isDeclaredSkillUnit = (unit, tctx) => ((tctx && tctx.PUBLIC_SKILL_PREFIXES) || []).some((p) => normUnit(p) === unit);
5069
5047
  const shortText = (s, n) => String(s == null ? "" : s).slice(0, n);
5070
5048
  // `tctx.SPACES` is a routing field, filled from the live manifests by `loadTenantContext`
5071
5049
  // on the request's normal path — which this route also goes through, since `handleRequest`
@@ -5162,11 +5140,7 @@ async function writeUnitLanding(tctx, env, spaceId, unit, table, changed, who, n
5162
5140
  // A unit the manifest already declares keeps the entry it has, spelling and all: the
5163
5141
  // set union that replaced it added a second, normalized entry beside one written
5164
5142
  // without its trailing slash, and two entries for one unit is two things to prune.
5165
- // The design-system unit is the one landing that adds NO public prefix: its public
5166
- // surface is the rendered-asset rule `publicSkillPrefixes` already spells, and a whole
5167
- // skill folder opened to the gate would expose the documents that rule keeps gated.
5168
- const skillUnit = ((cur.routing || {}).publicSkillPrefixes || []).some((p) => normUnit(p) === unit);
5169
- routing.publicPrefixes = authoredUnits(cur).has(unit) || skillUnit
5143
+ routing.publicPrefixes = authoredUnits(cur).has(unit)
5170
5144
  ? [...(routing.publicPrefixes || [])]
5171
5145
  : [...(routing.publicPrefixes || []), unit];
5172
5146
  routing.unitSources = { ...(routing.unitSources || {}), [unit]: { sha: null, dirty: false, landed: true, by: who.personId, at: now } };
@@ -5323,7 +5297,7 @@ async function unitApi(tctx, request, url, env) {
5323
5297
  // reached, so a folder that is not one is never given a draft to be adopted from later.
5324
5298
  // A unit the space ALREADY publishes is a unit whatever its shape (the manifest is the
5325
5299
  // authority on what exists); anything else has to be the shape a new one is created in.
5326
- if (!authoredUnits(live).has(unit) && !isDeclaredSkillUnit(unit, tctx)) {
5300
+ if (!authoredUnits(live).has(unit)) {
5327
5301
  if (!isNewUnitPath(unit)) return jsonResponse({ error: "bad-unit", reason: "not-a-prototype-folder" }, 400);
5328
5302
  if (isReservedUnitFolder(unit)) return jsonResponse({ error: "bad-unit", reason: "reserved-folder" }, 400);
5329
5303
  }
@@ -5975,7 +5949,7 @@ async function publishApi(tctx, request, url, env) {
5975
5949
  clientProtocol,
5976
5950
  minProtocol: tctx.MIN_CLIENT_PROTOCOL,
5977
5951
  protocol: PUBLISH_PROTOCOL,
5978
- upgrade: "npx augur@latest",
5952
+ upgrade: "npx @augurworks/augur@latest",
5979
5953
  }, 426);
5980
5954
  }
5981
5955
  // Prune any publicPrefixes entry THIS manifest declares without backing it with
@@ -7264,94 +7238,6 @@ async function composeChrome(tctx, res, url) {
7264
7238
  return new Response(html, { status: res.status, statusText: res.statusText, headers });
7265
7239
  }
7266
7240
 
7267
- // ---- Derived pages (drafts that land, §6.4) -------------------------------------
7268
- // Where drafts are served, the gallery, each opportunity's index, the playground, the
7269
- // library tiers and the finder's index are RENDERED HERE from the live store, so a landing
7270
- // is on them at once and no client build ships them. The stored copies a publish once
7271
- // baked are ignored on this path — they cannot know about a landing. Where drafts are not
7272
- // served, nothing here runs and the stored pages serve exactly as before.
7273
- //
7274
- // Two store reads feed a render besides the manifest — the status baseline
7275
- // (`/prototype-status.json`) and the design-system catalog (`/registry.json`) — cached per
7276
- // manifest version; the status OVERLAY is read fresh every time, one get, as the status
7277
- // route itself reads it. Per-request work after that is string rendering.
7278
- const DERIVED = tenantCache("derived");
7279
- const DERIVED_PATH_RE = /^\/(?:[^/]+\/?(?:index\.html)?|index\.html|__search\.json)?$/;
7280
- async function derivedInputs(tctx, env, spaceId, manifest) {
7281
- const slot = DERIVED.entry(tctx.tenantId, () => ({ key: null, baseline: {}, catalog: {} }));
7282
- const key = `${spaceId}:${manifest.version}`;
7283
- if (slot.key === key) return slot;
7284
- const readJson = async (p) => {
7285
- const f = (manifest.files || {})[p];
7286
- if (!f || !env.BUNDLES) return null;
7287
- try {
7288
- const obj = await env.BUNDLES.get("blobs/" + f.h);
7289
- return obj ? JSON.parse(await obj.text()) : null;
7290
- } catch (e) { return null; }
7291
- };
7292
- const base = await readJson("/prototype-status.json");
7293
- const baseline = {};
7294
- for (const [k, v] of Object.entries(base && typeof base === "object" ? base : {})) {
7295
- if (!k.startsWith("_") && typeof v === "string") baseline[k] = v;
7296
- }
7297
- slot.baseline = baseline;
7298
- slot.catalog = catalogFrom(await readJson("/registry.json"));
7299
- slot.key = key;
7300
- return slot;
7301
- }
7302
- /** A derived page for this path, or null when the path is not one (or drafts are not served here). */
7303
- async function derivedPage(tctx, env, url) {
7304
- if (!draftsServedHere(env) || !DERIVED_PATH_RE.test(url.pathname)) return null;
7305
- const manifests = await loadManifests(tctx.tenantId, env);
7306
- const spaceId = defaultSpaceIdFromCtx(tctx) || defaultSpaceIdFromManifests(manifests);
7307
- const manifest = spaceId ? manifests[spaceId] : null;
7308
- if (!manifest || !manifest.files) return null;
7309
- const inputs = await derivedInputs(tctx, env, spaceId, manifest);
7310
- const store = overlayFor(env, tctx);
7311
- let statuses = {};
7312
- try { statuses = store ? (await store.read("statuses")) || {} : {}; } catch (e) { statuses = {}; }
7313
- // A recorded author resolves to a face through the roster; an id nobody answers to
7314
- // (an ex-member, the `live` adoption) is nobody's face rather than a blank chip.
7315
- const people = (id) => { const f = personFace(tctx.USERS, id); return f.name ? { id, ...f } : null; };
7316
- const now = Date.now();
7317
- const model = siteModel({ manifest, statuses, baseline: inputs.baseline, people, now });
7318
- const kind = derivedPathKind(url.pathname, model);
7319
- if (!kind) return null;
7320
- const sp = (tctx.SPACES || []).find((s) => s.id === spaceId) || {};
7321
- const ctx = { spaces: tctx.SPACES || [], activeSpace: spaceId, chrome: tctx.CHROME_POINTER || null, projectsLabel: sp.projectsLabel || "", now };
7322
- if (kind.kind === "search") return jsonResponse(searchIndex(model, ctx), 200, { "Cache-Control": "no-cache" });
7323
- if (kind.kind === "opportunity" && !kind.slash) return Response.redirect(new URL(`/${encodeURIComponent(kind.name)}/${url.search}`, url).toString(), 308);
7324
- const html = kind.kind === "root" ? renderRootIndex(model, ctx)
7325
- : kind.kind === "opportunity" ? renderOpportunityIndex(model, kind.name, ctx)
7326
- : kind.kind === "playground" ? renderPlaygroundIndex(model, ctx)
7327
- : kind.kind === "tier" ? renderTierIndex(model, kind.tier, ctx)
7328
- : kind.kind === "components" ? renderComponentsIndex(model, inputs.catalog, ctx)
7329
- : null;
7330
- if (html == null) return null;
7331
- return new Response(html, { status: 200, headers: { "Content-Type": "text/html; charset=utf-8", "Cache-Control": ASSET_REVALIDATE } });
7332
- }
7333
-
7334
- // ---- The design-system overlay cookie (`?ds=`) --------------------------------
7335
- // `?ds=<draft>` on any page names a design-system draft to look through; the cookie keeps
7336
- // it while the person navigates; `?ds=` with nothing drops it. A malformed id is nobody's
7337
- // draft and touches no cookie. Members only — the branch that reads this is past the gate.
7338
- const DS_COOKIE = "augur_ds";
7339
- function dsOverlay(request, url) {
7340
- if (url.searchParams.has("ds")) {
7341
- const v = url.searchParams.get("ds") || "";
7342
- if (v === "") return { draft: null, setCookie: `${DS_COOKIE}=; Path=/; Max-Age=0; SameSite=Lax; Secure; HttpOnly` };
7343
- if (!DRAFT_ID_RE.test(v)) return { draft: null, setCookie: null };
7344
- return { draft: v, setCookie: `${DS_COOKIE}=${v}; Path=/; SameSite=Lax; Secure; HttpOnly` };
7345
- }
7346
- const c = cookieValue(request.headers.get("Cookie") || "", DS_COOKIE);
7347
- return { draft: c && DRAFT_ID_RE.test(c) ? c : null, setCookie: null };
7348
- }
7349
- function withSetCookie(res, cookie) {
7350
- const out = new Response(res.body, res);
7351
- out.headers.append("Set-Cookie", cookie);
7352
- return out;
7353
- }
7354
-
7355
7241
  // ---- The draft bar (drafts that land, §5) -------------------------------------
7356
7242
  // A prototype is raw HTML from a space and carries no chrome; a draft has no build step
7357
7243
  // that could bake one in. So a member looking at a unit's page — main or a draft address —
@@ -9149,7 +9035,7 @@ function doorFacts(tctx, url, env) {
9149
9035
  origin: url.origin,
9150
9036
  engine: tctx.INSTANCE_ENGINE_VERSION ? { version: tctx.INSTANCE_ENGINE_VERSION } : {},
9151
9037
  pairing: { enabled: pairing, start: "/__publish/_pair/start", approve: "/__connect" },
9152
- connect: pairing ? `npx augur connect --origin ${url.origin}` : null,
9038
+ connect: pairing ? `npx @augurworks/augur connect --origin ${url.origin}` : null,
9153
9039
  docs: DOOR_DOCS,
9154
9040
  // Where drafts are served, a prototype is changed by opening it, not by shipping a
9155
9041
  // tree — and the door is where a cold agent learns which of the two this instance is.
@@ -9170,7 +9056,7 @@ function doorText(f) {
9170
9056
  + `It prints a link and a code. The owner of this workspace opens the link in a\n`
9171
9057
  + `browser they are already signed in to and enters the code. The token lands on\n`
9172
9058
  + `that machine, and \`augur publish\` / \`augur ship\` use it from then on. With no\n`
9173
- + `source tree yet, \`npx augur clone --space ${f.workspace}\` then fetches one (it reads\n`
9059
+ + `source tree yet, \`npx @augurworks/augur clone --space ${f.workspace}\` then fetches one (it reads\n`
9174
9060
  + `the origin from the pairing).\n\n`
9175
9061
  + `Not on npm yet? The engine clone sits next to every workspace that publishes:\n\n`
9176
9062
  + ` node <engine>/scripts/cli.mjs connect --origin ${f.origin}\n\n`
@@ -12547,20 +12433,13 @@ async function handleRequest(request, env, ctx, url, trace) {
12547
12433
  // Past the gate (or nothing gates the site) → serve. A 404 gets one more chance
12548
12434
  // as a created canvas (a KV-registered board with no repo file — see canvasesApi).
12549
12435
  if (authed) {
12550
- // A design-system draft to look through (`?ds=`), remembered in a cookie.
12551
- const ds = dsOverlay(request, url);
12552
- const stamp = (res) => (ds.setCookie ? withSetCookie(res, ds.setCookie) : res);
12553
- // Where drafts are served, the gallery and its indexes are derived from the live
12554
- // store rather than read from it — a landing is on them at once.
12555
- const derived = await derivedPage(tctx, env, url);
12556
- if (derived) return stamp(derived.status === 308 ? derived : await serveContent(tctx, derived, url, me, env));
12557
- const asset = await assetFetch(tctx.tenantId, env, request, { dsDraft: ds.draft });
12436
+ const asset = await assetFetch(tctx.tenantId, env, request);
12558
12437
  if (asset.status === 404) {
12559
12438
  const virt = await virtualCanvas(tctx, request, env, url);
12560
- if (virt) return stamp(virt);
12561
- return stamp(notFoundResponse(tctx));
12439
+ if (virt) return virt;
12440
+ return notFoundResponse(tctx);
12562
12441
  }
12563
- return stamp(await serveContent(tctx, asset, url, me, env));
12442
+ return serveContent(tctx, asset, url, me, env);
12564
12443
  }
12565
12444
 
12566
12445
  // Created canvas boards are public like published prototypes — same obscure
@@ -12645,7 +12524,7 @@ export const __testables = Object.freeze({
12645
12524
  doorFacts, doorText, wantsMachineDoor, gateResponse, DOOR_DOCS, DOOR_WELL_KNOWN,
12646
12525
  resumeAfterDormancy,
12647
12526
  PITI_VIEW_KEY, PITI_REMARKS_KEY,
12648
- publishAuthDetailed, unitApi, unitCaller, personFace, withDraftUi, draftUiBoot, derivedPage, dsOverlay, isEngineChrome, publishRefusalBody, splitDraftPath,
12527
+ publishAuthDetailed, unitApi, unitCaller, personFace, withDraftUi, draftUiBoot, isEngineChrome, publishRefusalBody, splitDraftPath,
12649
12528
  adminStorageApi,
12650
12529
  adminCustomDomainApi,
12651
12530
  isPrefixBacked, backedPublicPrefixes,
package/src/seed-pack.mjs CHANGED
@@ -44,7 +44,7 @@
44
44
  //
45
45
  // ONE SUBSTITUTION. connect-your-terminal ships with an empty `CONNECT_COMMAND` slot and
46
46
  // derives a command from the URL it is served on. Provisioning fills the slot with the
47
- // workspace's REAL command — `npx augur connect --origin https://<label><suffix>` — so the
47
+ // workspace's REAL command — `npx @augurworks/augur connect --origin https://<label><suffix>` — so the
48
48
  // page is exact rather than merely not wrong. The filled page hashes differently, so it is
49
49
  // the one blob that is per-workspace rather than shared; everything else dedups.
50
50
 
@@ -60,7 +60,7 @@ export const SEED_CONNECT_SLOT = 'var CONNECT_COMMAND = "";';
60
60
  /** The one-line command a person runs to pair a terminal with their workspace. */
61
61
  export function connectCommandFor(origin) {
62
62
  const o = String(origin || "").replace(/\/+$/, "");
63
- return o ? `npx augur connect --origin ${o}` : "";
63
+ return o ? `npx @augurworks/augur connect --origin ${o}` : "";
64
64
  }
65
65
 
66
66
  /**
package/src/galleries.mjs DELETED
@@ -1,400 +0,0 @@
1
- // galleries.mjs — the pages a workspace DERIVES, rendered at serve time from the live store.
2
- //
3
- // docs/drafts-that-land.md §6.4. The gallery, each opportunity's index, the playground, the
4
- // library tiers and the search index used to be baked by a client's build and shipped with a
5
- // publish, so a landing could not appear on them until somebody published a tree. Here they
6
- // are a pure function of what the store holds: the live manifest (units, files, per-file
7
- // provenance), the status overlay, the roster (ids → faces) and the design-system catalog.
8
- // The worker calls this where drafts are served; the markup keeps the card contract the
9
- // chrome bundle's scripts read (class names and data-* attributes), so pins, renames, status
10
- // clicks, currency, faces and the finder keep working on a derived page exactly as they did
11
- // on a baked one.
12
- //
13
- // ⚠️ NO NODE IMPORTS, NO I/O. The worker runs this per request.
14
- import { renderAppChrome, renderSpaceContextScript, CHROME_MARK_START, CHROME_MARK_END, UI_VERSION, escAttr, titleCase, fmtDate, relTime } from "./chrome/appchrome.mjs";
15
- import { authoredUnits } from "./publish-units.mjs";
16
- import { unitProvenance, unitKey, STATUS_LABELS } from "./currency.mjs";
17
-
18
- export const TIERS = Object.freeze(["base", "components", "patterns", "pages"]);
19
- const TIER_TITLE = Object.freeze({ base: "Base", components: "Components", patterns: "Patterns", pages: "Pages" });
20
-
21
- const STATUS_META = Object.freeze({
22
- "in-progress": { label: STATUS_LABELS["in-progress"], cls: "is-wip" },
23
- "dev-ready": { label: STATUS_LABELS["dev-ready"], cls: "is-ready" },
24
- ignore: { label: STATUS_LABELS.ignore, cls: "is-ignore" },
25
- });
26
- const STATUS_ICONS = Object.freeze({
27
- "dev-ready": '<svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="9" fill="#17935a"/><path d="M5.8 10.4l2.7 2.7 5.7-6" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>',
28
- "in-progress": '<svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8" fill="none" stroke="#1c1c22" stroke-width="2.2"/><path d="M10 2.8a7.2 7.2 0 0 1 0 14.4z" fill="#1c1c22"/></svg>',
29
- ignore: '<svg viewBox="0 0 20 20" aria-hidden="true"><circle cx="10" cy="10" r="8" fill="none" stroke="#aeb3bd" stroke-width="2.2"/><line x1="6.4" y1="10" x2="13.6" y2="10" stroke="#aeb3bd" stroke-width="2.2" stroke-linecap="round"/></svg>',
30
- });
31
- const STATUS_RANK = Object.freeze({ "dev-ready": 0, "in-progress": 1, ignore: 2 });
32
- const IC_STAR = `<svg class="pin-star" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z"/></svg>`;
33
- // Spelled out rather than built with `ic()`: the module-scope lint reads a call initializer
34
- // as state it cannot prove pure, and a literal costs nothing.
35
- const IC_PLUS = `<svg class="gvic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14"/><path d="M5 12h14"/></svg>`;
36
- const EMOJI_POOL = Object.freeze(["🗳️", "🏛️", "📊", "🧭", "🛰️", "🧩", "🪧", "🌳", "🚲", "📣", "🗺️", "🧪", "💡", "🔭", "🪟", "🧱", "🎛️", "🛣️", "🧰", "📐", "🧮", "🗂️", "🔔", "🏘️", "🌍", "💬", "📝", "🚏", "🏙️", "🌿", "🎚️", "🧷"]);
37
-
38
- const dec = (s) => { try { return decodeURIComponent(String(s)); } catch (e) { return String(s); } };
39
- const enc = (s) => encodeURIComponent(s);
40
- const plural = (n, word) => `${n} ${n === 1 ? word : word.endsWith("y") ? word.slice(0, -1) + "ies" : word + "s"}`;
41
- function protoEmoji(slug) {
42
- let h = 0;
43
- for (let i = 0; i < slug.length; i++) h = (h * 31 + slug.charCodeAt(i)) >>> 0;
44
- return EMOJI_POOL[h % EMOJI_POOL.length];
45
- }
46
- const protoName = (slug) => `${protoEmoji(slug)} ${titleCase(slug)}`;
47
-
48
- // ── the site model ───────────────────────────────────────────────────────────
49
- /** Which list a unit belongs to, from its path alone. */
50
- export function unitHome(unit) {
51
- const segs = dec(unit).split("/").filter(Boolean);
52
- if (segs.length !== 2) return null;
53
- if (segs[0] === "playground") return { kind: "playground", name: segs[1] };
54
- if (TIERS.includes(segs[0])) return { kind: "tier", tier: segs[0], name: segs[1] };
55
- if (segs[0].startsWith("_") || segs[0] === "skills") return null;
56
- return { kind: "opportunity", opp: segs[0], name: segs[1] };
57
- }
58
-
59
- function unitEntry(manifest, unit, home, { statuses, baseline, people, now }) {
60
- const files = manifest.files || {};
61
- const prefix = dec(unit);
62
- const inUnit = Object.keys(files).filter((p) => dec(p).startsWith(prefix));
63
- const rel = (p) => dec(p).slice(prefix.length);
64
- const hasIndex = inUnit.some((p) => rel(p) === "index.html");
65
- const firstHtml = inUnit.map(rel).filter((r) => !r.includes("/") && r.endsWith(".html")).sort()[0] || null;
66
- const file = hasIndex ? `${unit}index.html` : firstHtml ? `${unit}${enc(firstHtml)}` : null;
67
- const { editedAt, by } = unitProvenance(manifest, unit);
68
- const key = unitKey(unit);
69
- const status = (statuses && Object.prototype.hasOwnProperty.call(statuses, key) && statuses[key])
70
- || (baseline && baseline[key]) || null;
71
- const ids = [...new Set(inUnit.map((p) => files[p] && files[p].by).filter(Boolean))];
72
- const editors = ids.map((id) => people(id)).filter(Boolean);
73
- return {
74
- name: home.name, unit, href: unit, file,
75
- poster: inUnit.some((p) => rel(p) === "preview.webp"),
76
- editedAt: editedAt || null,
77
- mtimeMs: editedAt ? Date.parse(editedAt) : 0,
78
- status, editors, by,
79
- };
80
- }
81
-
82
- const byRecency = (a, b) => b.mtimeMs - a.mtimeMs || a.name.localeCompare(b.name);
83
- const byStatusThenRecency = (a, b) => {
84
- const ra = a.status in STATUS_RANK ? STATUS_RANK[a.status] : 2;
85
- const rb = b.status in STATUS_RANK ? STATUS_RANK[b.status] : 2;
86
- return ra - rb || byRecency(a, b);
87
- };
88
-
89
- /**
90
- * Everything the derived pages need, from the live manifest and the overlays.
91
- * `people(id) → {id, name, initials, color} | null` resolves a recorded author id.
92
- */
93
- export function siteModel({ manifest, statuses = {}, baseline = {}, people = () => null, now = Date.now() }) {
94
- const opps = new Map(), playground = [], tiers = { base: [], components: [], patterns: [], pages: [] };
95
- const units = [];
96
- for (const unit of authoredUnits(manifest || {})) {
97
- const home = unitHome(unit);
98
- if (!home) continue;
99
- const e = unitEntry(manifest, unit, home, { statuses, baseline, people, now });
100
- units.push({ ...e, home });
101
- if (home.kind === "playground") { playground.push({ ...e, status: e.status || "in-progress" }); continue; }
102
- if (home.kind === "tier") { tiers[home.tier].push(e); continue; }
103
- if (!opps.has(home.opp)) opps.set(home.opp, { name: home.opp, prototypes: [] });
104
- opps.get(home.opp).prototypes.push(e);
105
- }
106
- const opportunities = [...opps.values()].map((o) => {
107
- o.prototypes.sort(byStatusThenRecency);
108
- const seen = new Map();
109
- for (const p of o.prototypes) for (const u of p.editors) if (!seen.has(u.id)) seen.set(u.id, u);
110
- return { ...o, people: [...seen.values()], mtimeMs: Math.max(0, ...o.prototypes.map((p) => p.mtimeMs)) };
111
- }).sort(byRecency);
112
- playground.sort(byStatusThenRecency);
113
- for (const t of TIERS) tiers[t].sort(byRecency);
114
- return { opportunities, playground, tiers, hasPlayground: playground.length > 0, units };
115
- }
116
-
117
- /** What a request path names on the derived surface, or null for anything else. */
118
- export function derivedPathKind(pathname, model) {
119
- const p = dec(pathname);
120
- if (p === "/" || p === "/index.html") return { kind: "root" };
121
- if (p === "/__search.json") return { kind: "search" };
122
- const m = /^\/([^/]+)\/?(index\.html)?$/.exec(p);
123
- if (!m) return null;
124
- const seg = m[1];
125
- if (seg === "playground") return model.hasPlayground || p.endsWith("/") ? { kind: "playground" } : null;
126
- if (TIERS.includes(seg)) return { kind: seg === "components" ? "components" : "tier", tier: seg };
127
- if (model.opportunities.some((o) => o.name === seg)) return { kind: "opportunity", name: seg, slash: p.endsWith("/") || p.endsWith("index.html") };
128
- return null;
129
- }
130
-
131
- // ── markup ───────────────────────────────────────────────────────────────────
132
- const media = (href, hasPoster) => hasPoster
133
- ? `<img class="preview-img" src="${href}preview.webp" alt="" aria-hidden="true" loading="lazy" decoding="async" width="768" height="480" />`
134
- : `<div class="preview-ph" aria-hidden="true"></div>`;
135
- const preview = (href, hasPoster) => `<div class="preview${hasPoster ? "" : " preview--ph"}">${media(href, hasPoster)}</div>`;
136
- const filterEmpty = () => `<p class="filter-empty" data-filter-empty hidden>No matches.</p>`;
137
- const pinStar = (key, href) => `<button type="button" class="pin-btn" data-pin-key="${key}" data-pin-href="${href}" aria-pressed="false" aria-label="Pin to sidebar" title="Pin to sidebar">${IC_STAR}</button>`;
138
- const newCanvasBtn = (dir) => `<button type="button" class="folderbar__new" data-new-canvas="${dir}" hidden>${IC_PLUS}New canvas</button>`;
139
- const emptyState = (...paras) => paras.map((p) => `<p class="empty">${p}</p>`).join("");
140
- const emptyHead = (title, keepOnMobile = false) =>
141
- `<header class="folderbar${keepOnMobile ? " folderbar--keep" : ""}"><h1 class="folderbar__title">${title}</h1><span class="folderbar__count">0</span><span class="folderbar__rule"></span></header>`;
142
- const ghostLine = (w) => `<span class="ghost-line" style="width:${w}"></span>`;
143
- const ghosts = (inner) => `<div class="ghosts" aria-hidden="true">${inner}</div>`;
144
- const ghostFolderGrid = (n = 6) => ghosts(`<div class="opp-grid">${Array.from({ length: n }, () => `<div class="card-opp"><div class="preview preview--pending"></div><div class="proto-meta"><div class="proto-text">${ghostLine("58%")}${ghostLine("30%")}</div></div></div>`).join("")}</div>`);
145
- const ghostCardGrid = (n = 8) => ghosts(`<div class="page-grid">${Array.from({ length: n }, () => `<div class="card-proto is-pending"><div class="preview preview--pending"></div><div class="proto-meta">${ghostLine("56%")}</div></div>`).join("")}</div>`);
146
- const ghostCompTable = (n = 4) => ghosts(`<table class="comp-table"><thead><tr><th>Preview</th><th>Component</th><th>What it is</th><th class="comp-status">Status</th></tr></thead><tbody>${Array.from({ length: n }, () => `<tr><td><span class="ghost-thumb"></span></td><td><div class="comp-name">${ghostLine("110px")}</div><div class="comp-badges"><span class="ghost-chip" style="width:54px"></span><span class="ghost-chip" style="width:40px"></span></div></td><td><div class="comp-desc">${ghostLine("100%")}${ghostLine("62%")}</div></td><td class="comp-status"><span class="ghost-dot"></span></td></tr>`).join("")}</tbody></table>`);
147
-
148
- function faceChip(u, cls) {
149
- const ini = escAttr((u.initials || (u.name || "?").slice(0, 2)).toUpperCase());
150
- const label = escAttr(u.name || "Someone");
151
- return `<span class="${cls}" style="background-color:${u.color || "#4f46e5"}" data-person="${escAttr(u.id)}" title="${label}" aria-label="${label}">${ini}</span>`;
152
- }
153
- function facePile(people, cap = 5) {
154
- if (!people || !people.length) return "";
155
- const shown = people.slice(0, cap), extra = people.length - shown.length;
156
- const chips = shown.map((u) => faceChip(u, "proto-editor opp-face")).join("");
157
- const more = extra > 0 ? `<span class="proto-editor opp-face opp-face--more" title="+${extra} more">+${extra}</span>` : "";
158
- return `<span class="opp-people" role="group" aria-label="Contributors">${chips}${more}</span>`;
159
- }
160
- function currencyLine(key, p, now) {
161
- const cur = STATUS_META[p.status] ? p.status : "ignore";
162
- const word = cur === "ignore" ? "" : `<span class="proto-state ${STATUS_META[cur].cls}">${STATUS_META[cur].label}</span> &middot; `;
163
- const when = p.mtimeMs ? `<span class="proto-when" title="${fmtDate(p.mtimeMs)}">${relTime(p.mtimeMs, now)}</span>` : `<span class="proto-when"></span>`;
164
- return `<div class="proto-date" data-currency="${escAttr(key)}">${word}${when}</div>`;
165
- }
166
- function statusChip(status, key) {
167
- const cur = STATUS_META[status] ? status : "ignore";
168
- const aria = `Status: ${STATUS_META[cur].label}. Click to change.`;
169
- return `<button type="button" class="status-chip ${STATUS_META[cur].cls}" data-status-key="${key}" data-status="${cur}" aria-label="${aria}" title="${aria}">${STATUS_ICONS[cur]}</button>`;
170
- }
171
- const compStatusChip = (name) => {
172
- const aria = "Validation: In progress. Click to mark reviewed.";
173
- return `<button type="button" class="status-chip is-wip" data-comp-status-key="components/${name}" data-status="in-progress" aria-label="${aria}" title="${aria}">${STATUS_ICONS["in-progress"]}</button>`;
174
- };
175
- function metaBadges(meta) {
176
- const layer = meta.layer ? `<span class="cbadge cbadge--layer-${escAttr(meta.layer)}">${escAttr(meta.layer)}</span>` : "";
177
- const surf = meta.surface ? `<span class="cbadge cbadge--surf">${escAttr(meta.surface)}</span>` : "";
178
- const cat = meta.category ? `<span class="cbadge cbadge--cat">${escAttr(meta.category)}</span>` : "";
179
- const stat = meta.status ? `<span class="cbadge cbadge--st-${escAttr(meta.status)}">${escAttr(meta.status)}</span>` : "";
180
- return `<div class="comp-badges">${layer}${surf}${cat}${stat}</div>`;
181
- }
182
-
183
- /** The page skeleton the build used to emit, with the CURRENT chrome. */
184
- export function shell({ title, body, activeTab = "prototypes", wrapClass = "", ctx }) {
185
- const state = { spaces: ctx.spaces || [], activeSpace: ctx.activeSpace || "", opportunities: [], hasPlayground: !!ctx.hasPlayground };
186
- const css = ctx.chrome && ctx.chrome.css ? `/${ctx.chrome.css}` : "";
187
- const js = ctx.chrome && ctx.chrome.js ? `/${ctx.chrome.js}` : "";
188
- return `<!doctype html>
189
- <html lang="en">
190
- <head>
191
- <meta charset="utf-8" />
192
- <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
193
- <meta name="robots" content="noindex, nofollow" />
194
- <title>${escAttr(title)}</title>
195
- <link rel="icon" type="image/png" href="/augur-mark.png?v=${UI_VERSION}" />
196
- <link rel="apple-touch-icon" href="/augur-mark.png?v=${UI_VERSION}" />
197
- <link rel="manifest" href="/manifest.webmanifest" />
198
- <meta name="theme-color" content="#2C2150" />
199
- <link rel="preload" href="/fonts/inter-latin-wght-normal.woff2" as="font" type="font/woff2" crossorigin />
200
- ${js ? ` <link rel="preload" href="${js}" as="script" />\n` : ""}${css ? ` <link rel="stylesheet" href="${css}" />\n` : ""}</head>
201
- <body>
202
- ${CHROME_MARK_START(state.activeSpace, activeTab, state.hasPlayground)}${renderAppChrome(activeTab, state, {})}${CHROME_MARK_END}
203
- <div class="wrap${wrapClass ? " " + wrapClass : ""}">
204
- ${body}
205
- </div>
206
- <script>${renderSpaceContextScript(state)}
207
- </script>
208
- ${js ? ` <script defer src="${js}"></script>\n` : ""} <script type="speculationrules">${JSON.stringify({ prefetch: [{ where: { and: [{ href_matches: "/*" }, { not: { href_matches: "/__*" } }] }, eagerness: "moderate" }] })}</script>
209
- </body>
210
- </html>
211
- `;
212
- }
213
-
214
- const folderbar = (title, count, extra = "") => `<header class="folderbar"><h1 class="folderbar__title">${title}</h1><span class="folderbar__count">${count}</span><span class="folderbar__rule"></span>${extra}</header>`;
215
-
216
- export function renderRootIndex(model, ctx) {
217
- const label = ctx.projectsLabel || "Projects";
218
- if (!model.opportunities.length) {
219
- return shell({ title: "Augur", wrapClass: "wrap--wide", ctx: { ...ctx, hasPlayground: model.hasPlayground },
220
- body: emptyHead(label) + ghostFolderGrid() + emptyState("Ask your agent for a clickable prototype and it shows up here, ready to send.") });
221
- }
222
- const cards = model.opportunities.map((opp) => {
223
- const oppPath = `${enc(opp.name)}/`;
224
- const cover = opp.prototypes[0];
225
- const coverSrc = cover ? cover.href : "";
226
- return `
227
- <div class="card-opp" data-fitem data-fkey="${titleCase(opp.name)}">
228
- <a class="card-cover-link" href="${oppPath}" aria-label="Open ${titleCase(opp.name)}"></a>
229
- ${preview(coverSrc, cover && cover.poster)}
230
- <div class="opp-meta">
231
- <div class="opp-name-row"><div class="proto-name">${titleCase(opp.name)}</div>${facePile(opp.people)}</div>
232
- <div class="proto-date">${plural(opp.prototypes.length, "prototype")} &middot; <span class="proto-when" data-currency-folder="${escAttr(opp.name)}" title="${opp.mtimeMs ? fmtDate(opp.mtimeMs) : ""}">${opp.mtimeMs ? relTime(opp.mtimeMs, ctx.now) : ""}</span></div>
233
- </div>
234
- </div>`;
235
- }).join("");
236
- return shell({ title: "Augur", wrapClass: "wrap--wide", ctx: { ...ctx, hasPlayground: model.hasPlayground },
237
- body: `${folderbar(label, model.opportunities.length)}<div data-fgroup><div class="opp-grid">${cards}</div></div>${filterEmpty()}` });
238
- }
239
-
240
- export function renderOpportunityIndex(model, oppName, ctx) {
241
- const opp = model.opportunities.find((o) => o.name === oppName);
242
- if (!opp) return null;
243
- const label = (ctx.projectsLabel || "Projects").toLowerCase();
244
- const cards = opp.prototypes.map((p) => {
245
- const download = p.file ? `<button type="button" data-dl="${p.file}" data-dlname="${enc(p.name)}.html" aria-label="Download HTML" hidden></button>` : "";
246
- const pinKey = `/${enc(opp.name)}/${enc(p.name)}/`;
247
- const dname = protoName(p.name);
248
- const key = `${opp.name}/${p.name}`;
249
- return `
250
- <div class="card-proto" data-fitem data-fkey="${titleCase(p.name)}" data-rename-key="${key}" data-default-name="${dname}" data-del-space="${escAttr(ctx.activeSpace || "")}" data-del-path="${opp.name}/prototypes/${p.name}">
251
- <div class="preview">
252
- ${media(p.href, p.poster)}
253
- <a class="preview-link" href="${p.href}" aria-label="Open ${titleCase(p.name)}"></a>
254
- <div class="preview-actions">
255
- ${download}
256
- ${pinStar(pinKey, pinKey)}
257
- </div>
258
- ${statusChip(p.status, key)}
259
- </div>
260
- <div class="proto-meta">
261
- <div class="proto-text">
262
- <div class="proto-name">${dname}</div>
263
- ${currencyLine(key, p, ctx.now)}
264
- </div>
265
- ${facePile(p.editors)}
266
- </div>
267
- </div>`;
268
- }).join("");
269
- const up = `<a class="folderbar__up" href="/" aria-label="All ${label}" title="All ${label}"><svg viewBox="0 0 16 16" fill="none" aria-hidden="true"><path d="M10 3L5 8l5 5" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></a>`;
270
- return shell({ title: titleCase(opp.name), activeTab: opp.name, wrapClass: "wrap--wide", ctx: { ...ctx, hasPlayground: model.hasPlayground },
271
- body: `<header class="folderbar">${up}<h1 class="folderbar__title">${titleCase(opp.name)}</h1><span class="folderbar__count">${opp.prototypes.length}</span><span class="folderbar__rule"></span>${newCanvasBtn(`/${opp.name}/`)}</header><div data-fgroup><div class="page-grid is-3up">${cards}</div></div>${filterEmpty()}` });
272
- }
273
-
274
- export function renderPlaygroundIndex(model, ctx) {
275
- const c = { ...ctx, hasPlayground: model.hasPlayground };
276
- if (!model.playground.length) {
277
- return shell({ title: "Playground", activeTab: "playground", wrapClass: "wrap--wide", ctx: c,
278
- body: emptyHead("Playground") + ghostFolderGrid() + emptyState("Ask your agent for something quick that doesn't need a project.") });
279
- }
280
- const cards = model.playground.map((p) => {
281
- const folder = `${enc(p.name)}/`;
282
- const pinKey = `/playground/${enc(p.name)}/`;
283
- const dname = protoName(p.name);
284
- const key = `playground/${p.name}`;
285
- return `
286
- <div class="card-opp" data-fitem data-fkey="${titleCase(p.name)}" data-rename-key="${key}" data-default-name="${dname}" data-del-space="${escAttr(ctx.activeSpace || "")}" data-del-path="playground/${p.name}">
287
- <a class="card-cover-link" href="${folder}" aria-label="Open ${titleCase(p.name)}"></a>
288
- <div class="preview">
289
- ${media(p.href, p.poster)}
290
- ${statusChip(p.status, key)}
291
- </div>
292
- <div class="preview-actions">${pinStar(pinKey, pinKey)}</div>
293
- <div class="proto-meta">
294
- <div class="proto-text">
295
- <div class="proto-name">${dname}</div>
296
- ${currencyLine(key, p, ctx.now)}
297
- </div>
298
- ${facePile(p.editors)}
299
- </div>
300
- </div>`;
301
- }).join("");
302
- return shell({ title: "Playground", activeTab: "playground", wrapClass: "wrap--wide", ctx: c,
303
- body: `${folderbar("Playground", model.playground.length, newCanvasBtn("/playground/"))}<div data-fgroup><div class="opp-grid">${cards}</div></div>${filterEmpty()}` });
304
- }
305
-
306
- const TIER_COPY = Object.freeze({
307
- base: { addHint: 'The atoms every screen here borrows: buttons, inputs, cards, badges, modal, icons. Components and Patterns are built out of these, and all of them wear <a href="/tokens/">Tokens</a>.', empty: "Ask your agent for your buttons, inputs and cards, one page each." },
308
- patterns: { addHint: "Layouts that keep coming back: several Components arranged the way real screens arrange them again and again.", empty: 'Build a few <a href="/pages/">Pages</a> first, then ask your agent to pull out what repeats.' },
309
- pages: { addHint: "", empty: "Ask your agent to build a whole screen out of your design system." },
310
- });
311
-
312
- export function renderTierIndex(model, tier, ctx) {
313
- if (!TIER_COPY[tier]) return null;
314
- const items = model.tiers[tier] || [];
315
- const title = TIER_TITLE[tier];
316
- const c = { ...ctx, hasPlayground: model.hasPlayground };
317
- if (!items.length) {
318
- return shell({ title, activeTab: tier, wrapClass: "wrap--wide", ctx: c, body: emptyHead(title, true) + ghostCardGrid() + emptyState(TIER_COPY[tier].empty) });
319
- }
320
- const card = (p) => `
321
- <div class="card-proto" data-fitem data-fkey="${titleCase(p.name)}" data-rename-key="${tier}/${p.name}" data-default-name="${titleCase(p.name)}">
322
- <div class="preview">
323
- ${media(p.href, p.poster)}
324
- <a class="preview-link" href="${p.href}" aria-label="Open ${titleCase(p.name)}"></a>
325
- </div>
326
- <div class="proto-meta">
327
- <div class="proto-name">${titleCase(p.name)}</div>
328
- </div>
329
- </div>`;
330
- const hint = TIER_COPY[tier].addHint ? `<p class="tier-hint">${TIER_COPY[tier].addHint}</p>` : "";
331
- return shell({ title, activeTab: tier, wrapClass: "wrap--wide", ctx: c,
332
- body: `${folderbar(title, items.length)}${hint}<div class="page-grid">${items.map(card).join("")}</div>${filterEmpty()}` });
333
- }
334
-
335
- /** The catalog a components table reads: registry.json → {slug → {name, classes, desc, meta}}. */
336
- export function catalogFrom(registry) {
337
- const out = {};
338
- for (const it of (registry && registry.items) || []) {
339
- if (!it || !it.name || it.type === "page") continue;
340
- const fams = it.classes || (it.class ? [it.class] : []);
341
- out[it.name] = {
342
- name: it.label || titleCase(it.name),
343
- classes: (it.cssClasses || fams).map((f) => "." + f).join(" / "),
344
- desc: it.description || "",
345
- meta: it.meta || null,
346
- };
347
- }
348
- return out;
349
- }
350
-
351
- export function renderComponentsIndex(model, catalog, ctx) {
352
- const items = model.tiers.components || [];
353
- const c = { ...ctx, hasPlayground: model.hasPlayground };
354
- if (!items.length) {
355
- return shell({ title: "Components", activeTab: "components", wrapClass: "wrap--wide", ctx: c,
356
- body: emptyHead("Components", true) + ghostCompTable() + emptyState("Ask your agent for a component you keep rebuilding, like a search field.") });
357
- }
358
- const rows = items.map((it) => {
359
- const blurb = (catalog && catalog[it.name]) || { name: "", classes: "", desc: "", meta: null };
360
- const dname = blurb.name || titleCase(it.name);
361
- const classes = blurb.classes ? `<code>${escAttr(blurb.classes)}</code>` : "";
362
- const badges = blurb.meta ? metaBadges(blurb.meta) : "";
363
- const tags = blurb.meta && blurb.meta.tags && blurb.meta.tags.length ? `<div class="comp-tags">${blurb.meta.tags.map((t) => `<span>#${escAttr(t)}</span>`).join("")}</div>` : "";
364
- const metaKey = blurb.meta ? [blurb.meta.surface, blurb.meta.category, blurb.meta.status, ...(blurb.meta.tags || [])].filter(Boolean).join(" ") : "";
365
- const fkey = `${dname} ${blurb.classes} ${blurb.desc} ${metaKey}`.replace(/<[^>]+>/g, " ").replace(/"/g, "");
366
- return `
367
- <tr data-fitem data-fkey="${escAttr(fkey)}" data-rename-key="components/${it.name}" data-default-name="${escAttr(dname)}">
368
- <td>
369
- <a class="comp-thumb" href="${it.href}" aria-label="Open ${escAttr(dname)}">
370
- ${media(it.href, it.poster)}
371
- </a>
372
- </td>
373
- <td><div class="comp-name"><span class="proto-name">${escAttr(dname)}</span>${classes}</div>${badges}${tags}</td>
374
- <td><div class="comp-desc" data-desc-key="components/${it.name}#desc">${escAttr(blurb.desc)}</div></td>
375
- <td class="comp-status">${compStatusChip(it.name)}</td>
376
- </tr>`;
377
- }).join("");
378
- return shell({ title: "Components", activeTab: "components", wrapClass: "wrap--wide", ctx: c,
379
- body: `${folderbar("Components", items.length)}<table class="comp-table">
380
- <thead><tr><th>Preview</th><th>Component</th><th>What it is</th><th class="comp-status">Status</th></tr></thead>
381
- <tbody>${rows}</tbody>
382
- </table>${filterEmpty()}` });
383
- }
384
-
385
- /** The rail's finder index — every navigable thing, the shape the chrome bundle reads. */
386
- export function searchIndex(model, ctx) {
387
- const idx = [{ t: ctx.projectsLabel || "Projects", y: "Index", u: "/" }];
388
- if (model.hasPlayground) idx.push({ t: "Playground", y: "Index", u: "/playground/" });
389
- for (const t of ["pages", "components", "base", "patterns"]) if (model.tiers[t].length) idx.push({ t: TIER_TITLE[t], y: "Index", u: `/${t}/` });
390
- const thumb = (p) => (p.poster ? { th: `${p.href}preview.webp` } : {});
391
- for (const opp of model.opportunities) {
392
- const cover = opp.prototypes[0];
393
- idx.push({ t: titleCase(opp.name), y: "Folder", u: `/${enc(opp.name)}/`, ...(cover ? thumb(cover) : {}) });
394
- for (const p of opp.prototypes) idx.push({ t: titleCase(p.name), y: "Prototype", g: titleCase(opp.name), u: p.href, k: `${opp.name}/${p.name}`, ...thumb(p) });
395
- }
396
- for (const p of model.playground) idx.push({ t: titleCase(p.name), y: "Playground", u: p.href, k: `playground/${p.name}`, ...thumb(p) });
397
- for (const p of model.tiers.pages) idx.push({ t: titleCase(p.name), y: "Page", u: p.href, k: `pages/${p.name}`, ...thumb(p) });
398
- for (const p of model.tiers.components) idx.push({ t: titleCase(p.name), y: "Component", u: p.href, k: `components/${p.name}`, ...thumb(p) });
399
- return idx;
400
- }