@marver-design/marver 0.10.0 → 0.10.2
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 +56 -0
- package/README.md +2 -2
- package/dist/{build-Dd91jjIg.mjs → build-zUf_kwKp.mjs} +1 -1
- package/dist/cli.mjs +3 -3
- package/dist/{daemon-B_8BmmSJ.mjs → daemon-BpKBJZee.mjs} +1 -1
- package/dist/{dev-Dv0GlYQq.mjs → dev-D2PG6igI.mjs} +12 -2
- package/dist/{init-D2EQEbV6.mjs → init-DwV0_d9G.mjs} +7 -28
- package/dist/{ledger-BgA7nQoH.mjs → ledger-Bu0BjqIe.mjs} +1 -12
- package/dist/managed-HwHVNI3h.mjs +94 -0
- package/dist/{plugin-CgLVYTR-.mjs → plugin-C1sDewZ3.mjs} +107 -6
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/dist/{shot-BRgPaFCs.mjs → shot-D7I0MCLu.mjs} +139 -12
- package/package.json +1 -1
- package/src/client/content/img-lod.ts +6 -0
- package/src/client/frame-host/bridge.js +3 -1
- package/src/client/frame-host/main.tsx +4 -1
- package/src/client/shell/App.tsx +276 -31
- package/src/client/shell/Play.tsx +5 -4
- package/src/client/shell/canvas/FrameNode.tsx +45 -6
- package/src/client/shell/canvas/ready-watch.ts +28 -0
- package/src/client/shell/store.ts +83 -8
- package/src/client/shell/styles.css +33 -0
- package/templates/AGENTS-embedded.md +11 -4
- package/templates/AGENTS-studio.md +11 -4
- package/templates/instructions/boards.md +3 -1
- package/templates/instructions/jam.md +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,62 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@marver-design/marver`. Format follows [Keep a Changelog](https://keepachangelog.com); versions follow semver.
|
|
4
4
|
|
|
5
|
+
## 0.10.2 - 2026-08-24
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`marver shot` renders content frames at their real size.** The verify-loop screenshot
|
|
10
|
+
(and the file-drop `design/.local/shots/*.request.json` transport) used to capture every
|
|
11
|
+
frame at the mobile viewport - a 780x1688 crop - so a `Doc layout="wide"` frame came back
|
|
12
|
+
squeezed and anything past ~1688px tall was invisible to the verifying agent. A shot now
|
|
13
|
+
sizes a content frame the way the canvas does: its `contentWidth` (760 document / 1280 wide,
|
|
14
|
+
or a declared `meta.viewport`'s width) at full measured height. Long frames render in full;
|
|
15
|
+
past a capture-safe height the shot drops to 1x pixel density and, if still enormous, reports
|
|
16
|
+
`truncated` with a note rather than producing a gigapixel PNG. The `/api/shot` and file-drop
|
|
17
|
+
results now include `scale` and, when clipped, `truncated`/`note`.
|
|
18
|
+
- **A slow dev server no longer fails a healthy frame.** A frame that hadn't reported ready
|
|
19
|
+
within 10s was marked `frame failed` / `frame never reported ready (10s)`, hidden behind a red
|
|
20
|
+
card that only a per-frame click could clear - but the frame was usually fine, just waiting on
|
|
21
|
+
a slow module response (Vite re-optimizing dependencies, or the box saturated by parallel Live
|
|
22
|
+
Jam agents). A genuine failure already reports itself immediately, so silence past the deadline
|
|
23
|
+
now auto-renavigates the frame once on a fresh revision; if it's still loading it stays visible
|
|
24
|
+
behind a quiet "still loading" pill (with its own reload) instead of a failure. A late ready
|
|
25
|
+
clears it, and a stale ready from the superseded document is dropped by generation so it can't
|
|
26
|
+
mark a reloading frame ready.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- **`marver dev` warns when your instructions predate the installed package.** Upgrading the
|
|
31
|
+
package doesn't refresh the managed files in `design/` - so a workspace scaffolded on an
|
|
32
|
+
older version could run the new daemon against an agent contract that predated it (for
|
|
33
|
+
example, Live Jam armed with no `jam.md` on disk). Dev now prints one line at boot when a
|
|
34
|
+
managed instruction file is behind the installed version - `instructions predate marver
|
|
35
|
+
<version> (jam.md, ...) - run: npx marver init` - comparing the on-disk `marver:managed`
|
|
36
|
+
hashes against what this version ships. Files you edited or detached are never flagged.
|
|
37
|
+
|
|
38
|
+
## 0.10.1 - 2026-08-21
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- **Point an agent at anything on the canvas by its location.** Right-click a board, scene,
|
|
43
|
+
or frame in the sidebar for a Copy path action (the same signpost icon as the floating
|
|
44
|
+
toolbar): a board copies `board: <b>`, a scene `board: <b> - scene: <s> (design/scenes/<s>/)`,
|
|
45
|
+
a frame `board: <b> - frame: <id> (<file>)`. Laser copy now leads with `[board > scene]`
|
|
46
|
+
before the file and selector, and the floating toolbar plus Shift+P copy that same frame
|
|
47
|
+
address (board, frame, file) - all routed through one helper so the three never drift. On a
|
|
48
|
+
real copy the toolbar signpost flashes to a check. The AGENTS templates and the Live Jam
|
|
49
|
+
guide explain how to read every pasted address.
|
|
50
|
+
- **Manage boards from the sidebar.** Rename a board inline from its right-click menu; the
|
|
51
|
+
dev server performs an atomic no-clobber move and refuses a board that has a live connection
|
|
52
|
+
or a comment log (comment and sync state is board-keyed and cannot follow a file rename).
|
|
53
|
+
Reorder boards by dragging - a pointer-driven gesture (not native drag-and-drop) that holds
|
|
54
|
+
a grabbing cursor for the whole drag and marks the landing with a single brand-blue seam per
|
|
55
|
+
gap, overlaid so the list never shifts. A plain click still switches boards.
|
|
56
|
+
|
|
57
|
+
### Removed
|
|
58
|
+
|
|
59
|
+
- The Duplicate-frame button on the floating selection toolbar.
|
|
60
|
+
|
|
5
61
|
## 0.10.0 - 2026-08-21
|
|
6
62
|
|
|
7
63
|
### Added
|
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Frames appear on the canvas the moment the files land. That's the loop.
|
|
|
32
32
|
|
|
33
33
|
## The canvas
|
|
34
34
|
|
|
35
|
-
- **Frames, scenes, boards.** Frames are screens, scenes group them (`design/scenes/<scene>/<frame>.tsx`), boards arrange them. Agents write `design/boards/<name>.json` (a frame list is enough); switch boards at the top of the sidebar. `all-scenes` is auto-managed.
|
|
35
|
+
- **Frames, scenes, boards.** Frames are screens, scenes group them (`design/scenes/<scene>/<frame>.tsx`), boards arrange them. Agents write `design/boards/<name>.json` (a frame list is enough); switch boards at the top of the sidebar. `all-scenes` is auto-managed. Right-click any board, scene, or frame in the sidebar to copy its path - the exact string to paste to your agent - and rename or drag-reorder boards from there too.
|
|
36
36
|
- **Devices view.** Hotkeys `1`-`5` (or the Devices menu) size every frame to mobile / tablet / laptop / monitor / tv to sweep your breakpoints; `0` restores your own layout exactly. Widths live in `design/config.ts`.
|
|
37
37
|
- **Prototype links.** `data-goto="scene/frame"` on any element links frames into a walkable prototype - across boards, too.
|
|
38
38
|
- **Play mode.** Press `p`: the board becomes a full-screen, clickable walkthrough. `data-goto` links navigate, arrow keys step between frames, `[` / `]` cycle variants, `Escape` exits. Publish it and you have a shareable prototype.
|
|
@@ -76,7 +76,7 @@ The same glow, driven from the terminal. When your agent takes a request, it cre
|
|
|
76
76
|
|
|
77
77
|
**Board & chrome** - `t` tidy · `d` toggle light/dark for the board · `⌘\` (ctrl+\) collapse/open sidebar.
|
|
78
78
|
|
|
79
|
-
**Selection** - click selects · shift+click (canvas or sidebar) builds a multi-selection · `⌘A` selects every frame on the board · `⇧P` copies the selected frames' file
|
|
79
|
+
**Selection** - click selects · shift+click (canvas or sidebar) builds a multi-selection · `⌘A` selects every frame on the board · `⇧P` copies the selected frames' paths (board, frame, and file) · double-click enters interact mode (`esc` or click outside leaves) · drag the title bar to move, edges to resize (widths snap to devices).
|
|
80
80
|
|
|
81
81
|
**Modes** - `c` comment mode · `l` laser mode · `⇧C` hide/show comment pins · `⇧L` laser comment (spotlight a thread's element) · `p` play mode · `h` hide all chrome.
|
|
82
82
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { i as ROUTE, n as NAME } from "./cli.mjs";
|
|
2
2
|
import { l as detectHost, r as scanFrames, s as loadConfig } from "./manifest-DIsp3ldB.mjs";
|
|
3
|
-
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-C1sDewZ3.mjs";
|
|
4
4
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { basename, dirname, join, sep } from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
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-DwV0_d9G.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-D2PG6igI.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-zUf_kwKp.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);
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { r as deviceId, t as has } from "./ledger-BgA7nQoH.mjs";
|
|
2
1
|
import { n as replay } from "./events-BMtBvvgU.mjs";
|
|
3
2
|
import { appendEvents, listBoards, readLog } from "./comments-BZBKhKRO.mjs";
|
|
4
3
|
import { n as localProfile } from "./profile-BkiWglVE.mjs";
|
|
5
4
|
import { i as toFrameId } from "./manifest-DIsp3ldB.mjs";
|
|
6
5
|
import { workActivity } from "./work-CLrmY-vQ.mjs";
|
|
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";
|
|
8
8
|
import { join } from "node:path";
|
|
9
9
|
import { randomBytes, randomUUID } from "node:crypto";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { n as NAME, r as PKG } from "./cli.mjs";
|
|
2
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-
|
|
3
|
+
import { n as tailwind3Css, r as tailwind4Plugin, t as marverPlugin } from "./plugin-C1sDewZ3.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";
|
|
@@ -141,6 +141,16 @@ async function dev(root, portFlag) {
|
|
|
141
141
|
const port = typeof addr === "object" && addr ? addr.port : picked.port;
|
|
142
142
|
if (picked.fellBack) console.log(`\n port ${desiredPort} is in use - serving "${projectName}" on ${port} instead`);
|
|
143
143
|
console.log(`\n ${NAME} · ${projectName} → http://localhost:${port}/\n`);
|
|
144
|
+
try {
|
|
145
|
+
const { staleManagedInstructions } = await import("./managed-HwHVNI3h.mjs");
|
|
146
|
+
const { installedVersion } = await import("./plugin-C1sDewZ3.mjs").then((n) => n.i);
|
|
147
|
+
const stale = staleManagedInstructions(root);
|
|
148
|
+
if (stale.length) {
|
|
149
|
+
const shown = stale.slice(0, 4).join(", ") + (stale.length > 4 ? `, +${stale.length - 4} more` : "");
|
|
150
|
+
const v = installedVersion();
|
|
151
|
+
console.log(` instructions predate ${v ? NAME + " " + v : "the installed version"} (${shown}) - run: npx ${NAME} init\n`);
|
|
152
|
+
}
|
|
153
|
+
} catch {}
|
|
144
154
|
const { loadCollab, syncOnce } = await import("./sync-BJKKmy1n.mjs");
|
|
145
155
|
if (loadCollab(root)) console.log(` comments: syncing with the published canvas (design/.local/collab.json)\n`);
|
|
146
156
|
let syncing = false;
|
|
@@ -171,7 +181,7 @@ async function dev(root, portFlag) {
|
|
|
171
181
|
});
|
|
172
182
|
}
|
|
173
183
|
if (config.jam) {
|
|
174
|
-
const { startJam } = await import("./daemon-
|
|
184
|
+
const { startJam } = await import("./daemon-BpKBJZee.mjs");
|
|
175
185
|
const jam = startJam(root, config.jam, (m) => console.log(m), (board) => server.ws.send("sh:jam-comment", { board }));
|
|
176
186
|
if (jam) {
|
|
177
187
|
const close = server.close.bind(server);
|
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
import { n as NAME } from "./cli.mjs";
|
|
2
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
|
+
import { MANAGED_PREFIX, enumerateInstructionTemplates, hashBody, managedFile, pkgDir } from "./managed-HwHVNI3h.mjs";
|
|
3
4
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
4
5
|
import { dirname, join, relative } from "node:path";
|
|
5
|
-
import { fileURLToPath } from "node:url";
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
6
|
//#region src/cli/init.ts
|
|
8
|
-
/** Package root = the nearest ancestor holding templates/ (one hop from dist/, two
|
|
9
|
-
* from src/cli/ - the walk serves both, and tests run init from source). */
|
|
10
|
-
const pkgDir = () => {
|
|
11
|
-
let dir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
for (let i = 0; i < 4; i++) {
|
|
13
|
-
if (existsSync(join(dir, "templates"))) return dir;
|
|
14
|
-
dir = dirname(dir);
|
|
15
|
-
}
|
|
16
|
-
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
17
|
-
};
|
|
18
7
|
/** Idempotent scaffolder: never overwrites existing files; every host-repo touch prints a diff. */
|
|
19
8
|
function init(root, opts) {
|
|
20
9
|
const host = detectHost(root);
|
|
@@ -57,7 +46,7 @@ function init(root, opts) {
|
|
|
57
46
|
rmSync(latest, { force: true });
|
|
58
47
|
return;
|
|
59
48
|
}
|
|
60
|
-
if (current.startsWith(
|
|
49
|
+
if (current.startsWith("<!-- marver:managed ")) {
|
|
61
50
|
const recorded = current.slice(MANAGED_PREFIX.length).split(" ")[0];
|
|
62
51
|
const nl = current.indexOf("\n");
|
|
63
52
|
const currentBody = nl >= 0 ? current.slice(nl + 1) : "";
|
|
@@ -75,7 +64,7 @@ function init(root, opts) {
|
|
|
75
64
|
}
|
|
76
65
|
console.warn(` ~ design/${rel}: you customized it and a newer version exists - your edits are untouched. Merge what you want from design/.local/latest/${rel}`);
|
|
77
66
|
}
|
|
78
|
-
} else if (current.startsWith(
|
|
67
|
+
} else if (current.startsWith("<!-- generated by marver init")) {
|
|
79
68
|
writeFileSync(file, next);
|
|
80
69
|
rmSync(latest, { force: true });
|
|
81
70
|
created.push(`design/${rel} (updated)`);
|
|
@@ -93,7 +82,7 @@ function init(root, opts) {
|
|
|
93
82
|
write("providers.tsx", providersTemplate(host.router, host.toaster, host.routerPkg));
|
|
94
83
|
writeManaged("AGENTS.md", readFileSync(join(templates, `AGENTS-${opts.mode}.md`), "utf8").replaceAll("{{UI_GUIDANCE}}", uiGuidance(host, noApp(host))).replace(/\{\{NEXT_NOTES\}\}\n?/, host.router === "next" ? NEXT_NOTES : ""));
|
|
95
84
|
const agentsNow = readFileSync(join(design, "AGENTS.md"), "utf8");
|
|
96
|
-
if (!agentsNow.startsWith(
|
|
85
|
+
if (!agentsNow.startsWith("<!-- marver:managed ") && !agentsNow.startsWith("<!-- generated by marver init") && agentsNow.includes("# Design canvas - agent contract") && !agentsNow.includes("## The method (binding)")) console.warn(` note: design/AGENTS.md predates managed regeneration - if you never edited it, delete it and re-run init to get the current contract (incl. the design/instructions routing).`);
|
|
97
86
|
const claudeMd = join(root, "CLAUDE.md");
|
|
98
87
|
const importLine = "@design/AGENTS.md";
|
|
99
88
|
if (!existsSync(claudeMd)) {
|
|
@@ -118,15 +107,12 @@ function init(root, opts) {
|
|
|
118
107
|
created.push("AGENTS.md (updated)");
|
|
119
108
|
}
|
|
120
109
|
}
|
|
121
|
-
const
|
|
122
|
-
for (const e of readdirSync(instrRoot, { withFileTypes: true })) if (e.isDirectory()) {
|
|
123
|
-
for (const f of readdirSync(join(instrRoot, e.name))) if (f.endsWith(".md")) writeManaged(`instructions/${e.name}/${f}`, readFileSync(join(instrRoot, e.name, f), "utf8"));
|
|
124
|
-
} else if (e.name.endsWith(".md")) writeManaged(`instructions/${e.name}`, readFileSync(join(instrRoot, e.name), "utf8"));
|
|
110
|
+
for (const { rel, body } of enumerateInstructionTemplates(templates)) writeManaged(rel, body);
|
|
125
111
|
const setupPath = join(design, "instructions", "setup.md");
|
|
126
112
|
const setupState = () => {
|
|
127
113
|
if (!existsSync(setupPath)) return "absent";
|
|
128
114
|
const s = readFileSync(setupPath, "utf8");
|
|
129
|
-
if (s.startsWith(
|
|
115
|
+
if (s.startsWith("<!-- marver:managed ")) {
|
|
130
116
|
const recorded = s.slice(MANAGED_PREFIX.length).split(" ")[0];
|
|
131
117
|
const nl = s.indexOf("\n");
|
|
132
118
|
return nl >= 0 && hashBody(s.slice(nl + 1)) === recorded ? "ours-pristine" : "ours-edited";
|
|
@@ -136,7 +122,7 @@ function init(root, opts) {
|
|
|
136
122
|
const setupWas = setupState();
|
|
137
123
|
const appJustAppeared = !noApp(host) && (setupWas === "ours-pristine" || setupWas === "ours-edited");
|
|
138
124
|
if (noApp(host)) {
|
|
139
|
-
if (setupWas === "ours-pristine" && !readFileSync(setupPath, "utf8").startsWith(
|
|
125
|
+
if (setupWas === "ours-pristine" && !readFileSync(setupPath, "utf8").startsWith("<!-- marver:managed ")) rmSync(setupPath);
|
|
140
126
|
if (setupWas !== "foreign") writeManaged("instructions/setup.md", SETUP_MD);
|
|
141
127
|
} else if (setupWas === "ours-pristine") {
|
|
142
128
|
rmSync(setupPath);
|
|
@@ -193,13 +179,6 @@ function init(root, opts) {
|
|
|
193
179
|
console.log(`\n next: npx ${NAME} dev (or: npx ${NAME} canvas - same thing; canvas on http://localhost:${DEFAULTS.port} by default)\n`);
|
|
194
180
|
if (!noApp(host)) console.log(` then, to your agent: "Read design/AGENTS.md. This is our first session - follow design/instructions/welcome.md."\n`);
|
|
195
181
|
}
|
|
196
|
-
const MANAGED_PREFIX = "<!-- marver:managed ";
|
|
197
|
-
const LEGACY_PREFIX = "<!-- generated by marver init";
|
|
198
|
-
const hashBody = (s) => createHash("sha256").update(s).digest("hex");
|
|
199
|
-
/** The marker carries a hash of the generated body: edits are DETECTED, not assumed.
|
|
200
|
-
* Edit freely - init preserves edits and stages upstream updates for merging.
|
|
201
|
-
* Deleting the marker line detaches the file from updates entirely. */
|
|
202
|
-
const managedFile = (body) => `${MANAGED_PREFIX}${hashBody(body)} - edit freely: init preserves your edits and stages upstream updates at design/.local/latest/ for you to merge. Delete this line to detach this file from updates entirely. -->\n${body}`;
|
|
203
182
|
/** The host's path aliases, re-rooted one level down for design/tsconfig.json.
|
|
204
183
|
* "./src/*" becomes "../src/*" so `@/` imports resolve from inside design/. */
|
|
205
184
|
function designPaths(root) {
|
|
@@ -1,19 +1,8 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
1
2
|
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, writeSync } from "node:fs";
|
|
2
3
|
import { dirname, join } from "node:path";
|
|
3
4
|
import { createHash } from "node:crypto";
|
|
4
5
|
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
6
|
//#region src/server/jam/device.ts
|
|
18
7
|
/**
|
|
19
8
|
* The device stamp - what makes "device-bound" true for the two files that decide whether
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
//#region src/server/managed.ts
|
|
6
|
+
/**
|
|
7
|
+
* Managed-file identity - the ONE source of truth for the `marver:managed <hash>` marker,
|
|
8
|
+
* shared by `init` (which WRITES managed files) and `dev`'s boot-time staleness probe (which
|
|
9
|
+
* only READS them). Kept side-effect-free on purpose: dev must learn whether the on-disk
|
|
10
|
+
* instructions are behind the installed package WITHOUT importing the mutating init() path.
|
|
11
|
+
*/
|
|
12
|
+
/** The marker records a HASH of the generated body: edits are DETECTED, not assumed. */
|
|
13
|
+
const MANAGED_PREFIX = "<!-- marver:managed ";
|
|
14
|
+
/** Hashless 0.2.2-dev marker: edits are undetectable, so init unconditionally upgrades it. */
|
|
15
|
+
const LEGACY_PREFIX = "<!-- generated by marver init";
|
|
16
|
+
const hashBody = (s) => createHash("sha256").update(s).digest("hex");
|
|
17
|
+
/** Prepend the managed marker to a generated body. Deleting the marker line detaches the
|
|
18
|
+
* file from updates entirely. */
|
|
19
|
+
const managedFile = (body) => `${MANAGED_PREFIX}${hashBody(body)} - edit freely: init preserves your edits and stages upstream updates at design/.local/latest/ for you to merge. Delete this line to detach this file from updates entirely. -->\n${body}`;
|
|
20
|
+
/** Package root = the nearest ancestor holding templates/ (one hop from dist/, two from
|
|
21
|
+
* src/** - the walk serves both, and tests run from source). */
|
|
22
|
+
const pkgDir = () => {
|
|
23
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
24
|
+
for (let i = 0; i < 4; i++) {
|
|
25
|
+
if (existsSync(join(dir, "templates"))) return dir;
|
|
26
|
+
dir = dirname(dir);
|
|
27
|
+
}
|
|
28
|
+
return join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
29
|
+
};
|
|
30
|
+
const templatesDir = () => join(pkgDir(), "templates");
|
|
31
|
+
function classifyManaged(current, shippedBody) {
|
|
32
|
+
if (current == null) return "absent";
|
|
33
|
+
if (current.startsWith("<!-- marver:managed ")) {
|
|
34
|
+
const nl = current.indexOf("\n");
|
|
35
|
+
if (nl < 0) return "detached";
|
|
36
|
+
const recorded = current.slice(20).split(" ")[0];
|
|
37
|
+
if (!/^[0-9a-f]{64}$/.test(recorded)) return "detached";
|
|
38
|
+
const body = current.slice(nl + 1);
|
|
39
|
+
const shipped = hashBody(shippedBody);
|
|
40
|
+
if (hashBody(body) === recorded) return recorded === shipped ? "pristine-current" : "pristine-stale";
|
|
41
|
+
return recorded === shipped ? "edited-current" : "edited-stale";
|
|
42
|
+
}
|
|
43
|
+
if (current.startsWith("<!-- generated by marver init")) return "legacy";
|
|
44
|
+
return "detached";
|
|
45
|
+
}
|
|
46
|
+
/** The managed INSTRUCTION set init writes, enumerated identically (init.ts): top-level
|
|
47
|
+
* templates/instructions/*.md plus one level of subdirectory *.md (reference/). setup.md is
|
|
48
|
+
* the SETUP_MD constant written separately/conditionally - it is not a template file, so it
|
|
49
|
+
* never appears here. */
|
|
50
|
+
function enumerateInstructionTemplates(templatesRoot) {
|
|
51
|
+
const instrRoot = join(templatesRoot, "instructions");
|
|
52
|
+
const out = [];
|
|
53
|
+
if (!existsSync(instrRoot)) return out;
|
|
54
|
+
for (const e of readdirSync(instrRoot, { withFileTypes: true })) if (e.isDirectory()) {
|
|
55
|
+
for (const f of readdirSync(join(instrRoot, e.name))) if (f.endsWith(".md")) out.push({
|
|
56
|
+
rel: `instructions/${e.name}/${f}`,
|
|
57
|
+
body: readFileSync(join(instrRoot, e.name, f), "utf8")
|
|
58
|
+
});
|
|
59
|
+
} else if (e.name.endsWith(".md") && e.name !== "setup.md") out.push({
|
|
60
|
+
rel: `instructions/${e.name}`,
|
|
61
|
+
body: readFileSync(join(instrRoot, e.name), "utf8")
|
|
62
|
+
});
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
const STALE = /* @__PURE__ */ new Set([
|
|
66
|
+
"absent",
|
|
67
|
+
"pristine-stale",
|
|
68
|
+
"edited-stale",
|
|
69
|
+
"legacy"
|
|
70
|
+
]);
|
|
71
|
+
/**
|
|
72
|
+
* Which managed instruction files on disk are behind the INSTALLED package (distinct from the
|
|
73
|
+
* registry update-check). Returns display names (basenames) of the stale files, AGENTS.md
|
|
74
|
+
* included only when the file is entirely ABSENT (a deleted marker is the documented opt-out,
|
|
75
|
+
* and AGENTS.md is host-substituted so its pristine hash never matches the raw template).
|
|
76
|
+
* Never throws: any read/enumeration failure yields an empty list so a boot can't crash on it.
|
|
77
|
+
*/
|
|
78
|
+
function staleManagedInstructions(root) {
|
|
79
|
+
try {
|
|
80
|
+
const design = join(root, "design");
|
|
81
|
+
const stale = [];
|
|
82
|
+
for (const { rel, body } of enumerateInstructionTemplates(templatesDir())) {
|
|
83
|
+
const file = join(design, rel);
|
|
84
|
+
const current = existsSync(file) ? readFileSync(file, "utf8") : null;
|
|
85
|
+
if (STALE.has(classifyManaged(current, body))) stale.push(rel.split("/").pop() ?? rel);
|
|
86
|
+
}
|
|
87
|
+
if (!existsSync(join(design, "AGENTS.md"))) stale.unshift("AGENTS.md");
|
|
88
|
+
return stale;
|
|
89
|
+
} catch {
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
//#endregion
|
|
94
|
+
export { LEGACY_PREFIX, MANAGED_PREFIX, enumerateInstructionTemplates, hashBody, managedFile, pkgDir, staleManagedInstructions };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
+
import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
|
|
1
2
|
import { i as ROUTE, n as NAME, r as PKG } from "./cli.mjs";
|
|
2
3
|
import { n as localProfile, t as isConnected } from "./profile-BkiWglVE.mjs";
|
|
3
4
|
import { a as writeManifest, n as hash, r as scanFrames, t as affectedFrameIds } from "./manifest-DIsp3ldB.mjs";
|
|
4
|
-
import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
5
|
+
import { copyFileSync, existsSync, linkSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, watch, writeFileSync } from "node:fs";
|
|
5
6
|
import { basename, dirname, join, resolve, sep } from "node:path";
|
|
6
7
|
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { randomBytes } from "node:crypto";
|
|
@@ -90,6 +91,31 @@ function apiMiddleware(root, opts = {}) {
|
|
|
90
91
|
const p = resolve(boardsDir, `${name}.json`);
|
|
91
92
|
return contained(p, boardsDir) ? p : null;
|
|
92
93
|
};
|
|
94
|
+
const validName = (n) => typeof n === "string" && n.length >= 1 && n.length <= 64 && BOARD_NAME.test(n);
|
|
95
|
+
const underRoot = (dir) => {
|
|
96
|
+
try {
|
|
97
|
+
const rr = realpathSync(root);
|
|
98
|
+
const rd = realpathSync(dir);
|
|
99
|
+
return rd === rr || rd.startsWith(rr + sep);
|
|
100
|
+
} catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const notSymlink = (p) => {
|
|
105
|
+
try {
|
|
106
|
+
return !existsSync(p) || !lstatSync(p).isSymbolicLink();
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
const nodeExists = (p) => {
|
|
112
|
+
try {
|
|
113
|
+
lstatSync(p);
|
|
114
|
+
return true;
|
|
115
|
+
} catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
93
119
|
try {
|
|
94
120
|
for (const f of readdirSync(boardsDir)) if (f.endsWith(".tmp")) rmSync(join(boardsDir, f), { force: true });
|
|
95
121
|
} catch {}
|
|
@@ -115,6 +141,72 @@ function apiMiddleware(root, opts = {}) {
|
|
|
115
141
|
};
|
|
116
142
|
}));
|
|
117
143
|
}
|
|
144
|
+
if (path === "boards/rename" && req.method === "POST") {
|
|
145
|
+
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
146
|
+
const raw = await readBody(req);
|
|
147
|
+
if (raw == null) return json(res, 400, { error: "body too large or unreadable" });
|
|
148
|
+
let parsed;
|
|
149
|
+
try {
|
|
150
|
+
parsed = JSON.parse(raw);
|
|
151
|
+
} catch {
|
|
152
|
+
return json(res, 400, { error: "malformed JSON" });
|
|
153
|
+
}
|
|
154
|
+
if (!parsed || typeof parsed !== "object") return json(res, 400, { error: "expected an object" });
|
|
155
|
+
const { from, to } = parsed;
|
|
156
|
+
if (!validName(from) || !validName(to)) return json(res, 400, { error: "invalid board name" });
|
|
157
|
+
if (to === "all-scenes" || from === "all-scenes" || to === from) return json(res, 400, { error: "invalid rename" });
|
|
158
|
+
if (!underRoot(boardsDir)) return json(res, 400, { error: "boards directory escapes the project" });
|
|
159
|
+
const fromPath = boardPath(from), toPath = boardPath(to);
|
|
160
|
+
if (!fromPath || !toPath) return json(res, 400, { error: "invalid board name" });
|
|
161
|
+
if (!existsSync(fromPath)) return json(res, 404, { error: `board "${from}" does not exist` });
|
|
162
|
+
if (!notSymlink(fromPath)) return json(res, 400, { error: "refusing to rename a symlinked board file" });
|
|
163
|
+
if (isConnected(root)) return json(res, 409, { error: "disconnect before renaming boards - comment sync is board-keyed" });
|
|
164
|
+
if (existsSync(join(root, "design", "comments", `${from}.jsonl`))) return json(res, 409, { error: "rename a board before commenting on it - this board has threads" });
|
|
165
|
+
if (nodeExists(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
166
|
+
try {
|
|
167
|
+
linkSync(fromPath, toPath);
|
|
168
|
+
} catch (err) {
|
|
169
|
+
const code = err.code;
|
|
170
|
+
if (code === "EEXIST") return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
171
|
+
if (code !== "EPERM" && code !== "ENOSYS" && code !== "ENOTSUP" && code !== "EOPNOTSUPP") throw err;
|
|
172
|
+
if (nodeExists(toPath)) return json(res, 409, { error: `a board named "${to}" already exists` });
|
|
173
|
+
renameSync(fromPath, toPath);
|
|
174
|
+
return json(res, 200, { name: to });
|
|
175
|
+
}
|
|
176
|
+
rmSync(fromPath);
|
|
177
|
+
return json(res, 200, { name: to });
|
|
178
|
+
}
|
|
179
|
+
if (path === "boards/reorder" && req.method === "POST") {
|
|
180
|
+
if (!ownerGated(req)) return json(res, 403, { error: "forbidden" });
|
|
181
|
+
const raw = await readBody(req);
|
|
182
|
+
if (raw == null) return json(res, 400, { error: "body too large or unreadable" });
|
|
183
|
+
let parsed;
|
|
184
|
+
try {
|
|
185
|
+
parsed = JSON.parse(raw);
|
|
186
|
+
} catch {
|
|
187
|
+
return json(res, 400, { error: "malformed JSON" });
|
|
188
|
+
}
|
|
189
|
+
if (!parsed || typeof parsed !== "object") return json(res, 400, { error: "expected an object" });
|
|
190
|
+
const order = parsed.order;
|
|
191
|
+
if (!Array.isArray(order) || order.length < 1 || order.length > 200) return json(res, 400, { error: "invalid order" });
|
|
192
|
+
if (!order.every(validName) || order.some((n) => n === "all-scenes")) return json(res, 400, { error: "invalid board name in order" });
|
|
193
|
+
if (new Set(order).size !== order.length) return json(res, 400, { error: "duplicate board in order" });
|
|
194
|
+
if (!underRoot(boardsDir)) return json(res, 400, { error: "boards directory escapes the project" });
|
|
195
|
+
for (let i = 0; i < order.length; i++) {
|
|
196
|
+
const p = boardPath(order[i]);
|
|
197
|
+
if (!p || !existsSync(p) || !notSymlink(p)) continue;
|
|
198
|
+
let obj;
|
|
199
|
+
try {
|
|
200
|
+
obj = JSON.parse(readFileSync(p, "utf8"));
|
|
201
|
+
} catch {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (!obj || typeof obj !== "object") continue;
|
|
205
|
+
obj.order = i;
|
|
206
|
+
atomicWrite(p, JSON.stringify(obj, null, 2) + "\n");
|
|
207
|
+
}
|
|
208
|
+
return json(res, 200, { ok: true });
|
|
209
|
+
}
|
|
118
210
|
const boardMatch = /^boards\/([^/]+)$/.exec(path);
|
|
119
211
|
if (boardMatch) {
|
|
120
212
|
const p = boardPath(boardMatch[1]);
|
|
@@ -174,7 +266,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
174
266
|
const frameId = url.searchParams.get("frame") ?? "";
|
|
175
267
|
const theme = url.searchParams.get("theme") ?? "light";
|
|
176
268
|
const origin = opts.origin?.() ?? `http://${req.headers.host ?? "localhost"}`;
|
|
177
|
-
const { shootFrame } = await import("./shot-
|
|
269
|
+
const { shootFrame } = await import("./shot-D7I0MCLu.mjs");
|
|
178
270
|
const r = await shootFrame({
|
|
179
271
|
root,
|
|
180
272
|
viewports: opts.viewports ?? {},
|
|
@@ -188,7 +280,12 @@ function apiMiddleware(root, opts = {}) {
|
|
|
188
280
|
frame: frameId,
|
|
189
281
|
theme,
|
|
190
282
|
width: r.width,
|
|
191
|
-
height: r.height
|
|
283
|
+
height: r.height,
|
|
284
|
+
scale: r.scale,
|
|
285
|
+
...r.truncated ? {
|
|
286
|
+
truncated: true,
|
|
287
|
+
note: r.note
|
|
288
|
+
} : {}
|
|
192
289
|
});
|
|
193
290
|
}
|
|
194
291
|
if (path === "me" && req.method === "GET") return json(res, 200, {
|
|
@@ -293,7 +390,7 @@ function apiMiddleware(root, opts = {}) {
|
|
|
293
390
|
};
|
|
294
391
|
});
|
|
295
392
|
const fresh = appendEvents(dir, cm[1], stamped);
|
|
296
|
-
const { record } = await import("./ledger-
|
|
393
|
+
const { record } = await import("./ledger-Bu0BjqIe.mjs").then((n) => n.n);
|
|
297
394
|
for (const ev of fresh) if (ev.type === "create" || ev.type === "reply") record(root, cm[1], ev.id);
|
|
298
395
|
backgroundPush(root);
|
|
299
396
|
return json(res, 200, { accepted: fresh.length });
|
|
@@ -395,6 +492,10 @@ function routesMiddleware(server, clientDir) {
|
|
|
395
492
|
* firewalled registries degrade to silence. MARVER_NO_UPDATE_CHECK=1 disables it.
|
|
396
493
|
* Published bundles never check anything - viewers are not the owner.
|
|
397
494
|
*/
|
|
495
|
+
var update_exports = /* @__PURE__ */ __exportAll({
|
|
496
|
+
checkUpdate: () => checkUpdate,
|
|
497
|
+
installedVersion: () => installedVersion
|
|
498
|
+
});
|
|
398
499
|
const TTL = 864e5;
|
|
399
500
|
/** Installed version: walk up from this module to the package's own package.json
|
|
400
501
|
* (one level from dist/, two from src/server/ - the walk covers both). */
|
|
@@ -697,7 +798,7 @@ function marverPlugin(ctx) {
|
|
|
697
798
|
});
|
|
698
799
|
return;
|
|
699
800
|
}
|
|
700
|
-
const { shootFrame } = await import("./shot-
|
|
801
|
+
const { shootFrame } = await import("./shot-D7I0MCLu.mjs");
|
|
701
802
|
write(await shootFrame({
|
|
702
803
|
root,
|
|
703
804
|
viewports: config.viewports,
|
|
@@ -777,4 +878,4 @@ async function tailwind3Css(root) {
|
|
|
777
878
|
return null;
|
|
778
879
|
}
|
|
779
880
|
//#endregion
|
|
780
|
-
export { tailwind3Css as n, tailwind4Plugin as r, marverPlugin as t };
|
|
881
|
+
export { update_exports as i, tailwind3Css as n, tailwind4Plugin as r, marverPlugin as t };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __exportAll = (all, no_symbols) => {
|
|
4
|
+
let target = {};
|
|
5
|
+
for (var name in all) __defProp(target, name, {
|
|
6
|
+
get: all[name],
|
|
7
|
+
enumerable: true
|
|
8
|
+
});
|
|
9
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
10
|
+
return target;
|
|
11
|
+
};
|
|
12
|
+
//#endregion
|
|
13
|
+
export { __exportAll as t };
|