@augurworks/augur 0.15.2 → 0.15.3

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/INSTALL.md CHANGED
@@ -108,8 +108,9 @@ git clone https://github.com/andratwiro/augur.git augur
108
108
  The engine has no runtime dependencies — `login`, `publish`, `build` and `status` run on
109
109
  a bare clone with plain `node`. `npm install` is only needed for the Playwright-backed
110
110
  screenshot scripts. The commands below call the scripts by path
111
- (`node <parent>/augur/scripts/<name>.mjs`); `npm link` inside `augur/` gives you the
112
- shorter `augur <cmd>` form if you want it.
111
+ (`node <parent>/augur/scripts/<name>.mjs`), which works on a bare clone;
112
+ `npm i -g @augurworks/augur` gives you the shorter `augur <cmd>` form from the
113
+ published package instead.
113
114
 
114
115
  Never nest one instance's parent inside another's — offline mode would serve both
115
116
  instances' spaces at once.
package/README.md CHANGED
@@ -96,12 +96,14 @@ clones, so a slower connection moves the number and nothing else does — the
96
96
  engine itself is about two seconds from cold to serving.
97
97
 
98
98
  ```bash
99
- git clone https://github.com/andratwiro/augur.git
100
99
  git clone https://github.com/andratwiro/augur-space-fulla.git
101
100
  cd augur-space-fulla
102
- node ../augur/scripts/dev.mjs
101
+ npx @augurworks/augur dev
103
102
  ```
104
103
 
104
+ Or, with an engine clone beside the workspace instead of the package:
105
+ `git clone https://github.com/andratwiro/augur.git` and `node ../augur/scripts/dev.mjs`.
106
+
105
107
  There is no install step and no build step in that list, and that is not an
106
108
  omission: **the engine has no runtime dependencies**, so plain `node` is enough.
107
109
  What you get is the full shell — the gallery, the library, review mode, boards —
package/agents/README.md CHANGED
@@ -34,10 +34,10 @@ are `augur open` and `augur land` instead — see [drafts.md](./drafts.md). To g
34
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
- Not on npm yet? The engine clone sits next to every workspace that publishes:
38
- `node <engine>/scripts/cli.mjs connect --origin <origin>`. The instance says all of this
39
- itself at `GET /llms.txt` (and as data at `/.well-known/augur.json`); a signed-out
40
- request for an engine path answers `401` with the same facts.
37
+ The instance says all of this itself at `GET /llms.txt` (and as data at
38
+ `/.well-known/augur.json`); a signed-out request for an engine path answers `401` with
39
+ the same facts. `npm i -g @augurworks/augur` puts `augur` on the PATH for good; an engine
40
+ clone beside the workspace still works too (`node <engine>/scripts/cli.mjs <verb>`).
41
41
 
42
42
  Two things an agent never does: type a password into a terminal (`augur login` exists
43
43
  for CI, and says so), and try the `pass` field from a deploy shell's `identity.json` —
@@ -49,7 +49,6 @@ Read them by trigger, not up front:
49
49
  | When you are… | Read |
50
50
  |---|---|
51
51
  | asking what is current here, or what changed lately | [currency.md](./currency.md) |
52
- | starting work on a prototype somebody else might also be in | [working-marks.md](./working-marks.md) — read it BEFORE the first edit |
53
52
  | changing a prototype on a workspace that serves drafts | [drafts.md](./drafts.md) — open, edit, land |
54
53
  | shipping / going live on a workspace without drafts | [publishing.md](./publishing.md) |
55
54
  | acting on review comments | [review-feedback.md](./review-feedback.md) |
package/agents/drafts.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Drafts — how a prototype is changed on a workspace that serves them
2
2
 
3
3
  **Does this apply here?** Ask the instance: `GET /.well-known/augur.json` carries
4
- `drafts.enabled`. `true` → this document. `false` [publishing.md](./publishing.md), and
5
- `augur ship` as before. `augur ship` on a drafts workspace refuses and points here.
4
+ `drafts.enabled`. `true` → this document; `augur publish` refuses there and points here.
5
+ `false` [publishing.md](./publishing.md), which publishes a whole tree.
6
6
 
7
7
  ## The whole day, in four lines
8
8
 
@@ -16,6 +16,12 @@ 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
+
19
25
  ## What a draft is
20
26
 
21
27
  Your own live copy of one prototype, at its own address: the prototype's URL with `@` and
@@ -25,7 +31,7 @@ you land. Two agents opening the same prototype get two drafts, are both told ab
25
31
  other, and both work — nothing locks, nobody waits.
26
32
 
27
33
  `augur open` prints who else has it open. That is the whole coordination step; there is
28
- nothing to leave and nothing to clear (working marks are for workspaces without drafts).
34
+ nothing to leave and nothing to clear.
29
35
 
30
36
  ## Editing
31
37
 
@@ -74,13 +80,12 @@ you do not want (its saves stay on the instance for a while; nothing else is tou
74
80
  | `stale-draft` | another process saved to this same draft | `augur sync`, then `augur save` |
75
81
  | `would-unpublish` | the draft has no files (the folder is empty) | check the folder; a deletion is its own verb |
76
82
  | `not-a-prototype-folder` / `reserved-folder` | the path is not `<opportunity>/<prototype>` | name the prototype folder |
77
- | `units-not-configured` | this instance does not serve drafts | use `augur ship` — see publishing.md |
83
+ | `units-not-configured` | this instance does not serve drafts | `augur publish` — see publishing.md |
78
84
  | no publish token | this machine is not paired | `augur connect` (never a password) |
79
85
  | unreachable | the instance could not be reached | nothing is lost; the next save carries every change since |
80
86
 
81
87
  ## What you never do
82
88
 
83
- - Ship a whole tree to a drafts workspace. `ship` refuses; `--legacy` exists for one
84
- release and is not for you.
89
+ - Publish a whole tree to a drafts workspace. `publish` refuses there, and `ship` is gone.
85
90
  - Wait for, or refuse over, somebody else's draft. Both work; the second landing syncs.
86
91
  - Hand over a path on disk as "done". Done is the URL `land` printed.
@@ -12,7 +12,7 @@ That `file://`-ability is a property of the *files* — it keeps them portable a
12
12
  buildless, nothing more. It is not a hand-off channel: publish the change and
13
13
  share the live URL (see `agents/publishing.md`). The only acceptable local
14
14
  stand-in, and only when publishing is genuinely impossible, is the real shell
15
- (`node ../augur/scripts/dev.mjs`), said out loud to be exactly that — local
15
+ (`augur dev`), said out loud to be exactly that — local
16
16
  only, not shipped, nobody else can see it.
17
17
 
18
18
  Neither is ever offered as a choice. An agent doesn't ask "should I publish
@@ -1,20 +1,22 @@
1
1
  # Publishing — how work in a workspace goes live
2
2
 
3
- **Where the workspace serves drafts, this is the legacy path** — `augur ship` refuses
4
- there and points at `augur open` / `augur land`; read [drafts.md](./drafts.md). The
3
+ **Where the workspace serves drafts, this document does not apply** — `augur publish`
4
+ refuses there and points at `augur open` / `augur land`; read [drafts.md](./drafts.md). The
5
5
  instance says which it is at `/.well-known/augur.json` (`drafts.enabled`). Everything
6
- below is for a workspace that does not serve drafts yet.
6
+ below is for a self-hosted instance without a unit store, which still publishes a tree.
7
7
 
8
- **One command, every time:**
8
+ **The command, every time your work is ready:**
9
9
 
10
10
  ```
11
- node ../augur/scripts/ship.mjs # commit + publish + push, then print the live URL
12
- node ../augur/scripts/ship.mjs -m "…" # with your own commit message
11
+ augur publish # the working tree goes live; the last line is the URL
12
+ git add -A && git commit -m "…" && git push # then keep the repository true, yourself
13
13
  ```
14
14
 
15
- `augur ship` is the same thing, and only works if someone ran `npm link` in the
16
- engine clone on this machine. The `node …` form always works prefer it in
17
- instructions, or an agent following them hits "command not found" and improvises.
15
+ `augur` is the npm package `@augurworks/augur` (`npm i -g @augurworks/augur`, or
16
+ `npx @augurworks/augur publish` for a one-off). On a machine that only has an engine
17
+ clone beside the workspace, `node ../augur/scripts/publish.mjs` is the same command.
18
+ (`augur ship`, which did the commit and the push for you, is retired: where drafts are
19
+ served there is nothing to commit, and elsewhere the three steps are the two lines above.)
18
20
 
19
21
  Agents: this is the default, and it runs every time you finish a piece of work —
20
22
  not once at the end of a session. Hand the human the URL it prints: the live
@@ -29,7 +31,9 @@ checkout, and telling them to run `git pull` is a support ticket you created.
29
31
  Assume **none of them know what git is** — that is the design target, not a
30
32
  worst case.
31
33
 
32
- So: keep the engine clone current yourself. `publish` fast-forwards it
34
+ So: keep the engine current yourself. A package install updates with
35
+ `npm i -g @augurworks/augur@latest`, and `publish` prints exactly that line when the
36
+ instance speaks a newer protocol than the package. A clone `publish` fast-forwards
33
37
  automatically when the instance speaks a newer publish protocol (clean tree,
34
38
  `--ff-only`, once per run), and re-runs itself — you will see
35
39
  `engine updated <a> → <b>`. When it cannot (uncommitted changes in the engine
@@ -173,10 +177,10 @@ site doesn't show your change, the answer is always the same: it wasn't publishe
173
177
 
174
178
  ## The command
175
179
 
176
- From the workspace repo's root (the engine clone must sit next to it):
180
+ From the workspace repo's root:
177
181
 
178
182
  ```
179
- node ../augur/scripts/publish.mjs # or: augur publish
183
+ augur publish # or, clone-only: node ../augur/scripts/publish.mjs
180
184
  ```
181
185
 
182
186
  - Infers the workspace from the working directory; `--dry-run` and `--engine`
@@ -225,7 +229,11 @@ them.
225
229
  purpose), say so:
226
230
 
227
231
  ```
228
- node ../augur/scripts/ship.mjs --allow-unpublish
232
+ <<<<<<< HEAD
233
+ node ../augur/scripts/publish.mjs --allow-unpublish
234
+ =======
235
+ augur publish --allow-unpublish
236
+ >>>>>>> 01da41b5 (npm: the engine speaks as a package — hooks, self-update and every contract)
229
237
  ```
230
238
 
231
239
  Adding pages is never blocked; only losing them is.
@@ -244,8 +252,7 @@ The first publish on a machine pairs it with your browser, on its own:
244
252
 
245
253
  Open the link in a browser where you are already signed in to the site, type the
246
254
  code, and the publish carries on. No password goes near the terminal, the shell
247
- history, or an agent transcript. `node ../augur/scripts/connect.mjs` (`augur connect`)
248
- runs that step by itself when you want it ahead of time. Agents: relay the link and
255
+ history, or an agent transcript. `augur connect` runs that step by itself when you want it ahead of time. Agents: relay the link and
249
256
  the code to your human; that is the whole hand-off.
250
257
 
251
258
  `augur login` (web email + password) still exists for two cases: CI and scripts,
@@ -299,7 +306,7 @@ responses agree before declaring a chrome bug.
299
306
 
300
307
  ## Local preview (no publish involved)
301
308
 
302
- `node ../augur/scripts/dev.mjs` from the workspace root runs the full site shell
309
+ `augur dev` from the workspace root runs the full site shell
303
310
  locally — login, rail, overlays, canvas, the same experience the live site
304
311
  gives (login `dev@local` / `dev` when no identity file is around). This is the
305
312
  **only** acceptable stand-in when you genuinely cannot publish, and always say
package/build.js CHANGED
@@ -1875,28 +1875,6 @@ const PAGE_CSS = `
1875
1875
  .card-opp.is-stale .preview :is(iframe, .preview-img, .preview-ph) { transition: none; }
1876
1876
  }
1877
1877
 
1878
- /* ---- Working mark ("somebody is in here right now"; MARKS_JS stamps it) ----
1879
- The card-side face of an AGENT protocol: a tool leaves a mark before it starts
1880
- editing so the next tool reads it. This badge is the byproduct, and it is drawn
1881
- to be exactly that — it refuses nothing, covers nothing clickable, and takes
1882
- itself off the card the second the mark's TTL passes.
1883
- Top-RIGHT, because the status glyph owns bottom-left and the two must never
1884
- cover each other on a small card. */
1885
- .mark-badge {
1886
- position: absolute; right: 8px; top: 8px; z-index: 3;
1887
- display: inline-flex; align-items: center; gap: 6px; max-width: calc(100% - 16px);
1888
- padding: 3px 9px 3px 4px; border-radius: 999px;
1889
- background: #fff; box-shadow: 0 2px 8px -1px rgba(16,24,40,0.32);
1890
- font-size: 11.5px; font-weight: 600; letter-spacing: -0.005em; color: #101828;
1891
- pointer-events: none; /* a note is not a control */
1892
- }
1893
- .mark-badge__who {
1894
- flex: none; width: 16px; height: 16px; border-radius: 50%;
1895
- display: inline-grid; place-items: center;
1896
- font-size: 8px; font-weight: 700; letter-spacing: 0; color: #fff;
1897
- background: var(--faint);
1898
- }
1899
- .mark-badge__text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
1900
1878
  /* Draft chips — one per open draft on a prototype card, a count on a folder card.
1901
1879
  Bottom-left so they never fight the mark badge (top-right) or the status chip. */
1902
1880
  .draft-chips {
@@ -3095,80 +3073,6 @@ const FACE_JS = `
3095
3073
  wire();
3096
3074
  })();`;
3097
3075
 
3098
- // Working marks, the card-side face of an AGENT protocol (`F-presence-marks`).
3099
- //
3100
- // A tool leaves a mark on a path before it starts editing so the next tool reads it; this
3101
- // badge is the BYPRODUCT of that, not the point, and it is written to stay one. It reads
3102
- // (`GET /__marks`) and never writes: a person with a tab open is not running a work-start
3103
- // step, and inventing a mark for them would put a claim in the store nobody made.
3104
- //
3105
- // ⚠️ IT REFUSES NOTHING. It adds a note to a card. It does not disable the link, dim the
3106
- // card, gate the menu or set anything another script could branch on to block — a mark
3107
- // that stopped a click would be a lock, which is the one thing this feature is not.
3108
- //
3109
- // EXACT PATHS ONLY, deliberately. The CLI matches a mark against a path by containment,
3110
- // because before you start work a mark on the folder above is the thing worth knowing. A
3111
- // CARD is a published unit, so stamping a folder's mark onto each prototype inside it
3112
- // would put "someone is working on this" on five cards nobody has touched. A folder has
3113
- // its own card on the landing grid and gets the badge there.
3114
- //
3115
- // One fetch per page load, only on pages that have cards, and each badge takes itself off
3116
- // at its own expiry — so a lapsed mark cannot outlive its meaning on an open tab, which
3117
- // is the same guarantee the instance makes to every other reader.
3118
- const MARKS_JS = `
3119
- (function(){
3120
- function norm(p){
3121
- var s = String(p == null ? '' : p).trim();
3122
- if(!s) return '';
3123
- try { s = decodeURIComponent(s); } catch(e){}
3124
- s = s.replace(/^\\.\\//, '').replace(/\\/{2,}/g, '/');
3125
- if(!s || s === '/') return '/';
3126
- return '/' + s.replace(/^\\/+/, '').replace(/\\/+$/, '') + '/';
3127
- }
3128
- function pathOf(card){
3129
- var a = card.querySelector('a.preview-link[href], a[href]');
3130
- if(!a) return '';
3131
- try { return norm(new URL(a.getAttribute('href'), location.href).pathname); } catch(e){ return ''; }
3132
- }
3133
- function badge(card, m){
3134
- if(card.dataset.markDone) return;
3135
- card.dataset.markDone = '1';
3136
- var host = card.querySelector('.preview') || card;
3137
- var b = document.createElement('span');
3138
- b.className = 'mark-badge';
3139
- var who = document.createElement('span');
3140
- who.className = 'mark-badge__who';
3141
- who.textContent = m.initials || '?';
3142
- if(m.color) who.style.background = m.color;
3143
- var t = document.createElement('span');
3144
- t.className = 'mark-badge__text';
3145
- // "Someone" when the id behind the mark answers to nobody on the roster — a token
3146
- // labelled by hand, or a person who has since left. Never a guess at a name.
3147
- t.textContent = (m.by || 'Someone') + ' is working on this';
3148
- b.appendChild(who); b.appendChild(t);
3149
- host.appendChild(b);
3150
- var left = Math.max(0, +m.expiresIn || 0);
3151
- setTimeout(function(){ b.remove(); delete card.dataset.markDone; }, left);
3152
- }
3153
- function wire(){
3154
- var cards = [].slice.call(document.querySelectorAll('.card-proto, .card-opp'));
3155
- if(!cards.length) return;
3156
- fetch('/__marks', { credentials: 'same-origin', headers: { Accept: 'application/json' } })
3157
- .then(function(r){ return r.ok ? r.json() : null; })
3158
- .then(function(d){
3159
- if(!d || !d.marks || !d.marks.length) return;
3160
- var by = {};
3161
- d.marks.forEach(function(m){ if(m && m.path) by[norm(m.path)] = m; });
3162
- cards.forEach(function(card){
3163
- var m = by[pathOf(card)];
3164
- if(m) badge(card, m);
3165
- });
3166
- })
3167
- .catch(function(){ /* a note may never be the reason a gallery looks broken */ });
3168
- }
3169
- window.__gvMarksWire = wire;
3170
- wire();
3171
- })();`;
3172
3076
 
3173
3077
  // Draft chips — the gallery's face of drafts that land (docs/drafts-that-land.md §5).
3174
3078
  //
@@ -6178,7 +6082,7 @@ const CHROME_CSS_BODY = `${FONT_CSS}${PAGE_CSS}${NAV_CSS}${TABBAR_CSS}`;
6178
6082
  const CHROME_JS_BODY = [
6179
6083
  CAROUSEL_JS, chromeScript(), STATUS_JS, CURRENCY_JS, COMP_STATUS_JS, CARD_MENU_JS,
6180
6084
  PINS_JS, PROFILE_JS, SETTINGS_JS, NEWCANVAS_JS, SPACE_JS, WORKSPACES_JS, TABBAR_JS(),
6181
- RESEARCH_JS, FACE_JS, MARKS_JS, DRAFTS_JS,
6085
+ RESEARCH_JS, FACE_JS, DRAFTS_JS,
6182
6086
  ].join("\n;\n") + "\n;\n" +
6183
6087
  // Register the service worker (P0), shipped inside the already-cached bundle so
6184
6088
  // it costs pages zero inline bytes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@augurworks/augur",
3
- "version": "0.15.2",
3
+ "version": "0.15.3",
4
4
  "files": [
5
5
  "scripts/",
6
6
  "build.js",
package/scripts/cli.mjs CHANGED
@@ -4,13 +4,11 @@
4
4
  // augur dev full local shell in the current space folder (or workspace root)
5
5
  // augur build compose dist/ once
6
6
  // augur deploy build + direct-upload the whole site (see deploy.mjs)
7
- // augur ship commit + publish + push — the default way a change goes out
8
7
  // augur publish publish only, without committing or pushing (see publish.mjs)
9
8
  // augur fork copy a published artifact to a new URL, moving no bytes (see fork.mjs)
10
9
  // augur status what is live vs what your clones hold (see status.mjs)
11
10
  // augur refine render every component, photograph it, measure it against the
12
11
  // original, and report a pass-rate nobody can assert (see refine.mjs)
13
- // augur mark say what you are about to work on; read what everyone else is
14
12
  // augur open open one prototype into a folder of its own, live at once
15
13
  // augur save push every changed file in this draft folder
16
14
  // augur land replace the prototype's main with this draft
@@ -40,13 +38,11 @@ const map = {
40
38
  dev: "dev.mjs",
41
39
  offline: "offline.mjs",
42
40
  deploy: "deploy.mjs",
43
- ship: "ship.mjs",
44
41
  publish: "publish.mjs",
45
42
  // The one publishing verb that needs no tree: two paths and a token (see fork.mjs).
46
43
  fork: "fork.mjs",
47
44
  status: "status.mjs",
48
45
  refine: "refine.mjs",
49
- mark: "mark.mjs",
50
46
  // Drafts that land (docs/drafts-that-land.md): one prototype, one folder, live at once.
51
47
  open: "open.mjs",
52
48
  save: "save.mjs",
@@ -83,8 +79,18 @@ const map = {
83
79
  canon: "canon.mjs",
84
80
  build: path.join("..", "build.js"),
85
81
  };
82
+ // Two verbs that are gone, answered rather than dropped: a person or an agent following an
83
+ // older note lands on one sentence saying where the work went, not on "unknown command".
84
+ if (sub === "ship") {
85
+ console.error("augur ship is retired. A prototype is changed by opening it: `augur open <opportunity>/<prototype>`, edit, `augur land` (engine agents/drafts.md). A workspace without a unit store still publishes a tree with `augur publish`.");
86
+ process.exit(1);
87
+ }
88
+ if (sub === "mark") {
89
+ console.error("augur mark is retired: a draft IS the mark. `augur open <prototype>` tells you who else has it open, and `augur status` lists what is open on this machine.");
90
+ process.exit(1);
91
+ }
86
92
  if (!map[sub]) {
87
- console.error("usage: augur <init|ship|dev|offline|build|deploy|publish|fork|status|canon|refine|mark|open|save|land|sync|close|read|watch|hook|clone|pull|export|restore|migrate|bundle-rekey|identity-rekey|adopt|freeze|thaw|connect|login> [options]");
93
+ console.error("usage: augur <init|dev|offline|build|deploy|publish|fork|status|canon|refine|open|save|land|sync|close|read|watch|hook|clone|pull|export|restore|migrate|bundle-rekey|identity-rekey|adopt|freeze|thaw|connect|login> [options]");
88
94
  process.exit(sub ? 1 : 0);
89
95
  }
90
96
  const child = spawn(process.execPath, [path.join(SCRIPTS, map[sub]), ...rest], {
package/scripts/clone.mjs CHANGED
@@ -68,7 +68,6 @@ import {
68
68
  resolveUnit, unitFilesFromManifest, skillDirsReferenced, skillFilesFromManifest,
69
69
  rerootHtml, residualFindings, isText, isSkillInternal,
70
70
  } from "./lib/graduate.mjs";
71
- import { fetchMarks, marksOverlap, markLine } from "./lib/marks.mjs";
72
71
 
73
72
  const C = { dim: "\x1b[2m", warn: "\x1b[33m", bad: "\x1b[31m", ok: "\x1b[32m", off: "\x1b[0m" };
74
73
  const log = (m) => console.log(`\x1b[35m[clone]\x1b[0m ${m}`);
@@ -332,25 +331,6 @@ async function main() {
332
331
  console.log(` ${C.warn}conflict${C.off} ${c.path} ${C.dim}${c.why}${C.off}`);
333
332
  }
334
333
 
335
- // ── WHO IS ALREADY IN HERE (`F-presence-marks`) ───────────────────────────
336
- //
337
- // A pull is a work-start: files are about to be written into a tree somebody is about to
338
- // edit. So the marks on the paths ABOUT TO BE TOUCHED are printed before the first byte
339
- // lands, which is the only moment the information can still change what happens next.
340
- //
341
- // ⚠️ IT NEVER REFUSES AND NEVER CHANGES THE EXIT CODE. A mark is a note, not a lock, and
342
- // a pull that stopped for one would be a lock with extra steps. Read on a URL rather than
343
- // a source path: a mark names the published unit, which is what both sides can agree on.
344
- const touching = [...plan.write, ...plan.conflict];
345
- if (touching.length) {
346
- const marks = (await fetchMarks(req))
347
- .filter((m) => touching.some((f) => marksOverlap(f.url, m.path)));
348
- if (marks.length) {
349
- console.log(`\n ${C.warn}somebody is working on paths this pull touches${C.off}`);
350
- for (const m of marks) console.log(` ${markLine(m)}`);
351
- console.log(` ${C.dim}Nothing here is blocked. It is worth knowing before you edit the same folder.${C.off}\n`);
352
- }
353
- }
354
334
  if (DRY) {
355
335
  log(`${C.dim}dry run — would write ${plan.write.length}, leave ${plan.skip.length} identical, ${plan.conflict.length} conflict(s)${C.off}`);
356
336
  process.exit(plan.conflict.length ? 2 : 0);
package/scripts/init.mjs CHANGED
@@ -96,7 +96,7 @@ const html = `<!doctype html>
96
96
  and ships exactly as written.</p>
97
97
  <p>When it works, ship it: on a workspace that serves drafts,
98
98
  <code>augur open ${project}/${proto}</code> then <code>augur land</code>; otherwise
99
- <code>augur ship</code>. Either way you get the live URL.</p>
99
+ <code>augur publish</code>. Either way you get the live URL.</p>
100
100
  </main>
101
101
  </body>
102
102
  </html>
@@ -119,4 +119,4 @@ log(` space.json`);
119
119
  log(` ${rel}`);
120
120
  if (wroteCanon) log(` CANON.md`);
121
121
  if (!origin) log(`no siteOrigin set — add one to space.json (or pass --origin) so connect/login/publish work from this clone.`);
122
- log(`next: edit the prototype, then \`augur land\` (a workspace that serves drafts) or \`augur ship\`.`);
122
+ log(`next: edit the prototype, then \`augur land\` (a workspace that serves drafts) or \`augur publish\`.`);
@@ -16,12 +16,16 @@ import fs from "node:fs";
16
16
  import os from "node:os";
17
17
  import path from "node:path";
18
18
  import { fileURLToPath } from "node:url";
19
+ import { execFileSync } from "node:child_process";
19
20
 
20
21
  // What identifies OUR entries in a settings file: the quoted script name and the event,
21
22
  // exactly as `hookCommand` spells them — so an entry survives the engine moving on disk
22
23
  // (the path changes, the tail does not) and nobody else's hook is ever mistaken for ours.
23
24
  export const HOOK_TAG = 'hook.mjs"';
24
- const OURS_RE = /hook\.mjs" (?:pre|post)$/;
25
+ // Two spellings are ours: the clone-era `node "<path>/hook.mjs" pre` and the package-era
26
+ // `augur hook pre`. Both are matched so an entry written by an older engine is replaced
27
+ // on the next `augur open`, never duplicated.
28
+ const OURS_RE = /(?:hook\.mjs" |(?:^|\s)augur hook )(?:pre|post)$/;
25
29
  export const HOOK_SCRIPT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "hook.mjs");
26
30
  export const UNIT_HOME_RE = /^(?:([^/]+)\/prototypes\/([^/]+)|(playground)\/([^/]+))(?:\/|$)/;
27
31
 
@@ -90,7 +94,22 @@ export const ADAPTERS = Object.freeze([
90
94
  }),
91
95
  ]);
92
96
 
93
- export const hookCommand = (event) => `node "${HOOK_SCRIPT}" ${event}`;
97
+ /**
98
+ * The command a tool runs for one of our hooks. With the package installed globally
99
+ * (`npm i -g @augurworks/augur`) `augur` resolves on PATH and the hook says so, because
100
+ * the absolute path of a global install moves on every upgrade. Otherwise — a clone, or
101
+ * a one-off `npx` run — the absolute path to this engine's hook.mjs is the one thing
102
+ * guaranteed to exist.
103
+ */
104
+ export const hookCommand = (event, { onPath = false } = {}) =>
105
+ onPath ? `augur hook ${event}` : `node "${HOOK_SCRIPT}" ${event}`;
106
+ /** Whether `augur` resolves on this machine's PATH. Asked once, at install time. */
107
+ export function augurOnPath() {
108
+ try {
109
+ const out = execFileSync(process.platform === "win32" ? "where" : "which", ["augur"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
110
+ return out.trim().length > 0;
111
+ } catch (e) { return false; }
112
+ }
94
113
  const ours = (h) => !!(h && Array.isArray(h.hooks) && h.hooks.some((x) => x && typeof x.command === "string" && OURS_RE.test(x.command.trim())));
95
114
 
96
115
  export function mergeHooks(settings, matcher, commands) {
@@ -146,7 +165,8 @@ function applyToAdapters({ home = os.homedir(), dryRun = false } = {}, decide) {
146
165
  /** Install the two hooks for every tool present on this machine. Idempotent; keeps every other hook. */
147
166
  export function installAdapters(opts = {}) {
148
167
  return applyToAdapters(opts, (a, cur) => {
149
- const r = mergeHooks(cur, a.matcher, { pre: hookCommand("pre"), post: hookCommand("post") });
168
+ const onPath = typeof opts.onPath === "boolean" ? opts.onPath : augurOnPath();
169
+ const r = mergeHooks(cur, a.matcher, { pre: hookCommand("pre", { onPath }), post: hookCommand("post", { onPath }) });
150
170
  const had = !!(cur && cur.hooks && Object.values(cur.hooks).some((l) => Array.isArray(l) && l.some(ours)));
151
171
  return { ...r, did: had ? "updated" : "installed" };
152
172
  });
@@ -28,6 +28,18 @@ const MIME = {
28
28
  export const mimeOf = (name) => MIME[path.extname(name).slice(1).toLowerCase()] || "application/octet-stream";
29
29
  export const hashBytes = (buf) => createHash("sha256").update(buf).digest("hex");
30
30
  export const relOf = (unit, urlPath) => urlPath.slice(unit.length);
31
+ /**
32
+ * A REPO folder or a URL, as the unit path it publishes to: `<project>/prototypes/<name>`
33
+ * is the nesting a space clone keeps, served at `/<project>/<name>/`. An agent has just been
34
+ * looking at the folder, so it is the spelling it will type.
35
+ */
36
+ export function unitPathFor(input) {
37
+ const s = String(input == null ? "" : input).trim().slice(0, 300).replace(/\/prototypes\//g, "/");
38
+ if (!s) return "";
39
+ const t = s.replace(/^\.\//, "").replace(/\/{2,}/g, "/");
40
+ if (!t || t === "/") return "/";
41
+ return `/${t.replace(/^\/+/, "").replace(/\/+$/, "")}/`;
42
+ }
31
43
  export const urlOf = (unit, rel) => unit + rel;
32
44
 
33
45
  export function scanFolder(dir) {
@@ -150,11 +162,19 @@ async function materialise(client, unit, table, dir) {
150
162
  }
151
163
  }
152
164
 
153
- async function doOpenImpl({ client, unit, dir, origin, space, session, now }) {
165
+ async function doOpenImpl({ client, unit, dir, origin, space, session, now, isNew = false }) {
154
166
  if (fs.existsSync(dir) && fs.readdirSync(dir).length) return { ok: false, error: "folder-not-empty", dir };
155
167
  const createdFolder = !fs.existsSync(dir);
156
168
  const o = await client.open({ unit });
157
169
  if (o.status) return { ok: false, ...o };
170
+ // A unit with no files is one that does not exist yet. Creating one is a decision the
171
+ // caller states with `isNew`; without it, a typo would quietly open an empty draft on a
172
+ // prototype that was never there. Either mismatch hands the draft straight back.
173
+ const exists = Object.keys(o.table || {}).length > 0;
174
+ if (exists !== !isNew) {
175
+ try { await client.discard({ unit, draftId: o.draftId }); } catch (e) { /* best-effort */ }
176
+ return { ok: false, error: exists ? "unit-exists" : "unknown-unit", unit };
177
+ }
158
178
  // From here the server-side draft exists, so any failure below must both undo what we
159
179
  // wrote to disk and tell the server to drop the orphan — otherwise a retry finds a
160
180
  // half-materialised folder (`folder-not-empty`) and the draft it opened is never freed.
@@ -177,7 +197,7 @@ async function doOpenImpl({ client, unit, dir, origin, space, session, now }) {
177
197
  throw err;
178
198
  }
179
199
  const others = (o.presence || []).filter((d) => d.id !== o.draftId);
180
- return { ok: true, draftId: o.draftId, address: `${origin}${o.address}`, files: Object.keys(o.table).length, others };
200
+ return { ok: true, draftId: o.draftId, address: `${origin}${o.address}`, files: Object.keys(o.table).length, others, isNew: !exists };
181
201
  }
182
202
 
183
203
  async function doSaveImpl({ client, dir, baseRevision, baseTable }) {
@@ -224,6 +224,7 @@ 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
227
228
  "ROSTER_OVERLAY",
228
229
  "FREEZE_STATE",
229
230
  "SUSPENSION_STATE",
@@ -281,6 +282,21 @@ const ALLOWLIST = {
281
282
  },
282
283
  },
283
284
 
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
+
284
300
  "src/bundle-keys.mjs": {
285
301
  frozen: [
286
302
  "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> [--dir <folder>] [--session <label>]
2
+ // augur open <prototype> [--new] [--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
@@ -8,8 +8,7 @@
8
8
  import fs from "node:fs";
9
9
  import path from "node:path";
10
10
  import { target, buildStamp } from "./lib/store.mjs";
11
- import { markPathFor } from "./lib/marks.mjs";
12
- import { unitClient, doOpen } from "./lib/draft.mjs";
11
+ import { unitClient, doOpen, unitPathFor } from "./lib/draft.mjs";
13
12
  import { installAdapters } from "./lib/adapters.mjs";
14
13
  import { normUnit } from "../src/unit-core.mjs";
15
14
 
@@ -21,7 +20,7 @@ const positional = argv.filter((a, i) => !a.startsWith("--") && !(i > 0 && argv[
21
20
 
22
21
  const raw = positional[0];
23
22
  if (!raw) die("name a prototype: `augur open <opportunity>/<prototype>` (a folder path works too).");
24
- const unit = normUnit(markPathFor(raw) || raw);
23
+ const unit = normUnit(unitPathFor(raw) || raw);
25
24
  if (!unit) die(`"${raw}" is not a prototype path.`);
26
25
  let origin, token;
27
26
  try { ({ origin, token } = target({ needToken: true })); } catch (e) { die(e.message); }
@@ -35,15 +34,18 @@ const session = process.env.AUGUR_SESSION || opt("--session") || `session-${proc
35
34
  const dir = path.resolve(opt("--dir") || unit.split("/").filter(Boolean).pop());
36
35
 
37
36
  const client = unitClient({ origin, token, space, session });
38
- const r = await doOpen({ client, unit, dir, origin, space, session, now: new Date().toISOString() });
37
+ const isNew = argv.includes("--new");
38
+ const r = await doOpen({ client, unit, dir, origin, space, session, now: new Date().toISOString(), isNew });
39
39
  if (!r.ok) {
40
40
  if (r.error === "folder-not-empty") die(`${r.dir} is not empty — pick another folder with --dir.`);
41
+ if (r.error === "unknown-unit") die(`${unit} does not exist here. To create it: \`augur open --new ${unit.replace(/^\/|\/$/g, "")}\`.`);
42
+ if (r.error === "unit-exists") die(`${unit} exists already — open it without --new.`);
41
43
  if (r.error === "units-not-configured") die("this instance does not serve drafts yet (no unit store bound).");
42
44
  if (r.error === "bad-unit" && r.reason === "reserved-folder") die(`${unit} sits under a folder the engine reserves — a prototype lives at <opportunity>/<prototype>.`);
43
45
  if (r.error === "bad-unit" && r.reason === "not-a-prototype-folder") die(`${unit} is not a prototype folder — name one as <opportunity>/<prototype>.`);
44
46
  die(`could not open: ${r.error || r.status}${r.reason ? ` (${r.reason})` : ""}`);
45
47
  }
46
- log(`draft ${r.draftId} on ${unit} — ${r.files} file(s) in ${dir}`);
48
+ 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}`);
47
49
  // The agent tool's hooks, installed for this machine the first time a draft is opened
48
50
  // here (idempotent; `AUGUR_NO_ADAPTERS=1` skips it — the suite and CI set it).
49
51
  if (!process.env.AUGUR_NO_ADAPTERS) {