@marver-design/marver 0.8.1 → 0.10.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/README.md +5 -4
- package/dist/{build-BBVQRetk.mjs → build-Dd91jjIg.mjs} +2 -2
- package/dist/cli.mjs +12 -3
- package/dist/{daemon-DkyNOwIt.mjs → daemon-B_8BmmSJ.mjs} +517 -31
- package/dist/{dev-BTAhTie-.mjs → dev-Dv0GlYQq.mjs} +5 -5
- package/dist/{init-DWdhjJD5.mjs → init-D2EQEbV6.mjs} +21 -5
- package/dist/ledger-BgA7nQoH.mjs +103 -0
- package/dist/{manifest-B4zcDGBf.mjs → manifest-DIsp3ldB.mjs} +173 -16
- package/dist/{plugin-BdQEeTLg.mjs → plugin-CgLVYTR-.mjs} +101 -4
- package/dist/shot-BRgPaFCs.mjs +254 -0
- package/dist/shot-DkkwuCZ2.mjs +29 -0
- package/package.json +2 -2
- package/src/client/frame-host/inspect.js +1 -1
- package/src/client/frame-host/main.tsx +8 -1
- package/src/client/shell/Comments.tsx +1 -1
- package/src/client/shell/styles.css +4 -4
- package/templates/AGENTS-embedded.md +1 -1
- package/templates/AGENTS-studio.md +1 -1
- package/templates/instructions/configure.md +6 -1
- package/templates/instructions/jam.md +114 -12
- package/templates/instructions/welcome.md +7 -1
- package/dist/ledger-CbzTJrV2.mjs +0 -64
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as NAME, r as PKG } from "./cli.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
2
|
+
import { l as detectHost, s as loadConfig } from "./manifest-DIsp3ldB.mjs";
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-CgLVYTR-.mjs";
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createLogger, createServer, searchForWorkspaceRoot } from "vite";
|
|
@@ -170,8 +170,8 @@ async function dev(root, portFlag) {
|
|
|
170
170
|
return close();
|
|
171
171
|
});
|
|
172
172
|
}
|
|
173
|
-
if (config.jam
|
|
174
|
-
const { startJam } = await import("./daemon-
|
|
173
|
+
if (config.jam) {
|
|
174
|
+
const { startJam } = await import("./daemon-B_8BmmSJ.mjs");
|
|
175
175
|
const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
|
|
176
176
|
if (jam) {
|
|
177
177
|
const close = server.close.bind(server);
|
|
@@ -180,7 +180,7 @@ async function dev(root, portFlag) {
|
|
|
180
180
|
return close();
|
|
181
181
|
});
|
|
182
182
|
}
|
|
183
|
-
}
|
|
183
|
+
} else if (config.jamOff === "no-agent") console.log(` jam: no agent CLI on PATH (claude, codex, cursor, droid, opencode, grok, or pi) - install one and tag @${NAME} in a comment\n`);
|
|
184
184
|
return server;
|
|
185
185
|
}
|
|
186
186
|
//#endregion
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
|
-
import { a as
|
|
2
|
+
import { a as writeManifest, c as detectAgent, l as detectHost, o as DEFAULTS, r as scanFrames, u as readJson } from "./manifest-DIsp3ldB.mjs";
|
|
3
3
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname, join, relative } from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -84,7 +84,8 @@ function init(root, opts) {
|
|
|
84
84
|
console.warn(` note: design/${rel} exists without a marver marker - left untouched. If you did not author it, delete it and re-run init to restore the managed version.`);
|
|
85
85
|
}
|
|
86
86
|
};
|
|
87
|
-
|
|
87
|
+
const jamAgent = detectAgent();
|
|
88
|
+
write("config.ts", configTemplate(opts.mode, jamAgent));
|
|
88
89
|
if (host.themeCss) {
|
|
89
90
|
const relCss = relative(design, join(root, host.themeCss)).split("\\").join("/");
|
|
90
91
|
write("theme.css", themeWrapper(relCss, host.tailwind === 4));
|
|
@@ -187,6 +188,8 @@ function init(root, opts) {
|
|
|
187
188
|
console.log(`\n commit design/ - only .local/ is ignored`);
|
|
188
189
|
console.log(` uninstall = delete design/, remove the ${NAME} dependency${host.tsconfigSweepsDesign ? ", revert the \"design\" line in tsconfig exclude" : ""}`);
|
|
189
190
|
if (!noApp(host) && !existsSync(join(design, "DESIGN.md"))) console.log(`\n note: design/DESIGN.md (the brand doc) does not exist yet - have your agent create it from the app's tokens (instructions/brand.md, Path A) to reach the idle state.`);
|
|
191
|
+
if (!jamAgent) console.log(`\n note: Live Jam found no agent CLI on PATH (claude, codex, cursor, droid, opencode, grok, or pi) - install one and it arms itself on the next \`${NAME} dev\`.`);
|
|
192
|
+
else if (created.includes("design/config.ts")) console.log(`\n Live Jam is on (${jamAgent}): tag @${NAME} in a canvas comment and your agent does the work, then replies in the thread.`);
|
|
190
193
|
console.log(`\n next: npx ${NAME} dev (or: npx ${NAME} canvas - same thing; canvas on http://localhost:${DEFAULTS.port} by default)\n`);
|
|
191
194
|
if (!noApp(host)) console.log(` then, to your agent: "Read design/AGENTS.md. This is our first session - follow design/instructions/welcome.md."\n`);
|
|
192
195
|
}
|
|
@@ -339,8 +342,10 @@ npx ${NAME} init
|
|
|
339
342
|
|
|
340
343
|
init is idempotent: it detects the real stack, deletes this file, and
|
|
341
344
|
regenerates AGENTS.md against reality. Verify the wiring (instructions/
|
|
342
|
-
configure.md): frames render styled, one app component imports cleanly
|
|
343
|
-
|
|
345
|
+
configure.md): frames render styled, one app component imports cleanly, and
|
|
346
|
+
\`jam.agent\` in design/config.ts names the tool you actually are - Live Jam is
|
|
347
|
+
on by default and init guessed it (instructions/jam.md). DESIGN.md comes next,
|
|
348
|
+
as part of the first draft.
|
|
344
349
|
|
|
345
350
|
|
|
346
351
|
## 6. The path they chose at the fork
|
|
@@ -476,7 +481,17 @@ function firstJsonBrace(src) {
|
|
|
476
481
|
}
|
|
477
482
|
return -1;
|
|
478
483
|
}
|
|
479
|
-
|
|
484
|
+
/** The jam block, written with the agent init detected - or commented out, with the way
|
|
485
|
+
* to arm it, when this machine has no agent CLI at all. */
|
|
486
|
+
const jamBlock = (agent) => agent ? ` // Live Jam - tag @${NAME} in a canvas comment and this agent picks the job up, edits the
|
|
487
|
+
// real frame, and replies in the thread. Detected at init; change the agent if it named
|
|
488
|
+
// the wrong tool, raise concurrency for more frames at once, \`jam: false\` to turn it off.
|
|
489
|
+
jam: { agent: ${JSON.stringify(agent)}, concurrency: 6 },` : ` // Live Jam - tag @${NAME} in a canvas comment and your coding agent picks the job up,
|
|
490
|
+
// edits the real frame, and replies in the thread. No agent CLI was on PATH when init ran;
|
|
491
|
+
// install an agent CLI (claude, codex, cursor, droid, opencode, grok, or pi) and it arms
|
|
492
|
+
// itself, or name one here. \`jam: false\` turns it off.
|
|
493
|
+
// jam: { agent: "claude", concurrency: 6 },`;
|
|
494
|
+
const configTemplate = (mode, jamAgent) => `// ${NAME} config - OPTIONAL. Delete this file and everything still works on defaults.
|
|
480
495
|
// Theme lives in design/theme.css (it imports your app's real stylesheet) - not here.
|
|
481
496
|
// Sharp edges (native Node TS import): erasable syntax only (no enums/namespaces),
|
|
482
497
|
// relative imports need extensions, tsconfig paths are ignored here.
|
|
@@ -494,6 +509,7 @@ export default {
|
|
|
494
509
|
port: ${DEFAULTS.port},
|
|
495
510
|
// Canvas zoom feel: 1 = default, 1.2 = 20% faster, 0.8 = 20% slower.
|
|
496
511
|
// zoomSpeed: 1,
|
|
512
|
+
${jamBlock(jamAgent)}
|
|
497
513
|
// Publishing (\`${NAME} build\` + \`${NAME} serve\`): gate identity + branding footer.
|
|
498
514
|
// name/logo default to the host package.json name and design/logo.svg (then public/).
|
|
499
515
|
// branding is the small "Powered by Marver.design" line under the gate. Marver is
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { homedir, hostname, userInfo } from "node:os";
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __exportAll = (all, no_symbols) => {
|
|
8
|
+
let target = {};
|
|
9
|
+
for (var name in all) __defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true
|
|
12
|
+
});
|
|
13
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/server/jam/device.ts
|
|
18
|
+
/**
|
|
19
|
+
* The device stamp - what makes "device-bound" true for the two files that decide whether
|
|
20
|
+
* a comment may spawn an agent (the ledger and the journal).
|
|
21
|
+
*
|
|
22
|
+
* Both live in design/.local/, which is gitignored and never synced. But gitignore is a
|
|
23
|
+
* convention, not provenance: a repo can force-add its own design/.local/ and hand a clone
|
|
24
|
+
* a pre-authorized ledger plus a pre-baselined journal, and the daemon would run jobs the
|
|
25
|
+
* owner never wrote. Stamping the files with THIS machine means a cloned one matches
|
|
26
|
+
* nothing and is treated as absent.
|
|
27
|
+
*
|
|
28
|
+
* Derived, never stored: marver's whole uninstall story is "delete design/", so it writes
|
|
29
|
+
* no state outside the repo. The trade is that the stamp is a hash of public facts, so it
|
|
30
|
+
* is guessable by someone who already knows the target's hostname and username. It defeats
|
|
31
|
+
* one repo published to everyone, not a stranger who knows your machine. The bigger lever in
|
|
32
|
+
* that scenario is unchanged either way: design/config.ts is imported and executed by
|
|
33
|
+
* `marver dev`, so running a dev server in a repo you do not trust is already running its code.
|
|
34
|
+
*/
|
|
35
|
+
let cached;
|
|
36
|
+
/** A short, stable id for this machine + user. */
|
|
37
|
+
function deviceId() {
|
|
38
|
+
if (cached) return cached;
|
|
39
|
+
let who = "";
|
|
40
|
+
try {
|
|
41
|
+
who = userInfo().username;
|
|
42
|
+
} catch {}
|
|
43
|
+
return cached = createHash("sha256").update([
|
|
44
|
+
hostname(),
|
|
45
|
+
who,
|
|
46
|
+
homedir()
|
|
47
|
+
].join("\0")).digest("hex").slice(0, 16);
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
//#region src/server/jam/ledger.ts
|
|
51
|
+
/**
|
|
52
|
+
* The device-bound authorization ledger - the whole trust boundary.
|
|
53
|
+
*
|
|
54
|
+
* When the dev POST accepts an owner-gated write, it records that event's id here. The
|
|
55
|
+
* daemon's owner-trigger check is `has(root, id)`, never a synced field: sync copies
|
|
56
|
+
* `origin` byte-for-byte, so a remote comment can spoof `origin:'local'` (proven RCE),
|
|
57
|
+
* but it can never appear in a file that is written only on THIS machine by the gated
|
|
58
|
+
* POST and never synced. Synced-in events are never in the ledger, so they never trigger.
|
|
59
|
+
*
|
|
60
|
+
* One `<device>\t<board>\t<id>` per line, append-only, gitignored, never synced (design/.local/
|
|
61
|
+
* is watch-ignored and sync-excluded). Agent-written events are never recorded (they are
|
|
62
|
+
* daemon-authored, not owner input, so they cannot self-authorize a next job).
|
|
63
|
+
*
|
|
64
|
+
* The key is (device, board, id), never id alone:
|
|
65
|
+
* - board, because event ids are client UUIDs that sync copies verbatim, so a remote
|
|
66
|
+
* collaborator could reuse an owner's ledgered id in a NEW malicious event. Binding to the
|
|
67
|
+
* board it was gate-written on defeats that - the forged copy lands on some board the ledger
|
|
68
|
+
* never authorized for that id, so it never triggers.
|
|
69
|
+
* - device, because gitignore is a convention, not provenance: a repo can force-add its own
|
|
70
|
+
* design/.local/ and hand a clone a ledger full of pre-authorized ids. Lines stamped with
|
|
71
|
+
* another machine match nothing here (device.ts).
|
|
72
|
+
*/
|
|
73
|
+
var ledger_exports = /* @__PURE__ */ __exportAll({
|
|
74
|
+
has: () => has,
|
|
75
|
+
record: () => record
|
|
76
|
+
});
|
|
77
|
+
const ledgerFile = (root) => join(root, "design", ".local", "jam-ledger");
|
|
78
|
+
const line = (board, id) => `${deviceId()}\t${board}\t${id}`;
|
|
79
|
+
/** Was this (board, id) authorized on this device by the gated dev POST? */
|
|
80
|
+
function has(root, board, id) {
|
|
81
|
+
if (!board || !id) return false;
|
|
82
|
+
const file = ledgerFile(root);
|
|
83
|
+
if (!existsSync(file)) return false;
|
|
84
|
+
const want = line(board, id);
|
|
85
|
+
for (const l of readFileSync(file, "utf8").split("\n")) if (l === want) return true;
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
/** Authorize a (board, id). fsync'd (a 200-acked, ledgered write must survive a crash) and
|
|
89
|
+
* 0600 (owner-only). Idempotent enough: a duplicate line is harmless, `has` matches either. */
|
|
90
|
+
function record(root, board, id) {
|
|
91
|
+
if (!board || !id) return;
|
|
92
|
+
const file = ledgerFile(root);
|
|
93
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
94
|
+
const fd = openSync(file, "a", 384);
|
|
95
|
+
try {
|
|
96
|
+
writeSync(fd, line(board, id) + "\n");
|
|
97
|
+
fsyncSync(fd);
|
|
98
|
+
} finally {
|
|
99
|
+
closeSync(fd);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
//#endregion
|
|
103
|
+
export { ledger_exports as n, deviceId as r, has as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { r as PKG, t as CONTENT_WIDTH } from "./cli.mjs";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { join, relative, sep } from "node:path";
|
|
2
|
+
import { accessSync, constants, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { delimiter, isAbsolute, join, relative, sep } from "node:path";
|
|
4
4
|
import { pathToFileURL } from "node:url";
|
|
5
5
|
import { createHash } from "node:crypto";
|
|
6
6
|
//#region src/server/detect.ts
|
|
@@ -125,6 +125,99 @@ function firstExisting(root, candidates) {
|
|
|
125
125
|
return null;
|
|
126
126
|
}
|
|
127
127
|
//#endregion
|
|
128
|
+
//#region src/server/jam/agent.ts
|
|
129
|
+
/**
|
|
130
|
+
* Which coding agent drives this repo.
|
|
131
|
+
*
|
|
132
|
+
* Live Jam is ON by default, so this is the answer to "on by default with WHAT" - asked
|
|
133
|
+
* once by `init` (which writes the answer into design/config.ts, where it is visible and
|
|
134
|
+
* editable) and again at every dev boot, so a workspace that predates the block - or a
|
|
135
|
+
* human who switched tools - still jams without editing anything.
|
|
136
|
+
*
|
|
137
|
+
* The tool RUNNING us wins: `init` is usually run BY the agent, so its env markers are
|
|
138
|
+
* evidence rather than a guess. Whatever wins must still be on PATH - the daemon has to
|
|
139
|
+
* spawn it.
|
|
140
|
+
*/
|
|
141
|
+
/** Ordered by preference when a machine has several installed and none is running us. */
|
|
142
|
+
const AGENTS = [
|
|
143
|
+
"claude",
|
|
144
|
+
"codex",
|
|
145
|
+
"cursor",
|
|
146
|
+
"droid",
|
|
147
|
+
"opencode",
|
|
148
|
+
"grok",
|
|
149
|
+
"pi"
|
|
150
|
+
];
|
|
151
|
+
/** Env vars each CLI sets in the processes it spawns. Deliberately NOT CODEX_HOME or
|
|
152
|
+
* ANTHROPIC_API_KEY: those are configuration a human exports by hand, not evidence that
|
|
153
|
+
* the tool is running right now. (opencode's `AGENT=1` and pi's `AI_AGENT` are skipped
|
|
154
|
+
* for the same reason in reverse - too generic to prove WHICH tool is running.)
|
|
155
|
+
* droid and grok set no marker at all in the shells they spawn (verified against the
|
|
156
|
+
* grok-build source and droid 0.200.0), so they are found by PATH order only. */
|
|
157
|
+
const MARKERS = {
|
|
158
|
+
claude: ["CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"],
|
|
159
|
+
codex: ["CODEX_SANDBOX", "CODEX_THREAD_ID"],
|
|
160
|
+
cursor: ["CURSOR_AGENT"],
|
|
161
|
+
droid: [],
|
|
162
|
+
opencode: ["OPENCODE", "OPENCODE_PID"],
|
|
163
|
+
grok: [],
|
|
164
|
+
pi: ["PI_CODING_AGENT", "PI_SESSION_ID"]
|
|
165
|
+
};
|
|
166
|
+
/** The executable each agent name maps to. Only cursor differs: its CLI installs BOTH
|
|
167
|
+
* `cursor-agent` and a bare `agent` - and grok's installer symlinks `agent` too, so the
|
|
168
|
+
* short name is a coin flip on a machine with both. The long name is unambiguous. */
|
|
169
|
+
const AGENT_BIN = {
|
|
170
|
+
claude: "claude",
|
|
171
|
+
codex: "codex",
|
|
172
|
+
cursor: "cursor-agent",
|
|
173
|
+
droid: "droid",
|
|
174
|
+
opencode: "opencode",
|
|
175
|
+
grok: "grok",
|
|
176
|
+
pi: "pi"
|
|
177
|
+
};
|
|
178
|
+
/** The valid `jam.agent` values as an English list - one string, so every surface
|
|
179
|
+
* (config warning, init note, dev boot line) names the same set and none goes stale. */
|
|
180
|
+
const AGENT_NAMES = `${AGENTS.slice(0, -1).map((a) => `"${a}"`).join(", ")}, or "${AGENTS[AGENTS.length - 1]}"`;
|
|
181
|
+
/** Is `cmd` an executable FILE on PATH? A direct stat per PATH entry - no spawn, no shell,
|
|
182
|
+
* so this is safe to call on every dev boot. Two deliberate narrowings, both in service of
|
|
183
|
+
* "found means spawnable", because arming an agent that cannot run is worse than staying off:
|
|
184
|
+
*
|
|
185
|
+
* - `isFile`, because a directory carries the execute bit too (it means "traversable"), so an
|
|
186
|
+
* access check alone would call a folder named `claude` an agent.
|
|
187
|
+
* - the bare name only, no PATHEXT: the daemon spawns without a shell, and Node cannot run a
|
|
188
|
+
* Windows `.cmd`/`.bat` shim that way. Finding one would arm a job that fails on every run.
|
|
189
|
+
*
|
|
190
|
+
* Only ABSOLUTE PATH entries count. An empty entry means the current directory on POSIX, and
|
|
191
|
+
* a relative one (`.`, `bin`) resolves against it too - and the current directory is the repo
|
|
192
|
+
* that was just opened, so a `claude` binary shipped inside it is precisely what must never
|
|
193
|
+
* be found and spawned. */
|
|
194
|
+
function onPath(cmd, env = process.env) {
|
|
195
|
+
for (const dir of (env.PATH ?? "").split(delimiter)) {
|
|
196
|
+
if (!isAbsolute(dir)) continue;
|
|
197
|
+
try {
|
|
198
|
+
const file = join(dir, cmd);
|
|
199
|
+
if (statSync(file).isFile()) {
|
|
200
|
+
accessSync(file, constants.X_OK);
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
} catch {}
|
|
204
|
+
}
|
|
205
|
+
return false;
|
|
206
|
+
}
|
|
207
|
+
/** The agent to jam with, or undefined when this machine has none installed.
|
|
208
|
+
*
|
|
209
|
+
* One knowingly-imperfect case: agents nest, and env is inherited, so a codex run started
|
|
210
|
+
* FROM Claude Code carries both marker families and this picks claude. Env has no depth to
|
|
211
|
+
* read, and walking the process tree to find the nearest agent ancestor costs more than the
|
|
212
|
+
* case is worth - so the answer is made visible instead of clever: `init` prints the agent
|
|
213
|
+
* it chose and writes it into design/config.ts, and instructions/jam.md has the agent confirm
|
|
214
|
+
* that line names the tool it actually is. One word to correct, once per repo. */
|
|
215
|
+
function detectAgent(env = process.env) {
|
|
216
|
+
const running = AGENTS.find((a) => MARKERS[a].some((k) => env[k]));
|
|
217
|
+
if (running && onPath(AGENT_BIN[running], env)) return running;
|
|
218
|
+
return AGENTS.find((a) => onPath(AGENT_BIN[a], env));
|
|
219
|
+
}
|
|
220
|
+
//#endregion
|
|
128
221
|
//#region src/server/config.ts
|
|
129
222
|
const DEFAULTS = {
|
|
130
223
|
mode: "studio",
|
|
@@ -155,7 +248,10 @@ const DEFAULTS = {
|
|
|
155
248
|
/** Load design/config.ts via native TS import (Node >= 22.18). Missing or broken fields fall back to defaults. */
|
|
156
249
|
async function loadConfig(root) {
|
|
157
250
|
const file = join(root, "design", "config.ts");
|
|
158
|
-
if (!existsSync(file)) return {
|
|
251
|
+
if (!existsSync(file)) return {
|
|
252
|
+
...DEFAULTS,
|
|
253
|
+
...jamFields(void 0)
|
|
254
|
+
};
|
|
159
255
|
try {
|
|
160
256
|
const user = (await import(`${pathToFileURL(file).href}?t=${Date.now()}`)).default ?? {};
|
|
161
257
|
return {
|
|
@@ -170,11 +266,14 @@ async function loadConfig(root) {
|
|
|
170
266
|
name: typeof user.share?.name === "string" ? user.share.name : void 0,
|
|
171
267
|
logo: typeof user.share?.logo === "string" ? user.share.logo : void 0
|
|
172
268
|
},
|
|
173
|
-
|
|
269
|
+
...jamFields(user.jam)
|
|
174
270
|
};
|
|
175
271
|
} catch (err) {
|
|
176
|
-
console.error(`[marver] design/config.ts failed to load, using defaults:\n ${err.message}`);
|
|
177
|
-
return {
|
|
272
|
+
console.error(`[marver] design/config.ts failed to load, using defaults and leaving Live Jam OFF:\n ${err.message}`);
|
|
273
|
+
return {
|
|
274
|
+
...DEFAULTS,
|
|
275
|
+
jamOff: "unreadable"
|
|
276
|
+
};
|
|
178
277
|
}
|
|
179
278
|
}
|
|
180
279
|
const validDim = (n) => typeof n === "number" && Number.isFinite(n) && n >= 1 && n <= 2e4;
|
|
@@ -184,20 +283,78 @@ function validPort(n) {
|
|
|
184
283
|
function validZoom(n) {
|
|
185
284
|
return typeof n === "number" && Number.isFinite(n) && n >= .1 && n <= 10 ? n : null;
|
|
186
285
|
}
|
|
187
|
-
/**
|
|
188
|
-
*
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
286
|
+
/** Six frames at once is what a jam actually feels like - at 3, half of a multi-frame
|
|
287
|
+
* ask sat waiting on the other half while the human watched. */
|
|
288
|
+
const DEFAULT_CONCURRENCY = 6;
|
|
289
|
+
/** A config value, printable in a warning. Never throws - a formatter that can crash inside an
|
|
290
|
+
* error path (JSON.stringify does, on a BigInt) turns a clear message into a mystery. */
|
|
291
|
+
const show = (v) => {
|
|
292
|
+
try {
|
|
293
|
+
return JSON.stringify(v) ?? String(v);
|
|
294
|
+
} catch {
|
|
295
|
+
return String(v);
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
/** A `{...}` written by hand, not a Date/Map/class instance that merely types as "object". */
|
|
299
|
+
const plainObject = (v) => {
|
|
300
|
+
if (!v || typeof v !== "object") return false;
|
|
301
|
+
const proto = Object.getPrototypeOf(v);
|
|
302
|
+
return proto === Object.prototype || proto === null;
|
|
303
|
+
};
|
|
304
|
+
/** The agent to arm with - or, when the human NAMED one we cannot use, why not (said once,
|
|
305
|
+
* here, because a bad `jam.agent` is a config-file error like any other).
|
|
306
|
+
*
|
|
307
|
+
* A named agent is never quietly swapped for another: running a different tool than the one
|
|
308
|
+
* asked for is worse than not running. Nor is one armed that cannot be spawned - that would
|
|
309
|
+
* claim every @marver mention, fail it, and never retry. */
|
|
310
|
+
function resolveAgent(named) {
|
|
311
|
+
if (named === void 0) return detectAgent() ?? "no-agent";
|
|
312
|
+
if (typeof named !== "string" || !Object.hasOwn(AGENT_BIN, named)) {
|
|
313
|
+
console.warn(`[marver] design/config.ts: jam.agent ${show(named)} is not an agent marver can spawn - Live Jam is off. Use ${AGENT_NAMES}.`);
|
|
314
|
+
return "bad-agent";
|
|
315
|
+
}
|
|
316
|
+
const agent = named;
|
|
317
|
+
if (!onPath(AGENT_BIN[agent])) {
|
|
318
|
+
console.warn(`[marver] design/config.ts names jam.agent "${agent}", but \`${AGENT_BIN[agent]}\` is not on PATH - Live Jam is off until it is installed.`);
|
|
319
|
+
return "bad-agent";
|
|
320
|
+
}
|
|
321
|
+
return agent;
|
|
322
|
+
}
|
|
323
|
+
/** Resolve the jam block. Live Jam is ON by default: an absent or partial `jam` resolves
|
|
324
|
+
* to whatever agent CLI this machine has, so a fresh workspace jams with nothing
|
|
325
|
+
* configured and an old one needs no re-init. `jam: false` is the off switch. */
|
|
326
|
+
function resolveJam(v) {
|
|
327
|
+
if (v === false) return "opted-out";
|
|
328
|
+
const shape = v === void 0 || v === true ? {} : typeof v === "string" ? { agent: v } : plainObject(v) ? v : null;
|
|
329
|
+
if (!shape) {
|
|
330
|
+
console.warn(`[marver] design/config.ts: jam must be false, an agent name, or an options object - got ${show(v)}. Live Jam is off.`);
|
|
331
|
+
return "bad-agent";
|
|
332
|
+
}
|
|
333
|
+
const j = shape;
|
|
334
|
+
const agent = resolveAgent(j.agent);
|
|
335
|
+
if (agent === "no-agent" || agent === "bad-agent") return agent;
|
|
336
|
+
const c = j.concurrency;
|
|
194
337
|
return {
|
|
195
|
-
agent
|
|
196
|
-
concurrency,
|
|
338
|
+
agent,
|
|
339
|
+
concurrency: typeof c === "number" && Number.isInteger(c) && c >= 1 && c <= 16 ? c : DEFAULT_CONCURRENCY,
|
|
197
340
|
subagents: j.subagents !== false,
|
|
198
341
|
proactive: j.proactive === true
|
|
199
342
|
};
|
|
200
343
|
}
|
|
344
|
+
/** Exactly one of `jam` (armed) or `jamOff` (why not) - so the dev server can speak up about
|
|
345
|
+
* the one off-state nothing has reported yet, and stay quiet about the rest. BOTH keys are
|
|
346
|
+
* always written: these spread over the user's own `jam`, and a raw `jam: false` reaching the
|
|
347
|
+
* server as config would arm the daemon against a truthy object check. */
|
|
348
|
+
function jamFields(v) {
|
|
349
|
+
const r = resolveJam(v);
|
|
350
|
+
return typeof r === "string" ? {
|
|
351
|
+
jam: void 0,
|
|
352
|
+
jamOff: r
|
|
353
|
+
} : {
|
|
354
|
+
jam: r,
|
|
355
|
+
jamOff: void 0
|
|
356
|
+
};
|
|
357
|
+
}
|
|
201
358
|
function validViewports(v) {
|
|
202
359
|
if (!v || typeof v !== "object") return null;
|
|
203
360
|
const out = {};
|
|
@@ -439,4 +596,4 @@ function writeManifest(root, manifest) {
|
|
|
439
596
|
}
|
|
440
597
|
const hash = (s) => createHash("sha256").update(s).digest("hex");
|
|
441
598
|
//#endregion
|
|
442
|
-
export {
|
|
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 };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as ROUTE, n as NAME, r as PKG } from "./cli.mjs";
|
|
2
2
|
import { n as localProfile, t as isConnected } from "./profile-BkiWglVE.mjs";
|
|
3
|
-
import {
|
|
3
|
+
import { a as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-DIsp3ldB.mjs";
|
|
4
4
|
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
@@ -82,7 +82,7 @@ function contained(target, base) {
|
|
|
82
82
|
return false;
|
|
83
83
|
}
|
|
84
84
|
}
|
|
85
|
-
function apiMiddleware(root) {
|
|
85
|
+
function apiMiddleware(root, opts = {}) {
|
|
86
86
|
const boardsDir = join(root, "design", "boards");
|
|
87
87
|
const boardPath = (name) => {
|
|
88
88
|
if (!BOARD_NAME.test(name)) return null;
|
|
@@ -167,6 +167,30 @@ function apiMiddleware(root) {
|
|
|
167
167
|
return json(res, 200, { sha256: hash(next2) });
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
|
+
if (path === "shot" && req.method === "GET") {
|
|
171
|
+
const { readDevInfo } = await import("./work-CLrmY-vQ.mjs");
|
|
172
|
+
const token = readDevInfo(root)?.token;
|
|
173
|
+
if (!ownerGated(req) && !(token && req.headers["x-mv-work"] === token)) return json(res, 403, { error: "forbidden" });
|
|
174
|
+
const frameId = url.searchParams.get("frame") ?? "";
|
|
175
|
+
const theme = url.searchParams.get("theme") ?? "light";
|
|
176
|
+
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
177
|
+
const { shootFrame } = await import("./shot-BRgPaFCs.mjs");
|
|
178
|
+
const r = await shootFrame({
|
|
179
|
+
root,
|
|
180
|
+
viewports: opts.viewports ?? {},
|
|
181
|
+
frameId,
|
|
182
|
+
theme,
|
|
183
|
+
origin
|
|
184
|
+
});
|
|
185
|
+
if (!r.ok) return json(res, r.error.startsWith("unknown frame") ? 404 : r.error === "invalid theme" ? 400 : 503, { error: r.error });
|
|
186
|
+
return json(res, 200, {
|
|
187
|
+
path: r.path,
|
|
188
|
+
frame: frameId,
|
|
189
|
+
theme,
|
|
190
|
+
width: r.width,
|
|
191
|
+
height: r.height
|
|
192
|
+
});
|
|
193
|
+
}
|
|
170
194
|
if (path === "me" && req.method === "GET") return json(res, 200, {
|
|
171
195
|
user: localProfile(root),
|
|
172
196
|
role: "owner",
|
|
@@ -269,7 +293,7 @@ function apiMiddleware(root) {
|
|
|
269
293
|
};
|
|
270
294
|
});
|
|
271
295
|
const fresh = appendEvents(dir, cm[1], stamped);
|
|
272
|
-
const { record } = await import("./ledger-
|
|
296
|
+
const { record } = await import("./ledger-BgA7nQoH.mjs").then((n) => n.n);
|
|
273
297
|
for (const ev of fresh) if (ev.type === "create" || ev.type === "reply") record(root, cm[1], ev.id);
|
|
274
298
|
backgroundPush(root);
|
|
275
299
|
return json(res, 200, { accepted: fresh.length });
|
|
@@ -565,7 +589,15 @@ function marverPlugin(ctx) {
|
|
|
565
589
|
}));
|
|
566
590
|
});
|
|
567
591
|
});
|
|
568
|
-
|
|
592
|
+
const listeningOrigin = () => {
|
|
593
|
+
const a = server.httpServer?.address();
|
|
594
|
+
if (!a || typeof a === "string") return null;
|
|
595
|
+
return `http://${a.family === "IPv6" ? a.address === "::" ? "[::1]" : `[${a.address}]` : a.address === "0.0.0.0" ? "127.0.0.1" : a.address}:${a.port}`;
|
|
596
|
+
};
|
|
597
|
+
server.middlewares.use(apiMiddleware(root, {
|
|
598
|
+
viewports: config.viewports,
|
|
599
|
+
origin: listeningOrigin
|
|
600
|
+
}));
|
|
569
601
|
server.middlewares.use(routesMiddleware(server, clientDir));
|
|
570
602
|
devServer = server;
|
|
571
603
|
const regen = debounce(() => {
|
|
@@ -630,6 +662,71 @@ function marverPlugin(ctx) {
|
|
|
630
662
|
});
|
|
631
663
|
server.httpServer?.once("close", () => watcher.close());
|
|
632
664
|
} catch {}
|
|
665
|
+
try {
|
|
666
|
+
const inbox = join(root, "design", ".local", "shots");
|
|
667
|
+
mkdirSync(inbox, { recursive: true });
|
|
668
|
+
const origin = listeningOrigin;
|
|
669
|
+
const inFlight = /* @__PURE__ */ new Set();
|
|
670
|
+
const processRequest = (file) => {
|
|
671
|
+
if (!file.endsWith(".request.json") || inFlight.has(file)) return;
|
|
672
|
+
const reqPath = join(inbox, file);
|
|
673
|
+
if (!existsSync(reqPath)) return;
|
|
674
|
+
inFlight.add(file);
|
|
675
|
+
setTimeout(async () => {
|
|
676
|
+
try {
|
|
677
|
+
let spec;
|
|
678
|
+
try {
|
|
679
|
+
spec = JSON.parse(readFileSync(reqPath, "utf8"));
|
|
680
|
+
} catch {
|
|
681
|
+
return;
|
|
682
|
+
}
|
|
683
|
+
const base = file.replace(/\.request\.json$/, "");
|
|
684
|
+
const resultPath = join(inbox, `${base}.result.json`);
|
|
685
|
+
const frameId = typeof spec.frame === "string" ? spec.frame : "";
|
|
686
|
+
const theme = typeof spec.theme === "string" ? spec.theme : "light";
|
|
687
|
+
const o = origin();
|
|
688
|
+
const write = (r) => {
|
|
689
|
+
try {
|
|
690
|
+
writeFileSync(resultPath, JSON.stringify(r, null, 2));
|
|
691
|
+
} catch {}
|
|
692
|
+
};
|
|
693
|
+
if (!o) {
|
|
694
|
+
write({
|
|
695
|
+
ok: false,
|
|
696
|
+
error: "dev server has no address yet - retry"
|
|
697
|
+
});
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const { shootFrame } = await import("./shot-BRgPaFCs.mjs");
|
|
701
|
+
write(await shootFrame({
|
|
702
|
+
root,
|
|
703
|
+
viewports: config.viewports,
|
|
704
|
+
frameId,
|
|
705
|
+
theme,
|
|
706
|
+
origin: o
|
|
707
|
+
}));
|
|
708
|
+
try {
|
|
709
|
+
rmSync(reqPath, { force: true });
|
|
710
|
+
} catch {}
|
|
711
|
+
} finally {
|
|
712
|
+
inFlight.delete(file);
|
|
713
|
+
}
|
|
714
|
+
}, 60);
|
|
715
|
+
};
|
|
716
|
+
const inboxWatcher = watch(inbox, { persistent: false }, (_e, file) => {
|
|
717
|
+
if (file) processRequest(file);
|
|
718
|
+
});
|
|
719
|
+
const sweep = setInterval(() => {
|
|
720
|
+
try {
|
|
721
|
+
for (const f of readdirSync(inbox)) processRequest(f);
|
|
722
|
+
} catch {}
|
|
723
|
+
}, 1e3);
|
|
724
|
+
sweep.unref?.();
|
|
725
|
+
server.httpServer?.once("close", () => {
|
|
726
|
+
inboxWatcher.close();
|
|
727
|
+
clearInterval(sweep);
|
|
728
|
+
});
|
|
729
|
+
} catch {}
|
|
633
730
|
}
|
|
634
731
|
};
|
|
635
732
|
}
|