@1agh/maude 0.23.0 → 0.25.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/README.md +34 -1
- package/cli/bin/maude.mjs +3 -0
- package/cli/commands/cache.mjs +181 -0
- package/cli/commands/cache.test.mjs +166 -0
- package/cli/commands/design-link.test.mjs +32 -2
- package/cli/commands/design.mjs +95 -4
- package/cli/commands/design.test.mjs +56 -0
- package/cli/commands/help.mjs +34 -0
- package/cli/commands/hub.mjs +255 -30
- package/cli/commands/hub.test.mjs +126 -2
- package/cli/commands/init.mjs +3 -0
- package/cli/commands/preflight.mjs +11 -0
- package/cli/commands/preflight.test.mjs +46 -0
- package/cli/commands/scenario-report.mjs +45 -0
- package/cli/lib/cache.mjs +407 -0
- package/cli/lib/cache.test.mjs +303 -0
- package/cli/lib/design-link.mjs +74 -10
- package/cli/lib/flow-design-integration.test.mjs +165 -0
- package/cli/lib/gitignore-block.mjs +90 -0
- package/cli/lib/gitignore-block.test.mjs +123 -0
- package/cli/lib/plugin-cli-reachability.test.mjs +56 -0
- package/cli/lib/preflight.mjs +41 -10
- package/package.json +8 -8
- package/plugins/design/dependencies.json +30 -2
- package/plugins/design/dev-server/ai-banner.tsx +2 -2
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +763 -123
- package/plugins/design/dev-server/annotations-layer.tsx +1624 -208
- package/plugins/design/dev-server/api.ts +123 -4
- package/plugins/design/dev-server/bin/_enumerate-artboards-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_html-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pdf-playwright.mjs +25 -8
- package/plugins/design/dev-server/bin/_png-playwright.mjs +20 -20
- package/plugins/design/dev-server/bin/_pptx-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_pw-launch.mjs +61 -0
- package/plugins/design/dev-server/bin/_screenshot-playwright.mjs +2 -2
- package/plugins/design/dev-server/bin/_svg-playwright.mjs +125 -19
- package/plugins/design/dev-server/bin/preflight.sh +21 -10
- package/plugins/design/dev-server/bin/prep.sh +211 -0
- package/plugins/design/dev-server/bin/scenario-report.mjs +209 -0
- package/plugins/design/dev-server/bin/screenshot.sh +18 -1
- package/plugins/design/dev-server/bin/smoke.sh +208 -23
- package/plugins/design/dev-server/canvas-arrowheads.ts +220 -0
- package/plugins/design/dev-server/canvas-comment-mount.tsx +8 -24
- package/plugins/design/dev-server/canvas-cursors.ts +173 -0
- package/plugins/design/dev-server/canvas-icons.tsx +299 -0
- package/plugins/design/dev-server/canvas-lib.tsx +25 -21
- package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
- package/plugins/design/dev-server/canvas-shell.tsx +404 -66
- package/plugins/design/dev-server/client/app.jsx +1231 -416
- package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
- package/plugins/design/dev-server/collab/index.ts +87 -9
- package/plugins/design/dev-server/collab/persistence.ts +34 -3
- package/plugins/design/dev-server/collab/registry.ts +80 -2
- package/plugins/design/dev-server/collab/room.ts +21 -8
- package/plugins/design/dev-server/context-menu.tsx +167 -23
- package/plugins/design/dev-server/context.ts +24 -0
- package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
- package/plugins/design/dev-server/dist/client.bundle.js +364 -29
- package/plugins/design/dev-server/dist/comment-mount.js +78 -33
- package/plugins/design/dev-server/dist/styles.css +16 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
- package/plugins/design/dev-server/export-dialog.tsx +208 -18
- package/plugins/design/dev-server/exporters/html.ts +4 -1
- package/plugins/design/dev-server/exporters/index.ts +4 -1
- package/plugins/design/dev-server/exporters/pdf.ts +7 -1
- package/plugins/design/dev-server/exporters/png.ts +21 -2
- package/plugins/design/dev-server/exporters/pptx.ts +330 -201
- package/plugins/design/dev-server/exporters/scope.ts +107 -11
- package/plugins/design/dev-server/exporters/svg.ts +4 -1
- package/plugins/design/dev-server/fs-watch.ts +1 -0
- package/plugins/design/dev-server/http.ts +299 -28
- package/plugins/design/dev-server/input-router.tsx +35 -5
- package/plugins/design/dev-server/participants-chrome.tsx +10 -10
- package/plugins/design/dev-server/server.ts +123 -1
- package/plugins/design/dev-server/sync/agent.ts +95 -0
- package/plugins/design/dev-server/sync/codec.ts +155 -0
- package/plugins/design/dev-server/sync/connection-state.ts +203 -0
- package/plugins/design/dev-server/sync/index.ts +479 -35
- package/plugins/design/dev-server/sync/materialize.ts +62 -0
- package/plugins/design/dev-server/sync/migrate-seed.ts +163 -0
- package/plugins/design/dev-server/sync/origins.ts +57 -0
- package/plugins/design/dev-server/sync/projection.ts +368 -0
- package/plugins/design/dev-server/sync/status.ts +115 -0
- package/plugins/design/dev-server/sync/untrusted.ts +153 -0
- package/plugins/design/dev-server/test/_helpers.ts +6 -2
- package/plugins/design/dev-server/test/annotations-draw-modifiers.test.ts +206 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +311 -0
- package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +519 -0
- package/plugins/design/dev-server/test/canvas-cursors.test.ts +162 -0
- package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
- package/plugins/design/dev-server/test/canvas-route.test.ts +4 -1
- package/plugins/design/dev-server/test/collab-reseed-guard.test.ts +78 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +21 -10
- package/plugins/design/dev-server/test/csp-canvas-shell.test.ts +46 -0
- package/plugins/design/dev-server/test/exporters/png.test.ts +24 -1
- package/plugins/design/dev-server/test/exporters/pptx-deck.test.ts +112 -0
- package/plugins/design/dev-server/test/exporters/pw-launch.test.ts +49 -0
- package/plugins/design/dev-server/test/exporters/scope.test.ts +0 -0
- package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
- package/plugins/design/dev-server/test/fixtures/phase-21-annotations.svg +1 -0
- package/plugins/design/dev-server/test/input-router.test.ts +32 -4
- package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +132 -0
- package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +265 -0
- package/plugins/design/dev-server/test/shared-doc-foundation.test.ts +63 -0
- package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +160 -0
- package/plugins/design/dev-server/test/shared-doc-projection.test.ts +238 -0
- package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
- package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
- package/plugins/design/dev-server/test/sync-runtime.test.ts +531 -4
- package/plugins/design/dev-server/test/sync-status.test.ts +80 -0
- package/plugins/design/dev-server/test/sync-untrusted.test.ts +104 -0
- package/plugins/design/dev-server/test/use-annotation-resize.test.ts +200 -0
- package/plugins/design/dev-server/test/use-tool-mode.test.tsx +39 -13
- package/plugins/design/dev-server/tool-palette.tsx +155 -24
- package/plugins/design/dev-server/undo-hud.tsx +4 -4
- package/plugins/design/dev-server/undo-stack.ts +20 -4
- package/plugins/design/dev-server/use-annotation-resize.tsx +219 -61
- package/plugins/design/dev-server/use-tool-mode.tsx +68 -19
- package/plugins/design/dev-server/ws.ts +40 -1
- package/plugins/design/templates/_shell.html +85 -10
- package/plugins/design/templates/canvas.tsx.template +13 -0
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +14 -7
- package/plugins/flow/.claude-plugin/config.schema.json +5 -0
- package/plugins/flow/templates/ai-skeleton/README.md +1 -1
- package/plugins/flow/templates/ai-skeleton/gitignore +10 -0
- package/plugins/flow/templates/ai-skeleton/scenarios/README.md +3 -1
- package/plugins/flow/templates/ai-skeleton/workflows.config.json +2 -1
|
@@ -135,6 +135,15 @@ export interface Api {
|
|
|
135
135
|
loadCommentsForFile(file: string): Promise<Comment[]>;
|
|
136
136
|
saveCommentsForFile(file: string, list: Comment[]): Promise<void>;
|
|
137
137
|
loadAllComments(): Promise<Record<string, Comment[]>>;
|
|
138
|
+
/**
|
|
139
|
+
* Resolve a canvas URL slug back to its repo-relative `file` path by scanning
|
|
140
|
+
* the ACTUAL canvas files under each canvas group — independent of whether the
|
|
141
|
+
* canvas has any comments yet. The inverse of `fileSlug`. Returns null when no
|
|
142
|
+
* canvas matches. Load-bearing for collab: a peer that has not yet received any
|
|
143
|
+
* comment for a canvas must still resolve the file to MATERIALIZE the first
|
|
144
|
+
* hub-pushed comment to disk (the receiving-peer projection gap, DDR-064).
|
|
145
|
+
*/
|
|
146
|
+
fileForSlug(slug: string): Promise<string | null>;
|
|
138
147
|
commentsAdd(payload: Partial<Comment> & { file: string; text: string }): Promise<Comment | null>;
|
|
139
148
|
commentsPatch(id: string, patch: Partial<Comment>): Promise<Comment | null>;
|
|
140
149
|
commentsDelete(id: string): Promise<boolean>;
|
|
@@ -173,6 +182,82 @@ export interface ApiHooks {
|
|
|
173
182
|
onAnnotationsChanged?: (file: string, svg: string) => void;
|
|
174
183
|
}
|
|
175
184
|
|
|
185
|
+
/**
|
|
186
|
+
* Elements the annotation tool legitimately emits (`strokesToSvg` in
|
|
187
|
+
* annotations-layer.tsx). This is the WHOLE vocabulary — purely presentational.
|
|
188
|
+
*/
|
|
189
|
+
const ANNOTATION_SVG_ELEMENTS = new Set([
|
|
190
|
+
'svg',
|
|
191
|
+
'g',
|
|
192
|
+
'path',
|
|
193
|
+
'rect',
|
|
194
|
+
'ellipse',
|
|
195
|
+
'line',
|
|
196
|
+
'polyline',
|
|
197
|
+
// Phase 24 — `polygon` (diamond / triangle shape primitives + closed arrowhead
|
|
198
|
+
// outlines + diamond heads) and `circle` (circle arrowhead). Inert shape
|
|
199
|
+
// elements with no script capability — same safety class as the rest.
|
|
200
|
+
'polygon',
|
|
201
|
+
'circle',
|
|
202
|
+
'text',
|
|
203
|
+
]);
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* A3 (DDR-060 F1 re-audit) — sanitize an annotation SVG before it is persisted /
|
|
207
|
+
* synced / mirrored into the collab Y.Map. ALLOWLIST, not denylist (the F1
|
|
208
|
+
* confirming pass showed a denylist loses the race: `<svg:script>` via namespace,
|
|
209
|
+
* `javascript:` via HTML entity, `<style>@import url()>`, CSS `url(javascript:)`
|
|
210
|
+
* all slipped a tag-name denylist). Two rules, both keyed to the fixed legit
|
|
211
|
+
* vocabulary so they're zero-regression on real annotations:
|
|
212
|
+
*
|
|
213
|
+
* 1. Element allowlist — drop the markup of any tag whose LOCAL name (namespace
|
|
214
|
+
* stripped, so `svg:script` → `script`) isn't in ANNOTATION_SVG_ELEMENTS.
|
|
215
|
+
* Dropped-tag text content survives as inert text (not in a script/style
|
|
216
|
+
* context), which is harmless.
|
|
217
|
+
* 2. Attribute denylist on survivors — the legit vocabulary uses none of
|
|
218
|
+
* `on*=`, `style=`, or any `href`, so stripping them closes inline handlers,
|
|
219
|
+
* CSS `url(javascript:)`, and entity-encoded `xlink:href` in one pass.
|
|
220
|
+
*
|
|
221
|
+
* The current consumer (`svgToStrokes` → DOMParser image/svg+xml → structured
|
|
222
|
+
* strokes → React) never raw-renders this string, so the live XSS risk is already
|
|
223
|
+
* nil; this keeps "inert" TRUE for any future raw-render consumer and for the
|
|
224
|
+
* synced file a peer / Claude-context ingests (defense-in-depth, DDR-054 §3).
|
|
225
|
+
*/
|
|
226
|
+
export function sanitizeAnnotationSvg(svg: string): string {
|
|
227
|
+
return (
|
|
228
|
+
svg
|
|
229
|
+
// 1. Remove the CONTENT of executable / instruction-bearing elements (not
|
|
230
|
+
// just their tags) so an injected script body / `@import` / prompt-
|
|
231
|
+
// injection string can't survive as inert text a future raw-renderer or
|
|
232
|
+
// Claude-context read might act on. Namespace-tolerant (`svg:script`),
|
|
233
|
+
// non-greedy to the first matching dangerous close tag.
|
|
234
|
+
.replace(
|
|
235
|
+
/<\s*(?:[\w-]+:)?(?:script|style|foreignObject|title|desc)\b[\s\S]*?<\s*\/\s*(?:[\w-]+:)?(?:script|style|foreignObject|title|desc)\s*>/gi,
|
|
236
|
+
''
|
|
237
|
+
)
|
|
238
|
+
// 2. Element allowlist — drop the markup of any tag whose LOCAL name isn't
|
|
239
|
+
// in the fixed annotation vocabulary. `[^>]*` stops at the first `>`;
|
|
240
|
+
// annotation attrs never contain a literal `>`.
|
|
241
|
+
.replace(/<\/?\s*([a-zA-Z][\w:-]*)\b[^>]*>/g, (match, rawName: string) => {
|
|
242
|
+
const local = String(rawName).split(':').pop()?.toLowerCase() ?? '';
|
|
243
|
+
return ANNOTATION_SVG_ELEMENTS.has(local) ? match : '';
|
|
244
|
+
})
|
|
245
|
+
// 3. Attribute denylist on the surviving allowlisted elements — the legit
|
|
246
|
+
// vocabulary uses no on*= / style= / *href=, so stripping them closes
|
|
247
|
+
// inline handlers, CSS url(javascript:), and entity-encoded hrefs.
|
|
248
|
+
// The leading boundary is a LOOKBEHIND on whitespace / quote / slash
|
|
249
|
+
// (not a consumed `\s`) so a handler glued to the previous attribute's
|
|
250
|
+
// closing quote — `<circle r="2"onload="…"/>`, which HTML/SVG parsers
|
|
251
|
+
// accept as a distinct attribute — is also stripped (Phase 24 security
|
|
252
|
+
// review, DDR-067). The non-consuming lookbehind leaves the preceding
|
|
253
|
+
// quote intact so the legit attribute it belonged to survives.
|
|
254
|
+
.replace(
|
|
255
|
+
/(?<=[\s"'/])(?:on[a-z]+|style|(?:[\w-]+:)?href)\s*=\s*("[^"]*"|'[^']*'|[^\s>]+)/gi,
|
|
256
|
+
''
|
|
257
|
+
)
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
176
261
|
export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
177
262
|
const onCommentsChanged = hooks.onCommentsChanged;
|
|
178
263
|
const onAnnotationsChanged = hooks.onAnnotationsChanged;
|
|
@@ -195,6 +280,28 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
195
280
|
.toLowerCase();
|
|
196
281
|
}
|
|
197
282
|
|
|
283
|
+
async function fileForSlug(slug: string): Promise<string | null> {
|
|
284
|
+
// Authoritative slug → canvas-file resolver: enumerate the real canvas
|
|
285
|
+
// files under each canvas group and match by the canonical slug. Unlike a
|
|
286
|
+
// comments-file scan, this resolves even when the canvas has NO comments yet
|
|
287
|
+
// — the fix for the receiving-peer projection gap where a fresh peer could
|
|
288
|
+
// not locate the file to write the first hub-pushed comment (DDR-064).
|
|
289
|
+
for (const g of cfg.canvasGroups) {
|
|
290
|
+
const groupAbs = path.join(paths.designRoot, g.path);
|
|
291
|
+
const groupRel = path.posix.join(paths.designRel, g.path);
|
|
292
|
+
let files: string[];
|
|
293
|
+
try {
|
|
294
|
+
files = await findHtmlFiles(groupAbs, groupRel);
|
|
295
|
+
} catch {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
for (const rel of files) {
|
|
299
|
+
if (fileSlug(rel) === slug) return rel;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
|
|
198
305
|
function commentsPath(file: string): string {
|
|
199
306
|
return path.join(paths.commentsDir, `${fileSlug(file)}.json`);
|
|
200
307
|
}
|
|
@@ -586,11 +693,22 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
586
693
|
if (typeof svg !== 'string') return false;
|
|
587
694
|
if (svg.length > 1024 * 1024) return false;
|
|
588
695
|
// Cheap content gate — must look like an <svg> document. Avoids accidental
|
|
589
|
-
// writes of arbitrary blobs through this endpoint.
|
|
590
|
-
// content fully, so we don't try to sanitize beyond a tag check.
|
|
696
|
+
// writes of arbitrary blobs through this endpoint.
|
|
591
697
|
if (!/^\s*<svg[\s>]/i.test(svg)) return false;
|
|
592
|
-
|
|
593
|
-
|
|
698
|
+
// A3 (DDR-060 F1 re-audit) — sanitize active content before persisting.
|
|
699
|
+
// This endpoint is on the canvas-origin allowlist (DDR-054 "inert collab
|
|
700
|
+
// write") and accepts ANY `file`, so a hub-pushed canvas can write a
|
|
701
|
+
// sibling's `.annotations.svg`. The persisted SVG is currently consumed only
|
|
702
|
+
// via `svgToStrokes` (DOMParser image/svg+xml → structured strokes → React
|
|
703
|
+
// re-render), so a `<script>`/`on*` payload is parsed inertly and discarded
|
|
704
|
+
// — the stored-XSS chain is LATENT today, not live. We sanitize anyway so
|
|
705
|
+
// "inert" stays true for any future raw-render consumer and for the synced
|
|
706
|
+
// file a peer/Claude-context ingests. The legit annotation vocabulary
|
|
707
|
+
// (strokesToSvg) is purely presentational — path/rect/ellipse/g/line/
|
|
708
|
+
// polyline/text — so stripping executable constructs is zero-regression.
|
|
709
|
+
const clean = sanitizeAnnotationSvg(svg);
|
|
710
|
+
await Bun.write(annotationsPath(file), clean);
|
|
711
|
+
onAnnotationsChanged?.(file, clean);
|
|
594
712
|
return true;
|
|
595
713
|
}
|
|
596
714
|
|
|
@@ -946,6 +1064,7 @@ export function createApi(ctx: Context, hooks: ApiHooks): Api {
|
|
|
946
1064
|
loadCommentsForFile,
|
|
947
1065
|
saveCommentsForFile,
|
|
948
1066
|
loadAllComments,
|
|
1067
|
+
fileForSlug,
|
|
949
1068
|
commentsAdd,
|
|
950
1069
|
commentsPatch,
|
|
951
1070
|
commentsDelete,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// merge. Lives as a subprocess (not a direct import) so `bun build --compile`
|
|
5
5
|
// of the dev-server binary doesn't pull in playwright + chromium-bidi deep deps.
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
8
8
|
|
|
9
9
|
const args = process.argv.slice(2);
|
|
10
10
|
let url;
|
|
@@ -24,7 +24,7 @@ if (!url) {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const timeoutMs = timeoutSec * 1000;
|
|
27
|
-
const browser = await
|
|
27
|
+
const browser = await launchChromium();
|
|
28
28
|
try {
|
|
29
29
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
30
30
|
const page = await ctx.newPage();
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
15
15
|
import { dirname, join } from 'node:path';
|
|
16
|
-
import {
|
|
16
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
17
17
|
|
|
18
18
|
const args = Object.fromEntries(
|
|
19
19
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
@@ -41,7 +41,7 @@ const widen = widenFlag !== undefined;
|
|
|
41
41
|
const multi = multiFlag !== undefined;
|
|
42
42
|
const timeoutMs = Number(timeout) * 1000;
|
|
43
43
|
|
|
44
|
-
const browser = await
|
|
44
|
+
const browser = await launchChromium();
|
|
45
45
|
try {
|
|
46
46
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
47
47
|
const page = await ctx.newPage();
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { dirname, join } from 'node:path';
|
|
14
|
-
import {
|
|
14
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
15
15
|
|
|
16
16
|
const args = Object.fromEntries(
|
|
17
17
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
@@ -20,17 +20,26 @@ const args = Object.fromEntries(
|
|
|
20
20
|
}, [])
|
|
21
21
|
);
|
|
22
22
|
|
|
23
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
url,
|
|
25
|
+
selector,
|
|
26
|
+
out,
|
|
27
|
+
'out-dir': outDir,
|
|
28
|
+
'widen-to-artboard': widenFlag,
|
|
29
|
+
multi: multiFlag,
|
|
30
|
+
timeout = '12',
|
|
31
|
+
} = args;
|
|
24
32
|
|
|
25
33
|
if (!url) {
|
|
26
34
|
console.error('usage: _pdf-playwright.mjs --url <url> --selector <css> --out <path>');
|
|
27
35
|
process.exit(2);
|
|
28
36
|
}
|
|
29
37
|
|
|
38
|
+
const widen = widenFlag !== undefined;
|
|
30
39
|
const multi = multiFlag !== undefined;
|
|
31
40
|
const timeoutMs = Number(timeout) * 1000;
|
|
32
41
|
|
|
33
|
-
const browser = await
|
|
42
|
+
const browser = await launchChromium();
|
|
34
43
|
try {
|
|
35
44
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
36
45
|
const page = await ctx.newPage();
|
|
@@ -63,12 +72,20 @@ try {
|
|
|
63
72
|
});
|
|
64
73
|
|
|
65
74
|
for (let i = 0; i < screens.length; i += 1) {
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
//
|
|
75
|
+
// Widen a descendant selector to its enclosing artboard when requested
|
|
76
|
+
// (artboard-via-descendant fallback); selection / artboard-by-id targets
|
|
77
|
+
// pass through unchanged.
|
|
78
|
+
const handle =
|
|
79
|
+
widen && !multi
|
|
80
|
+
? await screens[i].evaluateHandle((el) => el.closest('[data-dc-screen]') ?? el)
|
|
81
|
+
: screens[i];
|
|
82
|
+
// Pin the captured element's own artboard to (0,0) right before its
|
|
83
|
+
// capture so the world's multi-artboard layout doesn't push the bbox off
|
|
84
|
+
// the viewport.
|
|
69
85
|
await handle.evaluate((el) => {
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
87
|
+
ab.style.left = '0px';
|
|
88
|
+
ab.style.top = '0px';
|
|
72
89
|
});
|
|
73
90
|
const rect = await handle.evaluate((el) => {
|
|
74
91
|
const r = el.getBoundingClientRect();
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { dirname, join } from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
11
11
|
|
|
12
12
|
const args = Object.fromEntries(
|
|
13
13
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
@@ -37,7 +37,7 @@ const multi = multiFlag !== undefined;
|
|
|
37
37
|
const timeoutMs = Number(timeout) * 1000;
|
|
38
38
|
const deviceScaleFactor = Math.max(1, Math.min(4, Number(scale) || 1));
|
|
39
39
|
|
|
40
|
-
const browser = await
|
|
40
|
+
const browser = await launchChromium();
|
|
41
41
|
try {
|
|
42
42
|
// 1440x900 matches the canvas viewport the design tool uses everywhere;
|
|
43
43
|
// exporters resize per-target before each shot to fit the artboard exactly.
|
|
@@ -63,24 +63,24 @@ try {
|
|
|
63
63
|
// `zoom` (not `transform: scale`) on `.dc-world`, which actually shrinks
|
|
64
64
|
// layout — getBoundingClientRect returns 818×512 instead of 1440×900
|
|
65
65
|
// unless we zero both `zoom` and `transform` here.
|
|
66
|
-
await page.evaluate(
|
|
67
|
-
(
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
);
|
|
66
|
+
await page.evaluate(() => {
|
|
67
|
+
const world = document.querySelector('.dc-world');
|
|
68
|
+
if (world) {
|
|
69
|
+
world.style.zoom = '1';
|
|
70
|
+
world.style.transform = 'none';
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// Pin the captured element's OWN artboard to (0,0). Previously this reset
|
|
74
|
+
// `[data-dc-screen]:first-of-type` regardless of the target, so capturing
|
|
75
|
+
// artboard #2 (or any non-first selection's host) left the wrong artboard
|
|
76
|
+
// at the origin (item 5 root cause). Each artboard carries
|
|
77
|
+
// `style="left:…; top:…;"` for the world layout — zero the right one so the
|
|
78
|
+
// screenshot clip starts at the viewport origin.
|
|
79
|
+
await widenedHandle.evaluate((el) => {
|
|
80
|
+
const ab = el.closest('[data-dc-screen]') ?? el;
|
|
81
|
+
ab.style.left = '0px';
|
|
82
|
+
ab.style.top = '0px';
|
|
83
|
+
});
|
|
84
84
|
const rect = await widenedHandle.evaluate((el) => {
|
|
85
85
|
const r = el.getBoundingClientRect();
|
|
86
86
|
return { x: r.left, y: r.top, width: r.width, height: r.height };
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
13
|
import { dirname } from 'node:path';
|
|
14
|
-
import {
|
|
14
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
15
15
|
|
|
16
16
|
const args = Object.fromEntries(
|
|
17
17
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
@@ -34,7 +34,7 @@ const timeoutMs = Number(timeout) * 1000;
|
|
|
34
34
|
|
|
35
35
|
mkdirSync(dirname(out), { recursive: true });
|
|
36
36
|
|
|
37
|
-
const browser = await
|
|
37
|
+
const browser = await launchChromium();
|
|
38
38
|
try {
|
|
39
39
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
40
40
|
const page = await ctx.newPage();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// _pw-launch.mjs — shared Chromium launcher for the export / screenshot shims.
|
|
2
|
+
//
|
|
3
|
+
// Every renderer shim (_png/_pdf/_svg/_html/_pptx/_enumerate-artboards/
|
|
4
|
+
// _screenshot-playwright.mjs) opens a Chromium instance with `chromium.launch()`.
|
|
5
|
+
// Playwright the npm package can be present while its browser BINARY is not — the
|
|
6
|
+
// package install and `npx playwright install chromium` are separate steps, and
|
|
7
|
+
// the dependency check `npx playwright --version` only proves the former. When
|
|
8
|
+
// the binary is missing, `launch()` rejects with a multi-line stack trace that
|
|
9
|
+
// `exporters/*.ts` rethrows and `/_api/export` surfaces verbatim as an opaque
|
|
10
|
+
// 500 ("nefunguji exporty"). This helper funnels that one failure mode through a
|
|
11
|
+
// single place: it prints one actionable line and exits with a distinct code, so
|
|
12
|
+
// the export endpoint's 500 body becomes the remediation instead of a stack. Any
|
|
13
|
+
// OTHER launch error is rethrown untouched — real diagnostics are preserved.
|
|
14
|
+
//
|
|
15
|
+
// See `.ai/logs/rca/issue-nefunguji-exporty.md`.
|
|
16
|
+
|
|
17
|
+
import { chromium } from 'playwright';
|
|
18
|
+
|
|
19
|
+
/** Exit code the shims surface for "Chromium browser binary not installed". */
|
|
20
|
+
export const NO_BROWSER_EXIT = 3;
|
|
21
|
+
|
|
22
|
+
/** One-line, copy-pasteable remediation shown in the export 500 body. */
|
|
23
|
+
export const INSTALL_HINT =
|
|
24
|
+
"Playwright's Chromium browser isn't installed. Run: npx playwright install chromium";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* True when a `chromium.launch()` rejection is the "browser binary not
|
|
28
|
+
* downloaded" case (as opposed to a sandbox / port / crash failure). Pure +
|
|
29
|
+
* exported so it's unit-testable without spawning a browser. Matches the stable
|
|
30
|
+
* fragments Playwright emits across versions:
|
|
31
|
+
* "browserType.launch: Executable doesn't exist at <path>"
|
|
32
|
+
* "Please run the following command to download new browsers"
|
|
33
|
+
* "playwright install"
|
|
34
|
+
*/
|
|
35
|
+
export function isMissingBrowserError(message) {
|
|
36
|
+
const m = String(message ?? '');
|
|
37
|
+
return (
|
|
38
|
+
/Executable doesn'?t exist/i.test(m) ||
|
|
39
|
+
/please run the following command to download/i.test(m) ||
|
|
40
|
+
/playwright install/i.test(m)
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Launch Chromium, mapping the missing-binary failure to a clean exit. Drop-in
|
|
46
|
+
* for `chromium.launch(opts)` — same signature, same resolved Browser.
|
|
47
|
+
*/
|
|
48
|
+
export async function launchChromium(opts) {
|
|
49
|
+
try {
|
|
50
|
+
return await chromium.launch(opts);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
53
|
+
if (isMissingBrowserError(msg)) {
|
|
54
|
+
// Single clean line — the export endpoint pipes shim stderr into the 500
|
|
55
|
+
// body, so this is what the user reads. No stack trace.
|
|
56
|
+
console.error(INSTALL_HINT);
|
|
57
|
+
process.exit(NO_BROWSER_EXIT);
|
|
58
|
+
}
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
import { mkdirSync } from 'node:fs';
|
|
13
13
|
import { dirname } from 'node:path';
|
|
14
|
-
import {
|
|
14
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
15
15
|
|
|
16
16
|
const args = Object.fromEntries(
|
|
17
17
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
@@ -31,7 +31,7 @@ if (!url || !out) {
|
|
|
31
31
|
const timeoutMs = Number(timeout) * 1000;
|
|
32
32
|
mkdirSync(dirname(out), { recursive: true });
|
|
33
33
|
|
|
34
|
-
const browser = await
|
|
34
|
+
const browser = await launchChromium();
|
|
35
35
|
try {
|
|
36
36
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
37
37
|
const page = await ctx.newPage();
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { dirname, join } from 'node:path';
|
|
21
|
-
import {
|
|
21
|
+
import { launchChromium } from './_pw-launch.mjs';
|
|
22
22
|
|
|
23
23
|
const args = Object.fromEntries(
|
|
24
24
|
process.argv.slice(2).reduce((acc, cur, i, all) => {
|
|
@@ -49,7 +49,7 @@ const widen = widenFlag !== undefined;
|
|
|
49
49
|
const multi = multiFlag !== undefined;
|
|
50
50
|
const timeoutMs = Number(timeout) * 1000;
|
|
51
51
|
|
|
52
|
-
const browser = await
|
|
52
|
+
const browser = await launchChromium();
|
|
53
53
|
try {
|
|
54
54
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
55
55
|
const page = await ctx.newPage();
|
|
@@ -74,27 +74,142 @@ try {
|
|
|
74
74
|
|
|
75
75
|
const written = [];
|
|
76
76
|
|
|
77
|
-
|
|
77
|
+
// Single in-page serializer used by BOTH branches (single + multi) so they
|
|
78
|
+
// can't drift again (the `formatXML` 500 bug came from a divergent copy).
|
|
79
|
+
// Note: dom-to-svg exports only elementToSVG / inlineResources /
|
|
80
|
+
// documentToSVG — there is NO `formatXML` pretty-printer; serialize straight
|
|
81
|
+
// to a string. dom-to-svg also does NOT paint the captured root's background
|
|
82
|
+
// fill, so the artboard's background was dropped (item 4) — we prepend a
|
|
83
|
+
// backdrop <rect> from the artboard's effective (visible) background color.
|
|
84
|
+
const serializeOne = async (handle, widenToArtboard) => {
|
|
78
85
|
return await handle.evaluate(
|
|
79
86
|
async (el, opts) => {
|
|
80
87
|
const target = opts.widenToArtboard ? (el.closest('[data-dc-screen]') ?? el) : el;
|
|
81
88
|
// window.domToSvg is the IIFE-injected entry.
|
|
82
89
|
const { elementToSVG, inlineResources } = /** @type any */ (window).domToSvg;
|
|
90
|
+
|
|
91
|
+
// Convert ANY computed color (modern Chromium serializes `oklch(...)`
|
|
92
|
+
// verbatim from getComputedStyle) to sRGB rgb()/rgba(). Affinity
|
|
93
|
+
// Designer / older Illustrator / Inkscape don't parse CSS Color 4
|
|
94
|
+
// (`oklch`, `color()`) and silently drop the fill — the artboard then
|
|
95
|
+
// shows as transparent (the exact symptom: bg vanished in Affinity).
|
|
96
|
+
// A 1×1 canvas normalizes any CSS color string to sRGB bytes. Returns
|
|
97
|
+
// null if the value can't be painted (keep the rect off rather than
|
|
98
|
+
// emit a wrong colour).
|
|
99
|
+
const _srgbCache = new Map();
|
|
100
|
+
const _srgbCanvas = document.createElement('canvas');
|
|
101
|
+
_srgbCanvas.width = 1;
|
|
102
|
+
_srgbCanvas.height = 1;
|
|
103
|
+
const _srgbCtx = _srgbCanvas.getContext('2d');
|
|
104
|
+
const toSrgb = (color) => {
|
|
105
|
+
if (_srgbCache.has(color)) return _srgbCache.get(color);
|
|
106
|
+
let result = null;
|
|
107
|
+
try {
|
|
108
|
+
if (_srgbCtx) {
|
|
109
|
+
_srgbCtx.clearRect(0, 0, 1, 1);
|
|
110
|
+
_srgbCtx.fillStyle = '#000';
|
|
111
|
+
_srgbCtx.fillStyle = color; // ignored (stays #000) if syntax unsupported
|
|
112
|
+
_srgbCtx.fillRect(0, 0, 1, 1);
|
|
113
|
+
const [r, g, b, a] = _srgbCtx.getImageData(0, 0, 1, 1).data;
|
|
114
|
+
if (a !== 0) {
|
|
115
|
+
result =
|
|
116
|
+
a === 255
|
|
117
|
+
? `rgb(${r}, ${g}, ${b})`
|
|
118
|
+
: `rgba(${r}, ${g}, ${b}, ${(a / 255).toFixed(3)})`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch {
|
|
122
|
+
result = null;
|
|
123
|
+
}
|
|
124
|
+
_srgbCache.set(color, result);
|
|
125
|
+
return result;
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const isOpaque = (bg) =>
|
|
129
|
+
!!bg &&
|
|
130
|
+
bg !== 'transparent' &&
|
|
131
|
+
!/^rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0\s*\)$/.test(bg) &&
|
|
132
|
+
!/^rgba\(\s*0\s*,\s*0\s*,\s*0\s*,\s*0(\.0+)?\s*\)$/.test(bg);
|
|
133
|
+
// Effective background of the artboard region: the DS theme bg usually
|
|
134
|
+
// sits on an inner full-bleed wrapper (`.app` / `.mdcc`), occasionally
|
|
135
|
+
// on the artboard element itself, sometimes on body. Descend first to
|
|
136
|
+
// find a near-full-bleed opaque bg, then fall back to ancestors. An
|
|
137
|
+
// intentionally transparent artboard returns null → stays transparent.
|
|
138
|
+
const effectiveBg = (node) => {
|
|
139
|
+
const abRect = node.getBoundingClientRect();
|
|
140
|
+
const abArea = Math.max(1, abRect.width * abRect.height);
|
|
141
|
+
const queue = [node];
|
|
142
|
+
let guard = 0;
|
|
143
|
+
while (queue.length && guard < 80) {
|
|
144
|
+
const e = queue.shift();
|
|
145
|
+
guard += 1;
|
|
146
|
+
const bg = getComputedStyle(e).backgroundColor;
|
|
147
|
+
if (isOpaque(bg)) {
|
|
148
|
+
const r = e.getBoundingClientRect();
|
|
149
|
+
if (r.width * r.height >= abArea * 0.6) return bg;
|
|
150
|
+
}
|
|
151
|
+
for (const c of e.children) queue.push(c);
|
|
152
|
+
}
|
|
153
|
+
let cur = node.parentElement;
|
|
154
|
+
let up = 0;
|
|
155
|
+
while (cur && up < 12) {
|
|
156
|
+
const bg = getComputedStyle(cur).backgroundColor;
|
|
157
|
+
if (isOpaque(bg)) return bg;
|
|
158
|
+
cur = cur.parentElement;
|
|
159
|
+
up += 1;
|
|
160
|
+
}
|
|
161
|
+
return null;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const bg = effectiveBg(target);
|
|
165
|
+
// Normalize to sRGB so the fill survives in vector editors (item 4 /
|
|
166
|
+
// oklch follow-up). If conversion fails, skip the backdrop rather than
|
|
167
|
+
// emit an unparseable fill.
|
|
168
|
+
const fillColor = bg ? toSrgb(bg) : null;
|
|
83
169
|
const svgDoc = elementToSVG(target);
|
|
170
|
+
const root = svgDoc.documentElement;
|
|
171
|
+
if (fillColor) {
|
|
172
|
+
const NS = 'http://www.w3.org/2000/svg';
|
|
173
|
+
const rect = svgDoc.createElementNS(NS, 'rect');
|
|
174
|
+
const vb = (root.getAttribute('viewBox') || '').split(/\s+/).map(Number);
|
|
175
|
+
if (vb.length === 4 && vb.every((n) => !Number.isNaN(n))) {
|
|
176
|
+
rect.setAttribute('x', String(vb[0]));
|
|
177
|
+
rect.setAttribute('y', String(vb[1]));
|
|
178
|
+
rect.setAttribute('width', String(vb[2]));
|
|
179
|
+
rect.setAttribute('height', String(vb[3]));
|
|
180
|
+
} else {
|
|
181
|
+
rect.setAttribute('x', '0');
|
|
182
|
+
rect.setAttribute('y', '0');
|
|
183
|
+
rect.setAttribute('width', '100%');
|
|
184
|
+
rect.setAttribute('height', '100%');
|
|
185
|
+
}
|
|
186
|
+
rect.setAttribute('fill', fillColor);
|
|
187
|
+
root.insertBefore(rect, root.firstChild);
|
|
188
|
+
}
|
|
84
189
|
// base64-embeds fonts + images so the SVG is portable outside the
|
|
85
190
|
// dev-server origin. Some external fetches fail silently — Affinity
|
|
86
191
|
// tolerates missing resources better than missing primitives.
|
|
87
192
|
try {
|
|
88
|
-
await inlineResources(
|
|
193
|
+
await inlineResources(root);
|
|
89
194
|
} catch {
|
|
90
195
|
/* best-effort */
|
|
91
196
|
}
|
|
92
|
-
|
|
197
|
+
let out = new XMLSerializer().serializeToString(svgDoc);
|
|
198
|
+
// dom-to-svg copies computed colours through VERBATIM, and modern
|
|
199
|
+
// Chromium serializes DS tokens as `oklch(...)` — Affinity Designer /
|
|
200
|
+
// older Illustrator / Inkscape can't parse CSS Color 4 and drop every
|
|
201
|
+
// such fill/stroke (the artboard bg + many element colours vanish).
|
|
202
|
+
// Rewrite every CSS Color 4 colour function (oklch/oklab/lch/lab/color)
|
|
203
|
+
// anywhere in the serialized SVG to its sRGB rgb()/rgba() equivalent.
|
|
204
|
+
// Each token is a standalone colour (no nested parens), so a single
|
|
205
|
+
// `\([^)]*\)` match is safe; toSrgb returns null on an unconvertible
|
|
206
|
+
// value and we keep the original rather than corrupt it.
|
|
207
|
+
out = out.replace(/(?:oklch|oklab|lch|lab|color)\([^)]*\)/gi, (m) => toSrgb(m) || m);
|
|
208
|
+
return out;
|
|
93
209
|
},
|
|
94
|
-
{ widenToArtboard
|
|
210
|
+
{ widenToArtboard }
|
|
95
211
|
);
|
|
96
212
|
};
|
|
97
|
-
const opts = { widenToArtboard: widen };
|
|
98
213
|
|
|
99
214
|
if (multi) {
|
|
100
215
|
if (!outDir) {
|
|
@@ -106,17 +221,8 @@ try {
|
|
|
106
221
|
for (let i = 0; i < screens.length; i += 1) {
|
|
107
222
|
const handle = screens[i];
|
|
108
223
|
const id = (await handle.getAttribute('data-dc-screen')) ?? `artboard-${i + 1}`;
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const { elementToSVG, inlineResources, formatXML } = /** @type any */ (window).domToSvg;
|
|
112
|
-
const svgDoc = elementToSVG(el);
|
|
113
|
-
try {
|
|
114
|
-
await inlineResources(svgDoc.documentElement);
|
|
115
|
-
} catch {
|
|
116
|
-
/* */
|
|
117
|
-
}
|
|
118
|
-
return formatXML(new XMLSerializer().serializeToString(svgDoc));
|
|
119
|
-
});
|
|
224
|
+
// Each multi handle is already a `[data-dc-screen]` artboard — no widen.
|
|
225
|
+
const svg = await serializeOne(handle, false);
|
|
120
226
|
const target = join(outDir, `${id}.svg`);
|
|
121
227
|
writeFileSync(target, svg, 'utf8');
|
|
122
228
|
written.push(target);
|
|
@@ -129,7 +235,7 @@ try {
|
|
|
129
235
|
mkdirSync(dirname(out), { recursive: true });
|
|
130
236
|
const handle = page.locator(selector ?? '[data-dc-screen]:first-of-type').first();
|
|
131
237
|
await handle.waitFor({ state: 'visible', timeout: timeoutMs });
|
|
132
|
-
const svg = await serializeOne(handle);
|
|
238
|
+
const svg = await serializeOne(handle, widen);
|
|
133
239
|
writeFileSync(out, svg, 'utf8');
|
|
134
240
|
written.push(out);
|
|
135
241
|
}
|
|
@@ -13,9 +13,16 @@
|
|
|
13
13
|
# preflight.sh --warn-only # same as --quiet (SessionStart hook)
|
|
14
14
|
#
|
|
15
15
|
# Resolution (DDR-045): never compute paths from `dirname $0` inside a
|
|
16
|
-
# `bun --compile` standalone binary — that maps to /$bunfs/root.
|
|
17
|
-
#
|
|
18
|
-
#
|
|
16
|
+
# `bun --compile` standalone binary — that maps to /$bunfs/root.
|
|
17
|
+
#
|
|
18
|
+
# Resolution (DDR-061): in a MARKETPLACE install the plugin is copied alone into
|
|
19
|
+
# `cache/<marketplace>/<plugin>/<version>/` — there is NO sibling `$PKG_ROOT/cli/`.
|
|
20
|
+
# So: use the sibling `cli/lib` when it exists (running uncompiled from the repo
|
|
21
|
+
# or an npm package, where cli/ is present); otherwise reach the check through
|
|
22
|
+
# the on-PATH `maude` binary (a declared plugin dependency), which resolves the
|
|
23
|
+
# manifest from its own package root. Fixing a straggler — the relative-path
|
|
24
|
+
# crash (`Cannot find module …/cache/maude/cli/lib/preflight.mjs`) that the cache
|
|
25
|
+
# steps already avoid by calling on-PATH `maude`.
|
|
19
26
|
#
|
|
20
27
|
# Exit: 0 if all hard deps pass; 1 otherwise.
|
|
21
28
|
|
|
@@ -23,10 +30,14 @@ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
|
23
30
|
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
24
31
|
PKG_ROOT="$(cd "$PLUGIN_ROOT/../.." && pwd)"
|
|
25
32
|
|
|
26
|
-
|
|
27
|
-
#
|
|
28
|
-
#
|
|
29
|
-
|
|
30
|
-
cd "$PKG_ROOT" || exit 1
|
|
31
|
-
|
|
32
|
-
|
|
33
|
+
if [ -f "$PKG_ROOT/cli/lib/preflight.mjs" ]; then
|
|
34
|
+
# Sibling cli/ present (repo / npm package). The Node lib needs cwd = package
|
|
35
|
+
# root (it resolves `plugins/<plugin>/dependencies.json`). Subshell preserves
|
|
36
|
+
# the caller's cwd.
|
|
37
|
+
( cd "$PKG_ROOT" || exit 1; exec node "$PKG_ROOT/cli/lib/preflight.mjs" --plugin design "$@" )
|
|
38
|
+
elif command -v maude >/dev/null 2>&1; then
|
|
39
|
+
exec maude preflight --plugin design "$@"
|
|
40
|
+
else
|
|
41
|
+
echo "preflight: no cli/lib beside the plugin and 'maude' not on PATH — install maude (npm i -g @1agh/maude)." >&2
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|