@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 +3 -2
- package/README.md +4 -2
- package/agents/README.md +7 -6
- package/agents/drafts.md +14 -6
- package/agents/prototype-contract.md +1 -1
- package/agents/publishing.md +19 -16
- package/build.js +1 -97
- package/package.json +1 -1
- package/scripts/cli.mjs +11 -5
- package/scripts/clone.mjs +0 -20
- package/scripts/connect.mjs +57 -4
- package/scripts/hook.mjs +2 -1
- package/scripts/init.mjs +2 -2
- package/scripts/land.mjs +4 -1
- package/scripts/lib/adapters.mjs +23 -3
- package/scripts/lib/draft.mjs +76 -8
- package/scripts/no-tenant-globals.mjs +16 -0
- package/scripts/open.mjs +10 -7
- package/scripts/publish.mjs +19 -0
- package/scripts/read.mjs +13 -6
- package/scripts/save.mjs +3 -1
- package/scripts/status.mjs +5 -23
- package/scripts/sync.mjs +3 -1
- package/src/_worker.js +161 -314
- package/src/galleries.mjs +400 -0
- package/src/state-inventory.mjs +0 -4
- package/src/unit-object.mjs +22 -4
- package/agents/working-marks.md +0 -86
- package/scripts/lib/marks.mjs +0 -107
- package/scripts/mark.mjs +0 -112
- package/scripts/ship.mjs +0 -460
package/scripts/lib/draft.mjs
CHANGED
|
@@ -11,6 +11,8 @@ import { createHash } from "node:crypto";
|
|
|
11
11
|
import { merge3 } from "./merge3.mjs";
|
|
12
12
|
|
|
13
13
|
export const STATE_FILE = ".augur/draft.json";
|
|
14
|
+
/** How many times `land` tries again when the space's manifest is contended. */
|
|
15
|
+
export const LAND_RETRIES = 4;
|
|
14
16
|
export const THEIRS_DIR = ".augur/theirs";
|
|
15
17
|
// The machine-wide registry of open draft folders. `AUGUR_DRAFTS_REGISTRY` exists for the
|
|
16
18
|
// test suite, which must never write into the developer's own home folder.
|
|
@@ -28,6 +30,18 @@ const MIME = {
|
|
|
28
30
|
export const mimeOf = (name) => MIME[path.extname(name).slice(1).toLowerCase()] || "application/octet-stream";
|
|
29
31
|
export const hashBytes = (buf) => createHash("sha256").update(buf).digest("hex");
|
|
30
32
|
export const relOf = (unit, urlPath) => urlPath.slice(unit.length);
|
|
33
|
+
/**
|
|
34
|
+
* A REPO folder or a URL, as the unit path it publishes to: `<project>/prototypes/<name>`
|
|
35
|
+
* is the nesting a space clone keeps, served at `/<project>/<name>/`. An agent has just been
|
|
36
|
+
* looking at the folder, so it is the spelling it will type.
|
|
37
|
+
*/
|
|
38
|
+
export function unitPathFor(input) {
|
|
39
|
+
const s = String(input == null ? "" : input).trim().slice(0, 300).replace(/\/prototypes\//g, "/");
|
|
40
|
+
if (!s) return "";
|
|
41
|
+
const t = s.replace(/^\.\//, "").replace(/\/{2,}/g, "/");
|
|
42
|
+
if (!t || t === "/") return "/";
|
|
43
|
+
return `/${t.replace(/^\/+/, "").replace(/\/+$/, "")}/`;
|
|
44
|
+
}
|
|
31
45
|
export const urlOf = (unit, rel) => unit + rel;
|
|
32
46
|
|
|
33
47
|
export function scanFolder(dir) {
|
|
@@ -99,17 +113,30 @@ export const registryList = () => readRegistry().drafts;
|
|
|
99
113
|
* `/__unit/<verb>` and blobs at `/__publish/<space>/blob/<hash>`. Non-2xx answers come back
|
|
100
114
|
* as `{status, ...body}` rather than throwing, because a 409 is an answer, not a failure.
|
|
101
115
|
*/
|
|
116
|
+
/**
|
|
117
|
+
* The server answers a token it does not know with a bare `forbidden` and no sentence,
|
|
118
|
+
* ON PURPOSE — a guessed token must learn nothing. This side knows something the server
|
|
119
|
+
* will not say: the token it just sent is the one saved on this machine, so a bare
|
|
120
|
+
* refusal means that token is dead here — revoked by a role change or a removal, or
|
|
121
|
+
* minted for another workspace — and the way back is to pair again.
|
|
122
|
+
*/
|
|
123
|
+
export const TOKEN_NOT_ACCEPTED = "this machine's publish token is not accepted here — it may have been revoked (a role change or a removal does that), or it belongs to another workspace. Run `augur connect` again.";
|
|
124
|
+
export function explainRefusal(body, status) {
|
|
125
|
+
if (status === 403 && body && body.error === "forbidden" && !body.message) return { ...body, message: TOKEN_NOT_ACCEPTED };
|
|
126
|
+
return body;
|
|
127
|
+
}
|
|
128
|
+
|
|
102
129
|
export function unitClient({ origin, token, space, session }) {
|
|
103
130
|
const headers = { Authorization: `Bearer ${token}`, "X-Augur-Session": session || "" };
|
|
104
131
|
const post = async (verb, body) => {
|
|
105
132
|
const r = await fetch(`${origin}/__unit/${verb}`, { method: "POST", headers: { ...headers, "content-type": "application/json" }, body: JSON.stringify(body) });
|
|
106
133
|
const out = await r.json().catch(() => ({}));
|
|
107
|
-
return r.ok ? out : { status: r.status, ...out };
|
|
134
|
+
return r.ok ? out : { status: r.status, ...explainRefusal(out, r.status) };
|
|
108
135
|
};
|
|
109
136
|
const get = async (verb, unit) => {
|
|
110
137
|
const r = await fetch(`${origin}/__unit/${verb}?unit=${encodeURIComponent(unit)}`, { headers });
|
|
111
138
|
const out = await r.json().catch(() => ({}));
|
|
112
|
-
return r.ok ? out : { status: r.status, ...out };
|
|
139
|
+
return r.ok ? out : { status: r.status, ...explainRefusal(out, r.status) };
|
|
113
140
|
};
|
|
114
141
|
return {
|
|
115
142
|
open: (b) => post("open", b), save: (b) => post("save", b), land: (b) => post("land", b),
|
|
@@ -117,16 +144,31 @@ export function unitClient({ origin, token, space, session }) {
|
|
|
117
144
|
main: (unit) => get("main", unit),
|
|
118
145
|
async blobPut(h, body) {
|
|
119
146
|
const r = await fetch(`${origin}/__publish/${space}/blob/${h}`, { method: "PUT", headers, body });
|
|
120
|
-
if (!r.ok && r.status !== 204) throw
|
|
147
|
+
if (!r.ok && r.status !== 204) throw await refusalError("blob upload", r);
|
|
121
148
|
},
|
|
122
149
|
async blobGet(h) {
|
|
123
150
|
const r = await fetch(`${origin}/__publish/${space}/blob/${h}`, { headers });
|
|
124
|
-
if (!r.ok) throw
|
|
151
|
+
if (!r.ok) throw await refusalError("blob fetch", r);
|
|
125
152
|
return Buffer.from(await r.arrayBuffer());
|
|
126
153
|
},
|
|
127
154
|
};
|
|
128
155
|
}
|
|
129
156
|
|
|
157
|
+
/**
|
|
158
|
+
* A non-2xx answer from the blob routes, carried as an error that REMEMBERS it was an
|
|
159
|
+
* answer: the status and whatever the server said. `guarded` turns that back into a
|
|
160
|
+
* result, so a role refusal on an upload reads as the refusal it is — before this, a
|
|
161
|
+
* viewer's save came back as "unreachable, nothing is lost", which was neither.
|
|
162
|
+
*/
|
|
163
|
+
async function refusalError(what, r) {
|
|
164
|
+
let body = null;
|
|
165
|
+
try { body = await r.json(); } catch (e) { /* not JSON */ }
|
|
166
|
+
const err = new Error(`${what} failed: ${r.status}${body && body.error ? ` ${body.error}` : ""}`);
|
|
167
|
+
err.status = r.status;
|
|
168
|
+
err.body = body && typeof body === "object" ? explainRefusal(body, r.status) : null;
|
|
169
|
+
return err;
|
|
170
|
+
}
|
|
171
|
+
|
|
130
172
|
// ── the verbs ────────────────────────────────────────────────────────────────
|
|
131
173
|
// `blobPut`/`blobGet` throw on a non-2xx answer, and a raw `fetch` can reject outright
|
|
132
174
|
// (offline, DNS, a dropped connection mid-transfer) — a transient failure anywhere inside
|
|
@@ -138,7 +180,15 @@ export function unitClient({ origin, token, space, session }) {
|
|
|
138
180
|
function guarded(fn) {
|
|
139
181
|
return async (...args) => {
|
|
140
182
|
try { return await fn(...args); }
|
|
141
|
-
catch (err) {
|
|
183
|
+
catch (err) {
|
|
184
|
+
// An answer the server gave (a status) is a refusal and keeps the server's words;
|
|
185
|
+
// anything else — DNS, a dropped connection, a thrown fixture — is the network.
|
|
186
|
+
if (err && err.status) {
|
|
187
|
+
const body = err.body || {};
|
|
188
|
+
return { ok: false, status: err.status, error: body.error || "refused", message: body.message || String(err.message), ...(body.reason ? { reason: body.reason } : {}) };
|
|
189
|
+
}
|
|
190
|
+
return { ok: false, error: "network", message: String((err && err.message) || err) };
|
|
191
|
+
}
|
|
142
192
|
};
|
|
143
193
|
}
|
|
144
194
|
|
|
@@ -150,11 +200,19 @@ async function materialise(client, unit, table, dir) {
|
|
|
150
200
|
}
|
|
151
201
|
}
|
|
152
202
|
|
|
153
|
-
async function doOpenImpl({ client, unit, dir, origin, space, session, now }) {
|
|
203
|
+
async function doOpenImpl({ client, unit, dir, origin, space, session, now, isNew = false }) {
|
|
154
204
|
if (fs.existsSync(dir) && fs.readdirSync(dir).length) return { ok: false, error: "folder-not-empty", dir };
|
|
155
205
|
const createdFolder = !fs.existsSync(dir);
|
|
156
206
|
const o = await client.open({ unit });
|
|
157
207
|
if (o.status) return { ok: false, ...o };
|
|
208
|
+
// A unit with no files is one that does not exist yet. Creating one is a decision the
|
|
209
|
+
// caller states with `isNew`; without it, a typo would quietly open an empty draft on a
|
|
210
|
+
// prototype that was never there. Either mismatch hands the draft straight back.
|
|
211
|
+
const exists = Object.keys(o.table || {}).length > 0;
|
|
212
|
+
if (exists !== !isNew) {
|
|
213
|
+
try { await client.discard({ unit, draftId: o.draftId }); } catch (e) { /* best-effort */ }
|
|
214
|
+
return { ok: false, error: exists ? "unit-exists" : "unknown-unit", unit };
|
|
215
|
+
}
|
|
158
216
|
// From here the server-side draft exists, so any failure below must both undo what we
|
|
159
217
|
// wrote to disk and tell the server to drop the orphan — otherwise a retry finds a
|
|
160
218
|
// half-materialised folder (`folder-not-empty`) and the draft it opened is never freed.
|
|
@@ -177,7 +235,7 @@ async function doOpenImpl({ client, unit, dir, origin, space, session, now }) {
|
|
|
177
235
|
throw err;
|
|
178
236
|
}
|
|
179
237
|
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 };
|
|
238
|
+
return { ok: true, draftId: o.draftId, address: `${origin}${o.address}`, files: Object.keys(o.table).length, others, isNew: !exists };
|
|
181
239
|
}
|
|
182
240
|
|
|
183
241
|
async function doSaveImpl({ client, dir, baseRevision, baseTable }) {
|
|
@@ -206,7 +264,17 @@ async function doLandImpl({ client, dir, note }) {
|
|
|
206
264
|
if (!st) return { ok: false, error: "not-a-draft", dir };
|
|
207
265
|
const saved = await doSave({ client, dir });
|
|
208
266
|
if (!saved.ok) return saved;
|
|
209
|
-
|
|
267
|
+
// Every landing in a space writes the one manifest by compare-and-set, so eight agents
|
|
268
|
+
// landing eight prototypes in the same second contend on it. The server retries a few
|
|
269
|
+
// times and then answers `manifest-contended`; that is a moment, not a refusal — the
|
|
270
|
+
// draft is still open, its lease released — so this side lands again, with a little
|
|
271
|
+
// jitter, before telling anyone. Measured live: 8 parallel landings, 2 contended.
|
|
272
|
+
let r;
|
|
273
|
+
for (let attempt = 0; ; attempt++) {
|
|
274
|
+
r = await client.land({ unit: st.unit, draftId: st.draftId, baseRevision: st.baseRevision, note: note || "" });
|
|
275
|
+
if (r.error !== "manifest-contended" || attempt >= LAND_RETRIES) break;
|
|
276
|
+
await new Promise((res) => setTimeout(res, 150 + Math.random() * 400 * (attempt + 1)));
|
|
277
|
+
}
|
|
210
278
|
if (r.status) return { ok: false, ...r };
|
|
211
279
|
st.landed = true; st.landedRevision = r.revision;
|
|
212
280
|
writeState(dir, st);
|
|
@@ -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 {
|
|
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(
|
|
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,19 @@ 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
|
|
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
|
-
die(`could not
|
|
46
|
+
if (r.error === "network") die(`could not reach the instance (${r.message}).`);
|
|
47
|
+
die(`could not open: ${r.error || r.status}${r.reason ? ` (${r.reason})` : ""}${r.message ? ` — ${r.message}` : ""}`);
|
|
45
48
|
}
|
|
46
|
-
log(`draft ${r.draftId} on ${unit} — ${r.files} file(s) in ${dir}`);
|
|
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}`);
|
|
47
50
|
// The agent tool's hooks, installed for this machine the first time a draft is opened
|
|
48
51
|
// here (idempotent; `AUGUR_NO_ADAPTERS=1` skips it — the suite and CI set it).
|
|
49
52
|
if (!process.env.AUGUR_NO_ADAPTERS) {
|
package/scripts/publish.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import { readFile } from "node:fs/promises";
|
|
|
25
25
|
import path from "node:path";
|
|
26
26
|
import { fileURLToPath } from "node:url";
|
|
27
27
|
import { findShellDir, deployConfig, originHost } from "./lib/instance.mjs";
|
|
28
|
+
import { draftsServed } from "./lib/draft.mjs";
|
|
28
29
|
import { composePublish, filterLitter, unitPaths } from "./lib/publish-compose.mjs";
|
|
29
30
|
import { collectEvidence } from "./lib/publish-evidence.mjs";
|
|
30
31
|
import { stripVolatileHead } from "./lib/publish-conflict.mjs";
|
|
@@ -115,6 +116,17 @@ let ORIGIN = (process.env.AUGUR_ORIGIN || DEPLOY_ENV.AUGUR_ORIGIN ||
|
|
|
115
116
|
deployConfig(ROOT, originHost(cwdSpaceOrigin)).siteOrigin || cwdSpaceOrigin || "")
|
|
116
117
|
.replace(/\/+$/, "");
|
|
117
118
|
if (!ORIGIN) die("no target origin — set AUGUR_ORIGIN, or add \"siteOrigin\" to space.json.");
|
|
119
|
+
// Where the instance serves drafts, a TREE is not what goes live any more: a prototype is
|
|
120
|
+
// changed by opening it and landing it (docs/drafts-that-land.md), and publishing a whole
|
|
121
|
+
// checkout there would put every session's half-done work on the site at once. The engine
|
|
122
|
+
// chrome (`--engine`) is not content and still goes this way.
|
|
123
|
+
if (!ENGINE_ONLY && await draftsServed(ORIGIN)) {
|
|
124
|
+
die(`${ORIGIN} serves drafts, so a prototype is changed by opening it, not by publishing a tree:\n\n` +
|
|
125
|
+
` augur open <opportunity>/<prototype> # a folder of its own, live at once at its draft address\n` +
|
|
126
|
+
` …edit; every save is live there…\n` +
|
|
127
|
+
` augur land # the real URL moves; the last line printed is the live URL\n\n` +
|
|
128
|
+
`A prototype that does not exist yet: augur open --new <opportunity>/<name>. Read agents/drafts.md in the engine clone.`);
|
|
129
|
+
}
|
|
118
130
|
// A WORKSPACE THAT MOVED answers its old address with a redirect. A checkout that still
|
|
119
131
|
// names the old address (space.json not yet pulled, a stale AUGUR_ORIGIN) would otherwise
|
|
120
132
|
// POST through that redirect and read the answer as a bad token. One GET with redirects
|
|
@@ -642,6 +654,13 @@ function selfUpdate(why) {
|
|
|
642
654
|
if (NO_SELF_UPDATE || selfUpdateTried || process.env.AUGUR_SELF_UPDATED === "1") return false;
|
|
643
655
|
selfUpdateTried = true;
|
|
644
656
|
const git = (...a) => execFileSync("git", ["-C", ROOT, ...a], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
657
|
+
// A package install (`npm i -g @augurworks/augur`, or an `npx` run) has no clone to
|
|
658
|
+
// fast-forward. Say the one line that updates it, in the same place the clone would
|
|
659
|
+
// have updated itself, and let the caller report the skew.
|
|
660
|
+
if (!existsSync(path.join(ROOT, ".git"))) {
|
|
661
|
+
log(`this engine is a package install and is behind what the instance speaks (${why}) — update it: npm i -g @augurworks/augur@latest`);
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
645
664
|
try {
|
|
646
665
|
if (git("rev-parse", "--is-inside-work-tree") !== "true") return false;
|
|
647
666
|
if (git("status", "--porcelain")) {
|
package/scripts/read.mjs
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
// under _read/<unit>/ beside the draft folders; files carry no write bit and the deny hook
|
|
4
4
|
// refuses edits there. `augur close` inside it removes it. See docs/drafts-that-land.md §7.
|
|
5
5
|
import path from "node:path";
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import { unitClient, doRead, readDirFor } from "./lib/draft.mjs";
|
|
6
|
+
import fs from "node:fs";
|
|
7
|
+
import { target, buildStamp } from "./lib/store.mjs";
|
|
8
|
+
import { unitClient, doRead, readDirFor, unitPathFor } from "./lib/draft.mjs";
|
|
9
9
|
import { normUnit } from "../src/unit-core.mjs";
|
|
10
10
|
|
|
11
11
|
const log = (m) => console.error(`\x1b[35m[read]\x1b[0m ${m}`);
|
|
@@ -15,17 +15,24 @@ const opt = (n) => { const i = argv.indexOf(n); return i > -1 ? argv[i + 1] : nu
|
|
|
15
15
|
const positional = argv.filter((a, i) => !a.startsWith("--") && !(i > 0 && argv[i - 1].startsWith("--")));
|
|
16
16
|
const raw = positional[0];
|
|
17
17
|
if (!raw) die("name a prototype: `augur read <opportunity>/<prototype>`.");
|
|
18
|
-
const unit = normUnit(
|
|
18
|
+
const unit = normUnit(unitPathFor(raw) || raw);
|
|
19
19
|
if (!unit) die(`"${raw}" is not a prototype path.`);
|
|
20
20
|
let origin, token;
|
|
21
21
|
try { ({ origin, token } = target({ needToken: true })); } catch (e) { die(e.message); }
|
|
22
22
|
const dir = path.resolve(opt("--dir") || readDirFor(unit));
|
|
23
|
-
|
|
23
|
+
// The blob routes are per space, so the copy needs the space id exactly as `open` does:
|
|
24
|
+
// from a space.json in this folder, else from what the instance says it serves.
|
|
25
|
+
let space = null;
|
|
26
|
+
try { space = JSON.parse(fs.readFileSync("space.json", "utf8")).id; } catch (e) { /* not in a space folder */ }
|
|
27
|
+
if (!space) { try { space = Object.keys((await buildStamp(origin)).spaces || {})[0] || null; } catch (e) { /* stamp unreachable */ } }
|
|
28
|
+
if (!space) die("could not tell which space this instance serves — run from a folder with space.json, or set AUGUR_ORIGIN.");
|
|
29
|
+
const client = unitClient({ origin, token, space, session: "" });
|
|
24
30
|
const r = await doRead({ client, unit, dir, origin, now: new Date().toISOString() });
|
|
25
31
|
if (!r.ok) {
|
|
26
32
|
if (r.error === "folder-not-empty") die(`${r.dir} is not empty — pick another folder with --dir.`);
|
|
27
33
|
if (r.error === "units-not-configured") die("this instance does not serve drafts yet (no unit store bound).");
|
|
28
|
-
die(`could not
|
|
34
|
+
if (r.error === "network") die(`could not reach the instance (${r.message}).`);
|
|
35
|
+
die(`could not read: ${r.error || r.status}${r.reason ? ` (${r.reason})` : ""}${r.message ? ` — ${r.message}` : ""}`);
|
|
29
36
|
}
|
|
30
37
|
log(`${r.files} file(s) of ${unit} at revision ${r.revision}, read-only`);
|
|
31
38
|
console.log(dir);
|
package/scripts/save.mjs
CHANGED
|
@@ -15,7 +15,9 @@ const client = unitClient({ origin, token, space: st.space, session: st.session
|
|
|
15
15
|
const r = await doSave({ client, dir });
|
|
16
16
|
if (!r.ok) {
|
|
17
17
|
if (r.error === "stale-draft" || r.error === "stale-draft-revision") die("this draft moved under you (another process saved to it) — run `augur sync`.");
|
|
18
|
-
die(`
|
|
18
|
+
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.`);
|
|
19
|
+
if (r.error === "network") die(`could not reach the instance (${r.message}). Nothing is lost — the next save carries every change since.`);
|
|
20
|
+
die(`save refused: ${r.error || r.status}${r.message ? ` — ${r.message}` : ""}`);
|
|
19
21
|
}
|
|
20
22
|
if (r.changed.length) console.error(`\x1b[35m[save]\x1b[0m ${r.changed.length} file(s) live at ${origin}${st.address}`);
|
|
21
23
|
console.log(`${origin}${st.address}`);
|
package/scripts/status.mjs
CHANGED
|
@@ -16,7 +16,6 @@ import { execFileSync } from "node:child_process";
|
|
|
16
16
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
17
17
|
import path from "node:path";
|
|
18
18
|
import { resolveOrigin, resolveToken, apiClient, buildStamp, ENGINE_ROOT } from "./lib/store.mjs";
|
|
19
|
-
import { fetchMarks, markLine } from "./lib/marks.mjs";
|
|
20
19
|
import { registryList, unitClient, draftsReport } from "./lib/draft.mjs";
|
|
21
20
|
|
|
22
21
|
const args = process.argv.slice(2);
|
|
@@ -123,28 +122,11 @@ console.log("");
|
|
|
123
122
|
console.log(` ${C.dim}engine chrome ${eng.sha ? eng.sha.slice(0, 12) : "—"}` +
|
|
124
123
|
`${eng.version ? ` (v${eng.version})` : ""}${eng.publishedAt ? ` · shipped ${eng.publishedAt}` : ""}${C.off}`);
|
|
125
124
|
|
|
126
|
-
// ──
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
//
|
|
130
|
-
|
|
131
|
-
//
|
|
132
|
-
// NEEDS A TOKEN and does not ask for one. `status` has always been the command that runs
|
|
133
|
-
// with nothing configured — the build stamp is public — so a missing credential quietly
|
|
134
|
-
// costs the marks section and nothing else. It affects the exit code in no way: marks
|
|
135
|
-
// refuse nothing, and a mark is not a problem.
|
|
136
|
-
const markToken = resolveToken(origin);
|
|
137
|
-
if (markToken) {
|
|
138
|
-
const marks = await fetchMarks(apiClient(origin, markToken));
|
|
139
|
-
if (marks.length) {
|
|
140
|
-
console.log("");
|
|
141
|
-
console.log(` ${C.dim}being worked on right now${C.off}`);
|
|
142
|
-
for (const m of marks) console.log(` ${C.dim} ${markLine(m)}${C.off}`);
|
|
143
|
-
}
|
|
144
|
-
} else {
|
|
145
|
-
// The one thing a fresh agent needs to know and has no other way to learn: this machine
|
|
146
|
-
// holds no token, and the way to get one is pairing, not a password. Same words as the
|
|
147
|
-
// instance's own /llms.txt. Costs nothing here — status never needed the token.
|
|
125
|
+
// ── not paired? say how, once ───────────────────────────────────────────────
|
|
126
|
+
// The one thing a fresh agent needs to know and has no other way to learn: this machine
|
|
127
|
+
// holds no token, and the way to get one is pairing, not a password. Same words as the
|
|
128
|
+
// instance's own /llms.txt. Costs nothing here — status never needed the token.
|
|
129
|
+
if (!resolveToken(origin)) {
|
|
148
130
|
console.log("");
|
|
149
131
|
console.log(` ${C.dim}not paired with ${origin}. Publishing from here needs a token; get one without a password:${C.off}`);
|
|
150
132
|
console.log(` ${C.dim} augur connect --origin ${origin} (the owner approves a code in a signed-in browser)${C.off}`);
|
package/scripts/sync.mjs
CHANGED
|
@@ -15,7 +15,9 @@ const token = resolveToken(origin);
|
|
|
15
15
|
if (!token) die("no publish token — run `augur connect` once.");
|
|
16
16
|
const client = unitClient({ origin, token, space: st.space, session: st.session });
|
|
17
17
|
const r = await doSync({ client, dir });
|
|
18
|
-
if (!r.ok) die(`
|
|
18
|
+
if (!r.ok && 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.`);
|
|
19
|
+
if (!r.ok && r.error === "network") die(`could not reach the instance (${r.message}). Nothing is lost — run augur sync again.`);
|
|
20
|
+
if (!r.ok) die(`sync refused: ${r.error || r.status}${r.message ? ` — ${r.message}` : ""}`);
|
|
19
21
|
for (const f of r.taken) log(`took theirs ${f}`);
|
|
20
22
|
for (const f of r.merged) log(`merged ${f}`);
|
|
21
23
|
for (const c of r.conflicts) {
|