@augurworks/augur 0.15.2 → 0.15.4

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
@@ -25,7 +25,9 @@ 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
28
- enter `<code>`* — and waits. The owner opens the link in a browser they are already
28
+ enter `<code>`* — and waits. If your person reads you through messages rather than a
29
+ terminal, run it with `--no-wait`: it prints the line and exits, and running it again
30
+ after they approve collects the token for that same code. The owner opens the link in a browser they are already
29
31
  signed in to and types the code; the token lands in `~/.config/augur/tokens.json`, and
30
32
  `augur ship` / `augur publish` use it from then on. `publish` runs the pairing itself when
31
33
  it finds no token, so inside a workspace tree nothing has to be done first. On a workspace
@@ -34,10 +36,10 @@ are `augur open` and `augur land` instead — see [drafts.md](./drafts.md). To g
34
36
  tree from a hostname, pair first, then `npx @augurworks/augur clone --space <id>` — it reads the
35
37
  origin from the pairing.
36
38
 
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.
39
+ The instance says all of this itself at `GET /llms.txt` (and as data at
40
+ `/.well-known/augur.json`); a signed-out request for an engine path answers `401` with
41
+ the same facts. `npm i -g @augurworks/augur` puts `augur` on the PATH for good; an engine
42
+ clone beside the workspace still works too (`node <engine>/scripts/cli.mjs <verb>`).
41
43
 
42
44
  Two things an agent never does: type a password into a terminal (`augur login` exists
43
45
  for CI, and says so), and try the `pass` field from a deploy shell's `identity.json` —
@@ -49,7 +51,6 @@ Read them by trigger, not up front:
49
51
  | When you are… | Read |
50
52
  |---|---|
51
53
  | 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
54
  | changing a prototype on a workspace that serves drafts | [drafts.md](./drafts.md) — open, edit, land |
54
55
  | shipping / going live on a workspace without drafts | [publishing.md](./publishing.md) |
55
56
  | 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
 
@@ -72,15 +78,17 @@ you do not want (its saves stay on the instance for a while; nothing else is tou
72
78
  |---|---|---|
73
79
  | `main-moved` / "sync first" | somebody landed since you opened | `augur sync`, then `augur land` |
74
80
  | `stale-draft` | another process saved to this same draft | `augur sync`, then `augur save` |
81
+ | `draft-closed` | somebody landed or discarded this draft from the site (it says who and when) | your edits are still in the folder; `augur open` the prototype again and copy them in |
82
+ | `manifest-contended` | many landings hit the workspace in the same second; `land` already tried again | `augur land` once more |
83
+ | `forbidden` with "run `augur connect` again" | this machine's token was revoked (a role change or a removal) or belongs to another workspace | `augur connect` |
75
84
  | `would-unpublish` | the draft has no files (the folder is empty) | check the folder; a deletion is its own verb |
76
85
  | `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 |
86
+ | `units-not-configured` | this instance does not serve drafts | `augur publish` — see publishing.md |
78
87
  | no publish token | this machine is not paired | `augur connect` (never a password) |
79
88
  | unreachable | the instance could not be reached | nothing is lost; the next save carries every change since |
80
89
 
81
90
  ## What you never do
82
91
 
83
- - Ship a whole tree to a drafts workspace. `ship` refuses; `--legacy` exists for one
84
- release and is not for you.
92
+ - Publish a whole tree to a drafts workspace. `publish` refuses there, and `ship` is gone.
85
93
  - Wait for, or refuse over, somebody else's draft. Both work; the second landing syncs.
86
94
  - 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,7 @@ them.
225
229
  purpose), say so:
226
230
 
227
231
  ```
228
- node ../augur/scripts/ship.mjs --allow-unpublish
232
+ augur publish --allow-unpublish
229
233
  ```
230
234
 
231
235
  Adding pages is never blocked; only losing them is.
@@ -244,8 +248,7 @@ The first publish on a machine pairs it with your browser, on its own:
244
248
 
245
249
  Open the link in a browser where you are already signed in to the site, type the
246
250
  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
251
+ 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
252
  the code to your human; that is the whole hand-off.
250
253
 
251
254
  `augur login` (web email + password) still exists for two cases: CI and scripts,
@@ -299,7 +302,7 @@ responses agree before declaring a chrome bug.
299
302
 
300
303
  ## Local preview (no publish involved)
301
304
 
302
- `node ../augur/scripts/dev.mjs` from the workspace root runs the full site shell
305
+ `augur dev` from the workspace root runs the full site shell
303
306
  locally — login, rail, overlays, canvas, the same experience the live site
304
307
  gives (login `dev@local` / `dev` when no identity file is around). This is the
305
308
  **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.4",
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);
@@ -30,6 +30,28 @@ const ORIGIN = (opt("--origin") || process.env.AUGUR_ORIGIN || resolveOrigin() |
30
30
  if (!ORIGIN) die("no origin — pass --origin https://your.site, or set AUGUR_ORIGIN.");
31
31
 
32
32
  const POLL_MS = 2000;
33
+ // `--no-wait`: print the line to relay and exit at once. For an agent that talks to its
34
+ // person through messages rather than a terminal — a chat, a ticket, a print-mode run —
35
+ // waiting here is a dead end: the person cannot see the code until the command ends, and
36
+ // the command does not end until the person acts. The pairing is kept on this machine, so
37
+ // `augur connect` run again after the approval collects the token, minting no new code.
38
+ const NO_WAIT = argv.includes("--no-wait");
39
+ const PENDING_FILE = path.join(os.homedir(), ".config", "augur", "pairing.json");
40
+ const host = new URL(ORIGIN).host;
41
+ function readPending() {
42
+ try {
43
+ const all = JSON.parse(readFileSync(PENDING_FILE, "utf8"));
44
+ const p = all[host];
45
+ return p && p.code && p.deviceSecret && Date.parse(p.expiresAt) > Date.now() ? p : null;
46
+ } catch (e) { return null; }
47
+ }
48
+ function writePending(p) {
49
+ mkdirSync(path.dirname(PENDING_FILE), { recursive: true });
50
+ let all = {};
51
+ try { all = JSON.parse(readFileSync(PENDING_FILE, "utf8")); } catch (e) {}
52
+ if (p) all[host] = p; else delete all[host];
53
+ writeFileSync(PENDING_FILE, JSON.stringify(all, null, 2), { mode: 0o600 });
54
+ }
33
55
 
34
56
  async function post(pathPart, body) {
35
57
  const r = await fetch(`${ORIGIN}/__publish/_pair/${pathPart}`, {
@@ -42,7 +64,22 @@ async function post(pathPart, body) {
42
64
  return { status: r.status, json };
43
65
  }
44
66
 
45
- const start = await post("start");
67
+ // A pairing this machine already started and nobody has collected: ask once whether it
68
+ // was approved meanwhile, and if not, keep waiting on THAT code rather than minting a
69
+ // second one for the same person to type.
70
+ const pending = readPending();
71
+ if (pending) {
72
+ const res = await post("claim", { code: pending.code, deviceSecret: pending.deviceSecret });
73
+ if (res.status === 200 && res.json && res.json.token) {
74
+ writePending(null);
75
+ finish(res.json);
76
+ } else if (res.status === 202) {
77
+ log(`the pairing started earlier is still waiting to be approved.`);
78
+ } else {
79
+ writePending(null);
80
+ }
81
+ }
82
+ const start = readPending() ? { status: 200, json: { ...readPending(), expiresInMs: Date.parse(readPending().expiresAt) - Date.now() } } : await post("start");
46
83
  if (start.status === 429) die("too many attempts from here. Wait a few minutes.");
47
84
  if (start.status !== 200 || !start.json || !start.json.code) {
48
85
  // The routes answer as though they are not there when the instance has not opted in,
@@ -53,8 +90,9 @@ if (start.status !== 200 || !start.json || !start.json.code) {
53
90
  }
54
91
 
55
92
  const { code, deviceSecret, approveUrl, expiresInMs } = start.json;
56
- const mins = Math.round((expiresInMs || 300000) / 60000);
93
+ const mins = Math.max(1, Math.round((expiresInMs || 300000) / 60000));
57
94
  const pretty = code.length === 8 ? `${code.slice(0, 4)}-${code.slice(4)}` : code;
95
+ if (!readPending()) writePending({ code, deviceSecret, approveUrl, startedAt: new Date().toISOString(), expiresAt: new Date(Date.now() + (expiresInMs || 300000)).toISOString() });
58
96
 
59
97
  // Written to be RELAYED, not read: the usual runner of this command is an agent, which
60
98
  // pastes the first line to a person. So the first line is the whole instruction, and it
@@ -68,6 +106,15 @@ console.log(` The code is good for ${mins} minutes and only for this terminal.
68
106
  console.log("");
69
107
  console.log(` ${C.warn}If you did not just run this command, do not approve it.${C.off}`);
70
108
  console.log("");
109
+ if (NO_WAIT) {
110
+ console.log(` ${C.dim}Not waiting. Once they have approved, run \`augur connect\` again here: it collects the`);
111
+ console.log(` token for this same code and mints no new one.${C.off}`);
112
+ process.exit(0);
113
+ }
114
+ console.log(` ${C.dim}You can stop waiting (Ctrl-C) and run \`augur connect\` again after they approve —`);
115
+ console.log(` it collects the token for this same code. Talking to your person through messages`);
116
+ console.log(` rather than a terminal? Use \`augur connect --no-wait\`.${C.off}`);
117
+ console.log("");
71
118
 
72
119
  const deadline = Date.now() + (expiresInMs || 300000);
73
120
  let saved = null;
@@ -76,9 +123,13 @@ while (Date.now() < deadline) {
76
123
  const res = await post("claim", { code, deviceSecret });
77
124
  if (res.status === 200 && res.json && res.json.token) { saved = res.json; break; }
78
125
  if (res.status === 202) continue; // still waiting for somebody to approve
79
- if (res.status === 404) die("this pairing is no longer valid. Run `augur connect` again.");
126
+ if (res.status === 404) { writePending(null); die("this pairing is no longer valid. Run `augur connect` again."); }
80
127
  }
81
- if (!saved) die(`nobody approved it within ${mins} minutes. Run \`augur connect\` again.`);
128
+ if (!saved) { writePending(null); die(`nobody approved it within ${mins} minutes. Run \`augur connect\` again.`); }
129
+ writePending(null);
130
+ finish(saved);
131
+
132
+ function finish(saved) {
82
133
 
83
134
  const dir = path.join(os.homedir(), ".config", "augur");
84
135
  mkdirSync(dir, { recursive: true });
@@ -100,4 +151,6 @@ if (saved.expiresAt) {
100
151
  console.log(`${C.dim}It expires in ${days} days (${saved.expiresAt.slice(0, 10)}). Run \`augur connect\` again then.${C.off}`);
101
152
  } else {
102
153
  console.log(`${C.dim}It expires on its own. Run \`augur connect\` again when it does.${C.off}`);
154
+ }
155
+ process.exit(0);
103
156
  }
package/scripts/hook.mjs CHANGED
@@ -63,5 +63,6 @@ const client = unitClient({ origin, token, space: st.space, session: st.session
63
63
  const r = await doSave({ client, dir: target.dir });
64
64
  if (r.ok) process.exit(0);
65
65
  if (r.error === "stale-draft" || r.error === "stale-draft-revision") refuse(`draft ${st.draftId} not saved: it moved under you (another process saved to it) — run augur sync, then augur save.`);
66
+ if (r.error === "draft-closed") refuse(`draft ${st.draftId} not saved: it was ${r.landed ? `landed by ${r.name || r.by || "someone"}${r.session ? ` (${r.session})` : ""} at ${r.at}` : "discarded"} — this folder is no longer a draft. Your edits are still here; run augur open on the prototype again and copy them in.`);
66
67
  if (r.error === "network") refuse(`draft ${st.draftId} not saved: ${origin} is unreachable (${r.message}). Nothing is lost — the next save carries every change since.`);
67
- refuse(`draft ${st.draftId} not saved: ${r.error || r.status}${r.reason ? ` (${r.reason})` : ""}. Fix it and run augur save.`);
68
+ refuse(`draft ${st.draftId} not saved: ${r.error || r.status}${r.reason ? ` (${r.reason})` : ""}${r.message ? ` — ${r.message}` : ""}. Fix it and run augur save.`);
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\`.`);
package/scripts/land.mjs CHANGED
@@ -26,7 +26,10 @@ if (!r.ok) {
26
26
  die("run `augur sync` to fold those in, check the draft address, then `augur land` again.");
27
27
  }
28
28
  if (r.error === "landing-in-progress") die("somebody is landing this prototype right now — try again in a few seconds.");
29
- die(`land refused: ${r.error || r.status}`);
29
+ if (r.error === "manifest-contended") die("the workspace was busy landing other prototypes — nothing changed; run augur land again.");
30
+ if (r.error === "draft-closed") die(`this draft was ${r.landed ? `landed by ${r.name || r.by || "someone"}${r.session ? ` (${r.session})` : ""} at ${r.at}` : "discarded"} — the folder is no longer a draft. Your edits are still here; run augur open on the prototype again and copy them in.`);
31
+ if (r.error === "network") die(`could not reach the instance (${r.message}). Nothing is lost — run augur land again.`);
32
+ die(`land refused: ${r.error || r.status}${r.message ? ` — ${r.message}` : ""}`);
30
33
  }
31
34
  // The bytes are live; when `recorded` is false only the history entry is missing. Said out
32
35
  // loud because the next call adopts that landing as the instance's own, and nobody would
@@ -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
  });