@marver-design/marver 0.10.2 → 0.11.0
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/CHANGELOG.md +143 -0
- package/dist/auth-B5yuwnOq.mjs +494 -0
- package/dist/{build-zUf_kwKp.mjs → build-Cr_ZLMdq.mjs} +3 -2
- package/dist/cli.mjs +6 -6
- package/dist/{collab-s3k5byM1.mjs → collab-C-n-uQdm.mjs} +67 -11
- package/dist/{comments-J06jqCVV.mjs → comments-CRqaO2MH.mjs} +26 -11
- package/dist/{comments-BZBKhKRO.mjs → comments-DZyobpxG.mjs} +8 -2
- package/dist/{manifest-DIsp3ldB.mjs → config-t9coJ-Pq.mjs} +3 -233
- package/dist/{daemon-BpKBJZee.mjs → daemon-DSmaS453.mjs} +23 -5
- package/dist/{dev-D2PG6igI.mjs → dev-CLBjRs0J.mjs} +117 -13
- package/dist/{init-DwV0_d9G.mjs → init-BSclDg4I.mjs} +2 -1
- package/dist/manifest-C2tzkNaC.mjs +233 -0
- package/dist/marver-id-B8-3WiHk.mjs +411 -0
- package/dist/marver-id-gate-BDzW6ahN.mjs +728 -0
- package/dist/{plugin-C1sDewZ3.mjs → plugin-vE7sxEeT.mjs} +12 -84
- package/dist/{profile-BkiWglVE.mjs → profile-DcsJyppw.mjs} +9 -6
- package/dist/{serve-CZqPnj19.mjs → serve-CZ1KEQxd.mjs} +194 -42
- package/dist/sync-DELGomPk.mjs +397 -0
- package/dist/update-DuWDj5nR.mjs +77 -0
- package/dist/utm-CxC3QN5X.mjs +20 -0
- package/package.json +1 -1
- package/src/shared/utm.ts +2 -2
- package/templates/instructions/configure.md +4 -0
- package/templates/instructions/publish.md +84 -15
- package/dist/auth-KQ9Aj-nB.mjs +0 -245
- package/dist/events-BMtBvvgU.mjs +0 -101
- package/dist/sync-BJKKmy1n.mjs +0 -150
- package/dist/{shot-D7I0MCLu.mjs → shot-Cyv3GN79.mjs} +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -39,14 +39,14 @@ function version() {
|
|
|
39
39
|
}
|
|
40
40
|
const cli = cac(NAME);
|
|
41
41
|
cli.command("init", "Scaffold design/ in this repo").option("--mode <mode>", "studio | embedded", { default: "studio" }).option("--no-demo", "Skip the demo scene (the demo ships unless this flag is passed)").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
42
|
-
const { init } = await import("./init-
|
|
42
|
+
const { init } = await import("./init-BSclDg4I.mjs");
|
|
43
43
|
init(resolve(opts.root), {
|
|
44
44
|
mode: opts.mode === "embedded" ? "embedded" : "studio",
|
|
45
45
|
demo: opts.demo !== false
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
48
|
for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot reload, comments, Live Jam)"], ["canvas", "Start the local canvas - same as dev"]]) cli.command(name, desc).option("--root <dir>", "Host repo root", { default: "." }).option("--port <port>", "Port (default 5199)").action(async (opts) => {
|
|
49
|
-
const { dev } = await import("./dev-
|
|
49
|
+
const { dev } = await import("./dev-CLBjRs0J.mjs");
|
|
50
50
|
let port;
|
|
51
51
|
if (opts.port !== void 0) {
|
|
52
52
|
const n = Number(opts.port);
|
|
@@ -56,7 +56,7 @@ for (const [name, desc] of [["dev", "Start the local canvas (everything on: hot
|
|
|
56
56
|
await dev(resolve(opts.root), port);
|
|
57
57
|
});
|
|
58
58
|
cli.command("build", "Static export → design/.dist (what ships comes from design/publish.json - publishing is default-closed)").option("--boards <names>", "Publish only these boards (comma-separated); overrides the publish policy").option("--all-boards", "Publish every board - the loud override for the default-closed policy").option("--root <dir>", "Host repo root", { default: "." }).action(async (opts) => {
|
|
59
|
-
const { buildSite } = await import("./build-
|
|
59
|
+
const { buildSite } = await import("./build-Cr_ZLMdq.mjs");
|
|
60
60
|
try {
|
|
61
61
|
const boards = opts.boards === void 0 ? void 0 : typeof opts.boards === "string" ? opts.boards : "";
|
|
62
62
|
await buildSite(resolve(opts.root), boards, opts.allBoards === true);
|
|
@@ -66,7 +66,7 @@ cli.command("build", "Static export → design/.dist (what ships comes from desi
|
|
|
66
66
|
}
|
|
67
67
|
});
|
|
68
68
|
cli.command("serve", "Serve design/.dist (set MARVER_PASSWORD to gate it)").option("--root <dir>", "Host repo root", { default: "." }).option("--port <port>", "Port (default $PORT or 4199)").action(async (opts) => {
|
|
69
|
-
const { serve } = await import("./serve-
|
|
69
|
+
const { serve } = await import("./serve-CZ1KEQxd.mjs");
|
|
70
70
|
let port;
|
|
71
71
|
if (opts.port !== void 0) {
|
|
72
72
|
const n = Number(opts.port);
|
|
@@ -74,8 +74,8 @@ cli.command("serve", "Serve design/.dist (set MARVER_PASSWORD to gate it)").opti
|
|
|
74
74
|
}
|
|
75
75
|
serve(resolve(opts.root), port);
|
|
76
76
|
});
|
|
77
|
-
cli.command("comments <action> [value]", "Comment collaboration: connect <url> · sync · list · reply <thread> · resolve <thread> · invite <email> · revoke <email>").option("--root <dir>", "Host repo root", { default: "." }).option("--invite <token>", "connect: claim this invite instead of signing in").option("--canvas-password <password>", "connect: the canvas gate password (default $MARVER_PASSWORD or prompt)").option("--email <email>", "connect: account email (skips the prompt)").option("--password <password>", "connect: account password (skips the prompt - mind your shell history)").option("--name <name>", "connect --invite: display name for the new account").option("--open", "list: only unresolved threads").option("--json", "list: machine-readable output").option("--board <board>", "scope to one board").option("--body <text>", "reply: the reply text").option("--addressed-in <frame>", "resolve: the variant frame that answered the feedback").action(async (action, value, opts) => {
|
|
78
|
-
const { commentsCommand } = await import("./comments-
|
|
77
|
+
cli.command("comments <action> [value]", "Comment collaboration: connect <url> · sync · list · reply <thread> · resolve <thread> · invite <email> · revoke <email>").option("--root <dir>", "Host repo root", { default: "." }).option("--token <token>", "connect: the canvas's MARVER_CLI_TOKEN (default $MARVER_CLI_TOKEN) - the identity-mode path").option("--invite <token>", "connect: claim this invite instead of signing in").option("--canvas-password <password>", "connect: the canvas gate password (default $MARVER_PASSWORD or prompt)").option("--email <email>", "connect: account email (skips the prompt)").option("--password <password>", "connect: account password (skips the prompt - mind your shell history)").option("--name <name>", "connect --invite: display name for the new account").option("--open", "list: only unresolved threads").option("--json", "list: machine-readable output").option("--board <board>", "scope to one board").option("--body <text>", "reply: the reply text").option("--addressed-in <frame>", "resolve: the variant frame that answered the feedback").action(async (action, value, opts) => {
|
|
78
|
+
const { commentsCommand } = await import("./comments-CRqaO2MH.mjs");
|
|
79
79
|
try {
|
|
80
80
|
await commentsCommand(resolve(opts.root), action, value, opts);
|
|
81
81
|
} catch (err) {
|
|
@@ -1,8 +1,33 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { claimInvite,
|
|
1
|
+
import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
|
|
2
|
+
import { a as claimInvite, c as issueDeviceSession, d as publicUser, f as revokeUser, g as updateProfile, h as signOut, l as ownerName, m as signIn, o as createInvite, p as sessionUser, s as inviteInfo } from "./auth-B5yuwnOq.mjs";
|
|
3
3
|
import { readFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { randomBytes } from "node:crypto";
|
|
6
|
+
//#region src/server/secure-cookie.ts
|
|
7
|
+
/**
|
|
8
|
+
* Is this deployment https, for the purpose of setting `Secure` on a cookie?
|
|
9
|
+
*
|
|
10
|
+
* The obvious answer - read `X-Forwarded-Proto` - is wrong in exactly the case
|
|
11
|
+
* that matters, and it is the same trap that made `MARVER_PUBLIC_ORIGIN`
|
|
12
|
+
* mandatory for identity mode: nginx's own documented `proxy_pass
|
|
13
|
+
* http://localhost:PORT` sets no `X-Forwarded-*` at all. A canvas served over
|
|
14
|
+
* https behind that config sees no header, decides "not secure", and issues a
|
|
15
|
+
* thirty-day session cookie the browser will happily send over plain http.
|
|
16
|
+
*
|
|
17
|
+
* So the pinned origin wins whenever it is set. It is a deliberate statement by
|
|
18
|
+
* whoever deployed the canvas, rather than a guess about a proxy that may not be
|
|
19
|
+
* speaking. The header remains the fallback for a canvas with no pinned origin,
|
|
20
|
+
* where a guess is all there is - and http on loopback is the ordinary
|
|
21
|
+
* development case, where `Secure` would break the cookie entirely.
|
|
22
|
+
*/
|
|
23
|
+
function isSecureDeployment(req) {
|
|
24
|
+
const pinned = (process.env.MARVER_PUBLIC_ORIGIN ?? "").trim();
|
|
25
|
+
if (pinned) return pinned.toLowerCase().startsWith("https://");
|
|
26
|
+
return req.headers["x-forwarded-proto"] === "https";
|
|
27
|
+
}
|
|
28
|
+
/** `; Secure`, or nothing - the suffix a Set-Cookie line wants. */
|
|
29
|
+
const secureSuffix = (req) => isSecureDeployment(req) ? "; Secure" : "";
|
|
30
|
+
//#endregion
|
|
6
31
|
//#region src/server/collab.ts
|
|
7
32
|
const MONTH = 2592e3;
|
|
8
33
|
const MAX_BODY = 262144;
|
|
@@ -146,8 +171,11 @@ function collabHandler(dataDir, distDir) {
|
|
|
146
171
|
return hit;
|
|
147
172
|
};
|
|
148
173
|
const cookie = (req, name) => new RegExp(`(?:^|;\\s*)${name}=([\\w-]+)`).exec(String(req.headers.cookie ?? ""))?.[1];
|
|
174
|
+
/** browser session cookie, or a bearer token (the dev proxy / agent CLI path -
|
|
175
|
+
* same session store, held server-side by the Vite process, never by a page) */
|
|
176
|
+
const sessionToken = (req) => /^Bearer ([\w-]+)$/.exec(String(req.headers.authorization ?? ""))?.[1] ?? cookie(req, "mv_s") ?? null;
|
|
149
177
|
const currentUser = (req) => {
|
|
150
|
-
const tok =
|
|
178
|
+
const tok = sessionToken(req);
|
|
151
179
|
return tok ? sessionUser(dataDir, tok) : null;
|
|
152
180
|
};
|
|
153
181
|
const json = (res, code, body) => {
|
|
@@ -159,23 +187,36 @@ function collabHandler(dataDir, distDir) {
|
|
|
159
187
|
const readBody = (req) => new Promise((resolve, reject) => {
|
|
160
188
|
if (!/^application\/json\b/.test(String(req.headers["content-type"] ?? ""))) return reject(/* @__PURE__ */ new Error("content-type must be application/json"));
|
|
161
189
|
let body = "";
|
|
190
|
+
let done = false;
|
|
191
|
+
const settle = (fn) => {
|
|
192
|
+
if (done) return;
|
|
193
|
+
done = true;
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
fn();
|
|
196
|
+
};
|
|
197
|
+
const timer = setTimeout(() => settle(() => {
|
|
198
|
+
req.destroy();
|
|
199
|
+
reject(/* @__PURE__ */ new Error("body timed out"));
|
|
200
|
+
}), 1e4);
|
|
201
|
+
if (typeof timer.unref === "function") timer.unref();
|
|
162
202
|
req.on("data", (c) => {
|
|
163
203
|
body += c;
|
|
164
|
-
if (body.length > MAX_BODY) {
|
|
165
|
-
reject(/* @__PURE__ */ new Error("body too large"));
|
|
204
|
+
if (body.length > MAX_BODY) settle(() => {
|
|
166
205
|
req.destroy();
|
|
167
|
-
|
|
206
|
+
reject(/* @__PURE__ */ new Error("body too large"));
|
|
207
|
+
});
|
|
168
208
|
});
|
|
169
|
-
req.on("end", () => {
|
|
209
|
+
req.on("end", () => settle(() => {
|
|
170
210
|
try {
|
|
171
211
|
resolve(body ? JSON.parse(body) : {});
|
|
172
212
|
} catch {
|
|
173
213
|
reject(/* @__PURE__ */ new Error("bad json"));
|
|
174
214
|
}
|
|
175
|
-
});
|
|
215
|
+
}));
|
|
216
|
+
req.on("error", () => settle(() => reject(/* @__PURE__ */ new Error("request failed"))));
|
|
176
217
|
});
|
|
177
218
|
const setSession = (req, res, token) => {
|
|
178
|
-
const secure = req
|
|
219
|
+
const secure = secureSuffix(req);
|
|
179
220
|
res.setHeader("set-cookie", [`mv_s=${token}; Path=/; Max-Age=${MONTH}; HttpOnly; SameSite=Lax${secure}`, `mv_c=${randomBytes(16).toString("base64url")}; Path=/; Max-Age=${MONTH}; SameSite=Lax${secure}`]);
|
|
180
221
|
};
|
|
181
222
|
const csrfOk = (req) => !!/^Bearer /.test(String(req.headers.authorization ?? "")) || !cookie(req, "mv_s") || !!cookie(req, "mv_c") && req.headers["x-mv-c"] === cookie(req, "mv_c");
|
|
@@ -235,7 +276,7 @@ function collabHandler(dataDir, distDir) {
|
|
|
235
276
|
return false;
|
|
236
277
|
}
|
|
237
278
|
if (req.method !== "POST") return false;
|
|
238
|
-
if (path
|
|
279
|
+
if (!(path === "auth/claim" || path === "auth/signin") && !csrfOk(req)) return json(res, 403, { error: "missing or stale request token - reload the page" }), true;
|
|
239
280
|
if (path === "auth/claim") {
|
|
240
281
|
if (limited(`ip:${ip}`)) return json(res, 429, { error: "too many attempts - wait a minute" }), true;
|
|
241
282
|
const b = await readBody(req);
|
|
@@ -272,12 +313,27 @@ function collabHandler(dataDir, distDir) {
|
|
|
272
313
|
});
|
|
273
314
|
return json(res, 200, { user: publicUser(next) }), true;
|
|
274
315
|
}
|
|
316
|
+
if (path === "cli-session") {
|
|
317
|
+
const bearer = /^Bearer ([\w-]+)$/.exec(String(req.headers.authorization ?? ""))?.[1] ?? "";
|
|
318
|
+
if (limited(`ip:${ip}`)) return json(res, 429, { error: "too many attempts - wait a minute" }), true;
|
|
319
|
+
const issued = issueDeviceSession(dataDir, bearer);
|
|
320
|
+
if (!issued) return json(res, 401, { error: "MARVER_CLI_TOKEN was refused, or nobody owns this canvas yet" }), true;
|
|
321
|
+
return json(res, 200, {
|
|
322
|
+
token: issued.token,
|
|
323
|
+
exp: issued.exp,
|
|
324
|
+
user: publicUser(issued.user)
|
|
325
|
+
}), true;
|
|
326
|
+
}
|
|
275
327
|
if (path === "invite") {
|
|
276
328
|
if (!can(currentUser(req), rights, "", "admin")) return json(res, 403, { error: "owner only" }), true;
|
|
277
329
|
const b = await readBody(req);
|
|
278
330
|
const email = String(b.email ?? "");
|
|
279
331
|
if (!/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(email)) return json(res, 400, { error: "a valid email is required" }), true;
|
|
280
|
-
|
|
332
|
+
const idMode = !!process.env.MARVER_ID_ISSUER;
|
|
333
|
+
return json(res, 200, {
|
|
334
|
+
...createInvite(dataDir, email),
|
|
335
|
+
idMode
|
|
336
|
+
}), true;
|
|
281
337
|
}
|
|
282
338
|
if (path === "revoke") {
|
|
283
339
|
if (!can(currentUser(req), rights, "", "admin")) return json(res, 403, { error: "owner only" }), true;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import { n as replay } from "./
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { n as localProfile } from "./profile-BkiWglVE.mjs";
|
|
2
|
+
import { a as syncOnce, i as loadCollab, n as connectClaim, r as connectToken, s as replay, t as connect } from "./sync-DELGomPk.mjs";
|
|
3
|
+
import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
|
|
4
|
+
import { n as localProfile } from "./profile-DcsJyppw.mjs";
|
|
6
5
|
import { join } from "node:path";
|
|
7
6
|
import { createInterface } from "node:readline";
|
|
8
7
|
import { randomUUID } from "node:crypto";
|
|
@@ -10,8 +9,11 @@ import { randomUUID } from "node:crypto";
|
|
|
10
9
|
/**
|
|
11
10
|
* `marver comments <action>` - the collaboration CLI.
|
|
12
11
|
*
|
|
13
|
-
* connect <url> sign in
|
|
14
|
-
*
|
|
12
|
+
* connect <url> sign in against a published canvas and store the device credential
|
|
13
|
+
* in ~/.marver/canvases/ (outside the repo - `dev` serves the repo).
|
|
14
|
+
* Three ways in: --token <t>, the
|
|
15
|
+
* canvas's MARVER_CLI_TOKEN (the only one an identity-mode canvas
|
|
16
|
+
* has), --invite <t> to claim an invite, or email + password.
|
|
15
17
|
* sync one full exchange with the publish target (agent/CI path)
|
|
16
18
|
* list threads from design/comments/ (--open, --board, --json)
|
|
17
19
|
* reply <thread> append a reply (--body) - the agent's voice in the loop
|
|
@@ -53,6 +55,13 @@ async function commentsCommand(root, action, value, opts) {
|
|
|
53
55
|
switch (action) {
|
|
54
56
|
case "connect": {
|
|
55
57
|
if (!value) throw new Error("usage: comments connect <published-url>");
|
|
58
|
+
const cliToken = opts.token ?? process.env.MARVER_CLI_TOKEN;
|
|
59
|
+
if (cliToken) {
|
|
60
|
+
await connectToken(root, value, String(cliToken));
|
|
61
|
+
console.log(`[${NAME}] connected - the device credential is in ~/.marver/canvases/, outside this repo`);
|
|
62
|
+
console.log(`[${NAME}] \`${NAME} dev\` now syncs comments every 30s; \`${NAME} comments sync\` does one exchange`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
56
65
|
const canvasPassword = opts.canvasPassword ?? process.env.MARVER_PASSWORD ?? await ask("canvas password (blank if the canvas is ungated): ", true);
|
|
57
66
|
if (opts.invite) {
|
|
58
67
|
const name = opts.name ?? await ask("display name: ");
|
|
@@ -66,7 +75,7 @@ async function commentsCommand(root, action, value, opts) {
|
|
|
66
75
|
const password = opts.password ?? await ask("password: ", true);
|
|
67
76
|
await connect(root, value, email, password, canvasPassword || void 0);
|
|
68
77
|
}
|
|
69
|
-
console.log(`[${NAME}] connected -
|
|
78
|
+
console.log(`[${NAME}] connected - the device credential is in ~/.marver/canvases/, outside this repo`);
|
|
70
79
|
console.log(`[${NAME}] \`${NAME} dev\` now syncs comments every 30s; \`${NAME} comments sync\` does one exchange`);
|
|
71
80
|
return;
|
|
72
81
|
}
|
|
@@ -123,12 +132,18 @@ async function commentsCommand(root, action, value, opts) {
|
|
|
123
132
|
}
|
|
124
133
|
case "invite": {
|
|
125
134
|
if (!value || !/^[^@\s]+@[^@\s]+\.[^@\s]+$/.test(value)) throw new Error("usage: comments invite <email>");
|
|
126
|
-
const { token } = await ownerApi(root, "invite", { email: value });
|
|
135
|
+
const { token, idMode } = await ownerApi(root, "invite", { email: value });
|
|
127
136
|
const url = loadCollab(root).url.replace(/\/+$/, "");
|
|
128
137
|
console.log(` invite for ${value} (single-use, 7 days):`);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
if (idMode) {
|
|
139
|
+
console.log(` ${url}`);
|
|
140
|
+
console.log(` send them that address - they sign in with ${value} and they are in.`);
|
|
141
|
+
console.log(` no link to forward, no password: the invitation IS the email address.`);
|
|
142
|
+
} else {
|
|
143
|
+
console.log(` ${url}/#/i/${token}`);
|
|
144
|
+
console.log(` send them that link (plus the canvas password) - it opens straight into`);
|
|
145
|
+
console.log(` "pick your name and password".`);
|
|
146
|
+
}
|
|
132
147
|
return;
|
|
133
148
|
}
|
|
134
149
|
case "revoke":
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "./
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
2
2
|
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, statSync, writeSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
//#region src/server/comments.ts
|
|
@@ -11,6 +11,12 @@ import { join } from "node:path";
|
|
|
11
11
|
* state is derived by replaying events (ordered by ts then id, last writer wins per
|
|
12
12
|
* field). Files, not a database - tens to low hundreds of events per board.
|
|
13
13
|
*/
|
|
14
|
+
var comments_exports = /* @__PURE__ */ __exportAll({
|
|
15
|
+
appendEvents: () => appendEvents,
|
|
16
|
+
dataDir: () => dataDir,
|
|
17
|
+
listBoards: () => listBoards,
|
|
18
|
+
readLog: () => readLog
|
|
19
|
+
});
|
|
14
20
|
const BOARD_RE = /^[a-z0-9][a-z0-9-]*$/;
|
|
15
21
|
const boardFile = (dir, board) => {
|
|
16
22
|
if (!BOARD_RE.test(board)) throw new Error(`bad board name: ${board}`);
|
|
@@ -81,4 +87,4 @@ function dataDir() {
|
|
|
81
87
|
return dir;
|
|
82
88
|
}
|
|
83
89
|
//#endregion
|
|
84
|
-
export {
|
|
90
|
+
export { readLog as i, comments_exports as n, listBoards as r, appendEvents as t };
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { delimiter, isAbsolute, join, relative, sep } from "node:path";
|
|
1
|
+
import { accessSync, constants, existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
4
3
|
import { pathToFileURL } from "node:url";
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
4
|
//#region src/server/detect.ts
|
|
7
5
|
function readJson(file) {
|
|
8
6
|
try {
|
|
@@ -368,232 +366,4 @@ function validViewports(v) {
|
|
|
368
366
|
return Object.keys(out).length ? out : null;
|
|
369
367
|
}
|
|
370
368
|
//#endregion
|
|
371
|
-
|
|
372
|
-
const FRAME_EXT = /\.(tsx|jsx|html)$/;
|
|
373
|
-
const RESERVED_SCENES = /* @__PURE__ */ new Set(["components", "screens"]);
|
|
374
|
-
/** Extract `export const meta = {...}` with literal string values only. Anything else is silently omitted. */
|
|
375
|
-
function extractMeta(src) {
|
|
376
|
-
const m = /export\s+const\s+meta\s*=\s*\{([\s\S]*?)\}/.exec(src);
|
|
377
|
-
if (!m) return {};
|
|
378
|
-
const body = m[1];
|
|
379
|
-
const pick = (key) => {
|
|
380
|
-
const r = new RegExp(`(?:^|[{,])\\s*${key}\\s*:\\s*(['"\`])([^'"\`]*)\\1`).exec(body);
|
|
381
|
-
return r ? r[2] : void 0;
|
|
382
|
-
};
|
|
383
|
-
const out = {};
|
|
384
|
-
const title = pick("title");
|
|
385
|
-
if (title) out.title = title;
|
|
386
|
-
const viewport = pick("viewport");
|
|
387
|
-
if (viewport) out.viewport = viewport;
|
|
388
|
-
const theme = pick("theme");
|
|
389
|
-
if (theme) out.theme = theme;
|
|
390
|
-
const of = pick("of");
|
|
391
|
-
if (of) out.of = of;
|
|
392
|
-
const variant = pick("variant");
|
|
393
|
-
if (variant) out.variant = variant;
|
|
394
|
-
const intent = pick("intent");
|
|
395
|
-
if (intent) out.intent = intent;
|
|
396
|
-
return out;
|
|
397
|
-
}
|
|
398
|
-
/** Content-frame detection: LEXICAL by convention - the frame file
|
|
399
|
-
* itself imports PKG/content (an import specifier scan, so "<Diagram" inside a
|
|
400
|
-
* string in a UI frame can never misbadge it; barrels/re-exports are not
|
|
401
|
-
* detected - meta.intent is the taught path and always works). Returns the
|
|
402
|
-
* inferred intent + natural width, or null for UI frames. */
|
|
403
|
-
const CONTENT_IMPORT = new RegExp(`from\\s+['"]${PKG}/content['"]`);
|
|
404
|
-
const WIDE_DOC = /<Doc\b[^>]*\blayout\s*=\s*["']wide["']/;
|
|
405
|
-
const contentWidthOf = (src) => WIDE_DOC.test(src) ? CONTENT_WIDTH.wide : CONTENT_WIDTH.document;
|
|
406
|
-
function contentScan(src) {
|
|
407
|
-
if (!CONTENT_IMPORT.test(src)) return null;
|
|
408
|
-
const count = (re) => (src.match(re) ?? []).length;
|
|
409
|
-
const diagrams = count(/<Diagram[\s>/]/g);
|
|
410
|
-
const imgs = count(/<Img[\s>/]/g);
|
|
411
|
-
const mds = count(/<Md[\s>/]/g);
|
|
412
|
-
return {
|
|
413
|
-
intent: diagrams > 0 ? "diagram" : imgs > mds ? "moodboard" : "spec",
|
|
414
|
-
width: contentWidthOf(src)
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
/** id = path relative to design/, extension dropped, `scenes/` prefix dropped. Always `/`-separated. */
|
|
418
|
-
function toFrameId(designRelPath) {
|
|
419
|
-
const noExt = designRelPath.split(sep).join("/").replace(FRAME_EXT, "");
|
|
420
|
-
return noExt.startsWith("scenes/") ? noExt.slice(7) : noExt;
|
|
421
|
-
}
|
|
422
|
-
/**
|
|
423
|
-
* A7: which frames does a changed `design/**` file affect? Manifest + directory conventions,
|
|
424
|
-
* NOT a module-graph walk (deterministic, cheap, may over-reload but never misses a
|
|
425
|
-
* conventionally-affected frame). Returns:
|
|
426
|
-
* null -> uncontrolled: leave to default Vite HMR (src/** deps, theme.css, config, assets)
|
|
427
|
-
* [] -> controlled but affects no current frame (e.g. a layout in an empty dir)
|
|
428
|
-
* [ids] -> controlled: the shell drives a rev-stamped reload of these frames
|
|
429
|
-
*/
|
|
430
|
-
function affectedFrameIds(absFile, root, manifest) {
|
|
431
|
-
const design = join(root, "design");
|
|
432
|
-
if (absFile !== design && !absFile.startsWith(design + sep)) return null;
|
|
433
|
-
const rel = relative(design, absFile).split(sep).join("/");
|
|
434
|
-
const name = rel.split("/").pop() ?? rel;
|
|
435
|
-
const designRel = `design/${rel}`;
|
|
436
|
-
const tsxFrames = manifest.frames.filter((f) => f.kind === "tsx");
|
|
437
|
-
if (name === "theme.css") return null;
|
|
438
|
-
if (rel === "providers.tsx" || rel === "providers.jsx") return tsxFrames.map((f) => f.id);
|
|
439
|
-
if (/(^|\/)(_layout\.(tsx|jsx)|_fixtures\.(ts|tsx|js|jsx|json))$/.test(rel)) {
|
|
440
|
-
const prefix = `design/${rel.slice(0, rel.length - name.length)}`;
|
|
441
|
-
return tsxFrames.filter((f) => f.file.startsWith(prefix)).map((f) => f.id);
|
|
442
|
-
}
|
|
443
|
-
const direct = manifest.frames.find((f) => f.file === designRel);
|
|
444
|
-
if (direct) return [direct.id];
|
|
445
|
-
return null;
|
|
446
|
-
}
|
|
447
|
-
function walk(dir, out = []) {
|
|
448
|
-
if (!existsSync(dir)) return out;
|
|
449
|
-
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
450
|
-
if (e.name.startsWith(".")) continue;
|
|
451
|
-
const p = join(dir, e.name);
|
|
452
|
-
if (e.isDirectory()) walk(p, out);
|
|
453
|
-
else out.push(p);
|
|
454
|
-
}
|
|
455
|
-
return out;
|
|
456
|
-
}
|
|
457
|
-
function scanFrames(root) {
|
|
458
|
-
const design = join(root, "design");
|
|
459
|
-
const frames = [];
|
|
460
|
-
for (const base of ["scenes", "components"]) for (const abs of walk(join(design, base))) {
|
|
461
|
-
const name = abs.split(sep).pop();
|
|
462
|
-
if (name.startsWith("_") || !FRAME_EXT.test(name)) continue;
|
|
463
|
-
const rel = relative(design, abs).split(sep).join("/");
|
|
464
|
-
const id = toFrameId(rel);
|
|
465
|
-
const scene = id.includes("/") ? id.split("/")[0] : "";
|
|
466
|
-
if (base === "scenes" && RESERVED_SCENES.has(scene)) {
|
|
467
|
-
console.error(`[marver] scene "${scene}" is a reserved name - "${rel}" skipped. Rename the scene.`);
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
const kind = name.endsWith(".html") ? "html" : "tsx";
|
|
471
|
-
const entry = {
|
|
472
|
-
id,
|
|
473
|
-
file: `design/${rel}`,
|
|
474
|
-
kind,
|
|
475
|
-
scene
|
|
476
|
-
};
|
|
477
|
-
if (kind === "tsx") {
|
|
478
|
-
const src = readFileSync(abs, "utf8");
|
|
479
|
-
const meta = extractMeta(src);
|
|
480
|
-
if (meta.title) entry.title = meta.title;
|
|
481
|
-
if (meta.viewport) entry.viewport = meta.viewport;
|
|
482
|
-
if (meta.theme) entry.theme = meta.theme;
|
|
483
|
-
if (meta.of) entry.variantGroup = meta.of;
|
|
484
|
-
if (meta.variant) entry.variant = meta.variant;
|
|
485
|
-
const content = contentScan(src);
|
|
486
|
-
if (content || meta.intent) {
|
|
487
|
-
entry.intent = meta.intent ?? content.intent;
|
|
488
|
-
entry.contentWidth = content?.width ?? contentWidthOf(src);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
frames.push(entry);
|
|
492
|
-
}
|
|
493
|
-
const extRank = (file) => file.endsWith(".tsx") ? 0 : file.endsWith(".jsx") ? 1 : 2;
|
|
494
|
-
frames.sort((a, b) => a.id.localeCompare(b.id) || extRank(a.file) - extRank(b.file) || a.file.localeCompare(b.file));
|
|
495
|
-
const seen = /* @__PURE__ */ new Map();
|
|
496
|
-
const deduped = frames.filter((f) => {
|
|
497
|
-
const prev = seen.get(f.id);
|
|
498
|
-
if (prev) {
|
|
499
|
-
console.error(`[marver] duplicate frame id "${f.id}" (${prev} vs ${f.file}) - keeping ${prev}.`);
|
|
500
|
-
return false;
|
|
501
|
-
}
|
|
502
|
-
seen.set(f.id, f.file);
|
|
503
|
-
return true;
|
|
504
|
-
});
|
|
505
|
-
frames.length = 0;
|
|
506
|
-
frames.push(...deduped);
|
|
507
|
-
inferVariantGroups(frames);
|
|
508
|
-
const sceneCounts = /* @__PURE__ */ new Map();
|
|
509
|
-
for (const f of frames) sceneCounts.set(f.scene, (sceneCounts.get(f.scene) ?? 0) + 1);
|
|
510
|
-
return {
|
|
511
|
-
frames,
|
|
512
|
-
scenes: [...sceneCounts.entries()].map(([name, n]) => ({
|
|
513
|
-
name,
|
|
514
|
-
frames: n
|
|
515
|
-
})).sort((a, b) => a.name.localeCompare(b.name))
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
/** Variant groups. A group = 2+ frames in one DIRECTORY whose basenames
|
|
519
|
-
* are letter-prefixed (`a-terminal`), or frames declaring `meta.of`. Group id = the
|
|
520
|
-
* directory's id prefix (or meta.of); variant key = the letter (or meta.variant).
|
|
521
|
-
* Nested directories scope alternatives inside a busy scene (checkout/payment/a-card).
|
|
522
|
-
* States (empty.tsx, error.tsx) never letter-prefix, so they never misgroup.
|
|
523
|
-
* Mutates entries in place: only frames whose group materializes keep the fields. */
|
|
524
|
-
function inferVariantGroups(frames) {
|
|
525
|
-
const dirOf = (id) => {
|
|
526
|
-
const i = id.lastIndexOf("/");
|
|
527
|
-
return i >= 0 ? id.slice(0, i) : "";
|
|
528
|
-
};
|
|
529
|
-
const candidates = /* @__PURE__ */ new Map();
|
|
530
|
-
for (const f of frames) {
|
|
531
|
-
if (f.kind !== "tsx") {
|
|
532
|
-
delete f.variantGroup;
|
|
533
|
-
delete f.variant;
|
|
534
|
-
continue;
|
|
535
|
-
}
|
|
536
|
-
let group = f.variantGroup;
|
|
537
|
-
let key = f.variant;
|
|
538
|
-
if (!group) {
|
|
539
|
-
const base = f.id.slice(f.id.lastIndexOf("/") + 1);
|
|
540
|
-
const m = /^([a-z])-.+$/.exec(base);
|
|
541
|
-
if (!m) continue;
|
|
542
|
-
group = dirOf(f.id);
|
|
543
|
-
if (!group) continue;
|
|
544
|
-
key = key ?? m[1];
|
|
545
|
-
} else if (!key) {
|
|
546
|
-
const m = /^([a-z])-.+$/.exec(f.id.slice(f.id.lastIndexOf("/") + 1));
|
|
547
|
-
if (!m) {
|
|
548
|
-
console.warn(`[marver] ${f.file} declares of:"${group}" but no variant key (add meta.variant or a letter prefix) - not grouped.`);
|
|
549
|
-
delete f.variantGroup;
|
|
550
|
-
delete f.variant;
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
key = m[1];
|
|
554
|
-
}
|
|
555
|
-
f.variantGroup = group;
|
|
556
|
-
f.variant = key;
|
|
557
|
-
candidates.set(group, [...candidates.get(group) ?? [], f]);
|
|
558
|
-
}
|
|
559
|
-
for (const [group, members] of candidates) {
|
|
560
|
-
if (new Set(members.map((m) => dirOf(m.id))).size > 1) {
|
|
561
|
-
console.warn(`[marver] group "${group}" spans directories - variants must be siblings; not grouped.`);
|
|
562
|
-
for (const m of members) {
|
|
563
|
-
delete m.variantGroup;
|
|
564
|
-
delete m.variant;
|
|
565
|
-
}
|
|
566
|
-
continue;
|
|
567
|
-
}
|
|
568
|
-
const seen = /* @__PURE__ */ new Set();
|
|
569
|
-
const kept = [];
|
|
570
|
-
for (const m of members.sort((a, b) => (a.variant ?? "").localeCompare(b.variant ?? ""))) {
|
|
571
|
-
if (seen.has(m.variant)) {
|
|
572
|
-
console.warn(`[marver] duplicate variant "${m.variant}" in group "${group}" (${m.file}) - not grouped.`);
|
|
573
|
-
delete m.variantGroup;
|
|
574
|
-
delete m.variant;
|
|
575
|
-
continue;
|
|
576
|
-
}
|
|
577
|
-
seen.add(m.variant);
|
|
578
|
-
kept.push(m);
|
|
579
|
-
}
|
|
580
|
-
if (kept.length < 2) for (const m of kept) {
|
|
581
|
-
delete m.variantGroup;
|
|
582
|
-
delete m.variant;
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
for (const f of frames) if (f.variant && !f.variantGroup) delete f.variant;
|
|
586
|
-
}
|
|
587
|
-
/** Write design/manifest.json only when content changed. Returns the manifest either way. */
|
|
588
|
-
function writeManifest(root, manifest) {
|
|
589
|
-
const file = join(root, "design", "manifest.json");
|
|
590
|
-
const next = JSON.stringify(manifest, null, 2) + "\n";
|
|
591
|
-
const prev = existsSync(file) ? readFileSync(file, "utf8") : "";
|
|
592
|
-
if (hash(prev) === hash(next)) return false;
|
|
593
|
-
mkdirSync(join(root, "design"), { recursive: true });
|
|
594
|
-
writeFileSync(file, next);
|
|
595
|
-
return true;
|
|
596
|
-
}
|
|
597
|
-
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
598
|
-
//#endregion
|
|
599
|
-
export { writeManifest as a, detectAgent as c, toFrameId as i, detectHost as l, hash as n, DEFAULTS as o, scanFrames as r, loadConfig as s, affectedFrameIds as t, readJson as u };
|
|
369
|
+
export { readJson as a, detectHost as i, loadConfig as n, detectAgent as r, DEFAULTS as t };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { n as localProfile } from "./profile-
|
|
4
|
-
import { i as toFrameId } from "./manifest-
|
|
1
|
+
import { s as replay } from "./sync-DELGomPk.mjs";
|
|
2
|
+
import { i as readLog, r as listBoards, t as appendEvents } from "./comments-DZyobpxG.mjs";
|
|
3
|
+
import { n as localProfile } from "./profile-DcsJyppw.mjs";
|
|
4
|
+
import { i as toFrameId } from "./manifest-C2tzkNaC.mjs";
|
|
5
5
|
import { workActivity } from "./work-CLrmY-vQ.mjs";
|
|
6
6
|
import { r as deviceId, t as has } from "./ledger-Bu0BjqIe.mjs";
|
|
7
7
|
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, rmSync, statSync, unlinkSync, watch, writeFileSync, writeSync } from "node:fs";
|
|
@@ -938,6 +938,24 @@ const fenceGroup = (pid) => {
|
|
|
938
938
|
if (pid) process.kill(-pid, "SIGKILL");
|
|
939
939
|
} catch {}
|
|
940
940
|
};
|
|
941
|
+
/**
|
|
942
|
+
* What a spawned agent is allowed to inherit.
|
|
943
|
+
*
|
|
944
|
+
* Agents get the environment because they need it - PATH, HOME, the API key
|
|
945
|
+
* their own CLI signs in with. `MARVER_CLI_TOKEN` is the exception: it is the
|
|
946
|
+
* operator's credential for the PUBLISHED canvas, it is accepted as a shell
|
|
947
|
+
* fallback by `comments connect`, and an agent is the one process on this machine
|
|
948
|
+
* that is running somebody else's instructions. Handing it over would let a
|
|
949
|
+
* prompt-injected agent take the published canvas, which is a much larger blast
|
|
950
|
+
* radius than the frames it was asked to edit.
|
|
951
|
+
*
|
|
952
|
+
* Exported so the removal is testable: a spawn option nobody asserts is a spawn
|
|
953
|
+
* option that quietly grows back.
|
|
954
|
+
*/
|
|
955
|
+
function agentEnv(from = process.env) {
|
|
956
|
+
const { MARVER_CLI_TOKEN: _withheld, ...rest } = from;
|
|
957
|
+
return rest;
|
|
958
|
+
}
|
|
941
959
|
const FRAME_FILE = /\.(tsx|jsx|html)$/;
|
|
942
960
|
/** Every frame FILE under design/scenes, as `frame id -> mtimeMs`. Used to see which frames the
|
|
943
961
|
* agent creates or edits mid-job (so the working glow can follow the work, not sit on the frame
|
|
@@ -987,7 +1005,7 @@ function createJam(root, cfg, adapter, log = () => {}, hooks = {}) {
|
|
|
987
1005
|
"ignore"
|
|
988
1006
|
],
|
|
989
1007
|
env: {
|
|
990
|
-
...
|
|
1008
|
+
...agentEnv(),
|
|
991
1009
|
...env,
|
|
992
1010
|
PWD: root
|
|
993
1011
|
}
|