@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
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
// Maude design-runtime .gitignore block — Phase 9 Task 9 (gitignore strategy).
|
|
2
|
+
//
|
|
3
|
+
// Single `full` strategy in v1.1 (DDR-056): canvases + their JSON snapshots
|
|
4
|
+
// stay in git (cold backup, PR-reviewable .html diffs, bootstrap-from-clone),
|
|
5
|
+
// while regenerable per-machine runtime state is ignored — even in linked mode.
|
|
6
|
+
//
|
|
7
|
+
// The rules live between `# maude:begin` / `# maude:end` markers so the writer
|
|
8
|
+
// is idempotent: re-running replaces the block in place (never duplicates), and
|
|
9
|
+
// anything the user authored outside the markers is untouched. `maude design
|
|
10
|
+
// unlink` deliberately leaves the block — the rules are harmless in solo mode
|
|
11
|
+
// (the ignored files just don't get created).
|
|
12
|
+
|
|
13
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
14
|
+
import { resolve } from 'node:path';
|
|
15
|
+
|
|
16
|
+
export const BEGIN_MARKER = '# maude:begin';
|
|
17
|
+
export const END_MARKER = '# maude:end';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The runtime paths Maude ignores under the design root, relative to repo root.
|
|
21
|
+
* `designRel` defaults to `.design` (the only design root in v1.1 defaults).
|
|
22
|
+
*/
|
|
23
|
+
export function buildBlock(designRel = '.design') {
|
|
24
|
+
const root = designRel.replace(/\/+$/, '');
|
|
25
|
+
const lines = [
|
|
26
|
+
BEGIN_MARKER,
|
|
27
|
+
'# Maude design plugin runtime — gitignored even in linked mode (DDR-056).',
|
|
28
|
+
`${root}/_state/`, // binary CRDT logs (regenerable from hub)
|
|
29
|
+
`${root}/_server.json`,
|
|
30
|
+
`${root}/_server.log`,
|
|
31
|
+
`${root}/_active.json`,
|
|
32
|
+
`${root}/_sync.json`, // linked-mode offline/sync status (Task 8)
|
|
33
|
+
`${root}/_history/`,
|
|
34
|
+
`${root}/_canvas-state/`, // per-machine canvas undo/redo + scratch state
|
|
35
|
+
`${root}/_chat/`, // ACP transcripts (per-machine)
|
|
36
|
+
END_MARKER,
|
|
37
|
+
];
|
|
38
|
+
return `${lines.join('\n')}\n`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** True when the file already contains a maude-managed block. */
|
|
42
|
+
export function hasBlock(contents) {
|
|
43
|
+
return contents.includes(BEGIN_MARKER) && contents.includes(END_MARKER);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Replace the existing maude block in `contents`, or append a fresh one.
|
|
48
|
+
* Pure — returns the new file text. Preserves everything outside the markers.
|
|
49
|
+
*/
|
|
50
|
+
export function applyBlock(contents, block) {
|
|
51
|
+
if (!hasBlock(contents)) {
|
|
52
|
+
// Append with a single blank-line separator (avoid leading newline on an
|
|
53
|
+
// empty file; avoid double-blank when the file already ends in a newline).
|
|
54
|
+
if (contents.length === 0) return block;
|
|
55
|
+
const sep = contents.endsWith('\n') ? '\n' : '\n\n';
|
|
56
|
+
return `${contents}${sep}${block}`;
|
|
57
|
+
}
|
|
58
|
+
const begin = contents.indexOf(BEGIN_MARKER);
|
|
59
|
+
const endIdx = contents.indexOf(END_MARKER);
|
|
60
|
+
const after = endIdx + END_MARKER.length;
|
|
61
|
+
// Consume one trailing newline after END so we don't accumulate blank lines.
|
|
62
|
+
const tail = contents.slice(after).replace(/^\n/, '');
|
|
63
|
+
const head = contents.slice(0, begin);
|
|
64
|
+
return `${head}${block}${tail ? `\n${tail}` : ''}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Write (or refresh) the maude block in `<repoRoot>/.gitignore`.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} repoRoot
|
|
71
|
+
* @param {{ designRel?: string, dryRun?: boolean }} [opts]
|
|
72
|
+
* @returns {{ action: 'created' | 'updated' | 'unchanged', path: string }}
|
|
73
|
+
*/
|
|
74
|
+
export function writeGitignoreBlock(repoRoot, { designRel = '.design', dryRun = false } = {}) {
|
|
75
|
+
const path = resolve(repoRoot, '.gitignore');
|
|
76
|
+
const existed = existsSync(path);
|
|
77
|
+
const current = existed ? readFileSync(path, 'utf8') : '';
|
|
78
|
+
const block = buildBlock(designRel);
|
|
79
|
+
const next = applyBlock(current, block);
|
|
80
|
+
|
|
81
|
+
let action;
|
|
82
|
+
if (!existed) action = 'created';
|
|
83
|
+
else if (next === current) action = 'unchanged';
|
|
84
|
+
else action = 'updated';
|
|
85
|
+
|
|
86
|
+
if (!dryRun && next !== current) {
|
|
87
|
+
writeFileSync(path, next, 'utf8');
|
|
88
|
+
}
|
|
89
|
+
return { action, path };
|
|
90
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// gitignore-block helper tests — Phase 9 Task 9 (DDR-056).
|
|
2
|
+
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
BEGIN_MARKER,
|
|
11
|
+
END_MARKER,
|
|
12
|
+
applyBlock,
|
|
13
|
+
buildBlock,
|
|
14
|
+
hasBlock,
|
|
15
|
+
writeGitignoreBlock,
|
|
16
|
+
} from './gitignore-block.mjs';
|
|
17
|
+
|
|
18
|
+
function withRepo(fn) {
|
|
19
|
+
const dir = mkdtempSync(join(tmpdir(), 'maude-gitignore-'));
|
|
20
|
+
try {
|
|
21
|
+
return fn(dir);
|
|
22
|
+
} finally {
|
|
23
|
+
rmSync(dir, { recursive: true, force: true });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('buildBlock includes the runtime paths and is marker-wrapped', () => {
|
|
28
|
+
const block = buildBlock('.design');
|
|
29
|
+
assert.ok(block.startsWith(BEGIN_MARKER));
|
|
30
|
+
assert.ok(block.trimEnd().endsWith(END_MARKER));
|
|
31
|
+
for (const p of [
|
|
32
|
+
'.design/_state/',
|
|
33
|
+
'.design/_server.json',
|
|
34
|
+
'.design/_active.json',
|
|
35
|
+
'.design/_sync.json',
|
|
36
|
+
'.design/_history/',
|
|
37
|
+
'.design/_chat/',
|
|
38
|
+
]) {
|
|
39
|
+
assert.ok(block.includes(p), `missing ${p}`);
|
|
40
|
+
}
|
|
41
|
+
// Committed artifacts must NOT be ignored.
|
|
42
|
+
assert.ok(!block.includes('.design/config.json'));
|
|
43
|
+
assert.ok(!/\.design\/\*\.html/.test(block));
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('buildBlock honors a custom design root', () => {
|
|
47
|
+
const block = buildBlock('design');
|
|
48
|
+
assert.ok(block.includes('design/_server.json'));
|
|
49
|
+
assert.ok(!block.includes('.design/_server.json'));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('writeGitignoreBlock creates .gitignore when absent', () => {
|
|
53
|
+
withRepo((dir) => {
|
|
54
|
+
const { action, path } = writeGitignoreBlock(dir);
|
|
55
|
+
assert.equal(action, 'created');
|
|
56
|
+
const text = readFileSync(path, 'utf8');
|
|
57
|
+
assert.ok(hasBlock(text));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('writeGitignoreBlock is idempotent — second run is unchanged', () => {
|
|
62
|
+
withRepo((dir) => {
|
|
63
|
+
writeGitignoreBlock(dir);
|
|
64
|
+
const first = readFileSync(join(dir, '.gitignore'), 'utf8');
|
|
65
|
+
const { action } = writeGitignoreBlock(dir);
|
|
66
|
+
const second = readFileSync(join(dir, '.gitignore'), 'utf8');
|
|
67
|
+
assert.equal(action, 'unchanged');
|
|
68
|
+
assert.equal(first, second);
|
|
69
|
+
// Exactly one block — no duplication.
|
|
70
|
+
assert.equal(first.split(BEGIN_MARKER).length - 1, 1);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test('writeGitignoreBlock preserves user content outside the markers', () => {
|
|
75
|
+
withRepo((dir) => {
|
|
76
|
+
const userContent = 'node_modules/\n*.log\n.env\n';
|
|
77
|
+
writeFileSync(join(dir, '.gitignore'), userContent);
|
|
78
|
+
writeGitignoreBlock(dir);
|
|
79
|
+
const text = readFileSync(join(dir, '.gitignore'), 'utf8');
|
|
80
|
+
assert.ok(text.includes('node_modules/'));
|
|
81
|
+
assert.ok(text.includes('.env'));
|
|
82
|
+
assert.ok(hasBlock(text));
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('writeGitignoreBlock updates an existing block in place (no duplication)', () => {
|
|
87
|
+
withRepo((dir) => {
|
|
88
|
+
// Seed a stale block with a different design root.
|
|
89
|
+
const stale = `keep-me\n${buildBlock('olddesign')}trailing\n`;
|
|
90
|
+
writeFileSync(join(dir, '.gitignore'), stale);
|
|
91
|
+
const { action } = writeGitignoreBlock(dir, { designRel: '.design' });
|
|
92
|
+
assert.equal(action, 'updated');
|
|
93
|
+
const text = readFileSync(join(dir, '.gitignore'), 'utf8');
|
|
94
|
+
assert.equal(text.split(BEGIN_MARKER).length - 1, 1, 'still exactly one block');
|
|
95
|
+
assert.ok(text.includes('.design/_server.json'));
|
|
96
|
+
assert.ok(!text.includes('olddesign/_server.json'));
|
|
97
|
+
assert.ok(text.includes('keep-me'));
|
|
98
|
+
assert.ok(text.includes('trailing'));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('writeGitignoreBlock --dry-run does not touch disk', () => {
|
|
103
|
+
withRepo((dir) => {
|
|
104
|
+
const { action } = writeGitignoreBlock(dir, { dryRun: true });
|
|
105
|
+
assert.equal(action, 'created');
|
|
106
|
+
// File was not actually written.
|
|
107
|
+
assert.throws(() => readFileSync(join(dir, '.gitignore'), 'utf8'));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('applyBlock appends to a file lacking a trailing newline cleanly', () => {
|
|
112
|
+
const out = applyBlock('a\nb', buildBlock('.design'));
|
|
113
|
+
assert.ok(out.includes('a\nb'));
|
|
114
|
+
assert.ok(hasBlock(out));
|
|
115
|
+
// No run-together: marker starts on its own line.
|
|
116
|
+
assert.ok(out.includes(`\n${BEGIN_MARKER}`));
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('hasBlock detects both markers', () => {
|
|
120
|
+
assert.ok(hasBlock(`x\n${BEGIN_MARKER}\ny\n${END_MARKER}\n`));
|
|
121
|
+
assert.ok(!hasBlock('just user content'));
|
|
122
|
+
assert.ok(!hasBlock(BEGIN_MARKER)); // begin without end
|
|
123
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// DDR-061 + DDR-062 regression guard.
|
|
2
|
+
//
|
|
3
|
+
// DDR-061: a plugin must NEVER invoke a `cli/lib/*.mjs` module by a relative
|
|
4
|
+
// path unconditionally: in a marketplace install the plugin is copied alone into
|
|
5
|
+
// `cache/<marketplace>/<plugin>/<version>/`, so the sibling `cli/` does not
|
|
6
|
+
// exist and the call crashes with MODULE_NOT_FOUND. The reachable contract is
|
|
7
|
+
// the on-PATH `maude` binary (a declared plugin dependency).
|
|
8
|
+
//
|
|
9
|
+
// So any plugin file that runs `node … cli/lib/<x>.mjs` is only allowed when the
|
|
10
|
+
// SAME file also offers a `command -v maude` fallback (the sibling-first /
|
|
11
|
+
// maude-fallback pattern). This test fails loudly if a new straggler lands.
|
|
12
|
+
//
|
|
13
|
+
// DDR-062: plugin MARKDOWN must reach the dev-server bash helpers through
|
|
14
|
+
// `maude design <verb>`, never `bash "$CLAUDE_PLUGIN_ROOT/dev-server/bin/<x>.sh"`.
|
|
15
|
+
// The marketplace never copies the dev-server beside a plugin, and a flow
|
|
16
|
+
// command's $CLAUDE_PLUGIN_ROOT points at plugins/flow (no dev-server/ at all).
|
|
17
|
+
// Prose mentions of the path are fine; INVOCATIONS (bash/sh/exec/$( prefixed)
|
|
18
|
+
// are banned.
|
|
19
|
+
|
|
20
|
+
import assert from 'node:assert/strict';
|
|
21
|
+
import { execSync } from 'node:child_process';
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
import { test } from 'node:test';
|
|
24
|
+
|
|
25
|
+
test('no plugin runs `node cli/lib/*.mjs` without an on-PATH maude fallback', () => {
|
|
26
|
+
const out = execSync(
|
|
27
|
+
String.raw`grep -rlE 'node[^\n]*cli/lib/[a-z0-9_-]+\.mjs' plugins/ || true`,
|
|
28
|
+
{
|
|
29
|
+
encoding: 'utf8',
|
|
30
|
+
}
|
|
31
|
+
);
|
|
32
|
+
const files = out.trim().split('\n').filter(Boolean);
|
|
33
|
+
const offenders = files.filter((f) => !/command -v maude/.test(readFileSync(f, 'utf8')));
|
|
34
|
+
assert.deepEqual(
|
|
35
|
+
offenders,
|
|
36
|
+
[],
|
|
37
|
+
`These plugin files invoke a relative cli/lib module with no \`command -v maude\` fallback — they will crash in a marketplace install (DDR-061). Route through \`maude <subcommand>\` or guard with a sibling-exists check + maude fallback:\n ${offenders.join('\n ')}`
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('no plugin markdown invokes a $CLAUDE_PLUGIN_ROOT/dev-server/bin/*.sh helper (DDR-062)', () => {
|
|
42
|
+
// INVOCATION = the bin path preceded by bash / sh / exec, or inside a $(…)
|
|
43
|
+
// command substitution. Prose mentions of the path (backtick-wrapped, no verb)
|
|
44
|
+
// are exempt — they don't run anything. Matches both the $CLAUDE_PLUGIN_ROOT
|
|
45
|
+
// form and the repo-relative `plugins/<x>/dev-server/bin/` form.
|
|
46
|
+
const pattern = String.raw`(bash|sh|exec) +"?(\$\{?CLAUDE_PLUGIN_ROOT\}?|plugins/[a-z]+)/dev-server/bin/[A-Za-z0-9_-]+\.sh|\$\("?\$?\{?CLAUDE_PLUGIN_ROOT\}?/dev-server/bin/[A-Za-z0-9_-]+\.sh`;
|
|
47
|
+
const out = execSync(`grep -rnE '${pattern}' plugins --include='*.md' || true`, {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
});
|
|
50
|
+
const hits = out.trim().split('\n').filter(Boolean);
|
|
51
|
+
assert.deepEqual(
|
|
52
|
+
hits,
|
|
53
|
+
[],
|
|
54
|
+
`These plugin markdown lines INVOKE a dev-server bin helper directly — they break in marketplace installs (the dev-server is never copied beside a plugin) and in flow commands (whose $CLAUDE_PLUGIN_ROOT has no dev-server/). Route through \`maude design <verb>\` instead (DDR-062):\n ${hits.join('\n ')}`
|
|
55
|
+
);
|
|
56
|
+
});
|
package/cli/lib/preflight.mjs
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
// because every consumer wants the same table format.
|
|
13
13
|
|
|
14
14
|
import { spawnSync } from 'node:child_process';
|
|
15
|
-
import { readFile } from 'node:fs/promises';
|
|
15
|
+
import { readFile, writeFile } from 'node:fs/promises';
|
|
16
16
|
import { resolve } from 'node:path';
|
|
17
17
|
|
|
18
18
|
const SEM_RE = /(\d+)\.(\d+)\.(\d+)/;
|
|
@@ -172,10 +172,11 @@ function colorFor(g) {
|
|
|
172
172
|
// ─── CLI entry — `node cli/lib/preflight.mjs --plugin design --json` ──────
|
|
173
173
|
|
|
174
174
|
function parseFlags(args) {
|
|
175
|
-
const out = { plugin: null, mode: 'text' };
|
|
175
|
+
const out = { plugin: null, mode: 'text', cache: null };
|
|
176
176
|
for (let i = 0; i < args.length; i++) {
|
|
177
177
|
const a = args[i];
|
|
178
178
|
if (a === '--plugin') out.plugin = args[++i];
|
|
179
|
+
else if (a === '--cache') out.cache = args[++i];
|
|
179
180
|
else if (a === '--json') out.mode = 'json';
|
|
180
181
|
else if (a === '--shell-export') out.mode = 'shell-export';
|
|
181
182
|
else if (a === '--quiet') out.mode = 'quiet';
|
|
@@ -184,17 +185,46 @@ function parseFlags(args) {
|
|
|
184
185
|
return out;
|
|
185
186
|
}
|
|
186
187
|
|
|
187
|
-
|
|
188
|
-
|
|
188
|
+
// Cross-command short-circuit cache (Phase A, Task A15). Written after a
|
|
189
|
+
// successful check so sibling commands within the freshness window skip the
|
|
190
|
+
// whole preflight. Path is caller-supplied (design → <designRoot>/_preflight.json,
|
|
191
|
+
// flow → .ai/state/_preflight.json) because this lib runs with cwd = the maude
|
|
192
|
+
// package root when shelled from preflight.sh, not the target repo.
|
|
193
|
+
async function writeCache(cachePath, env) {
|
|
194
|
+
const payload = {
|
|
195
|
+
checked: new Date().toISOString(),
|
|
196
|
+
plugin: env.plugin,
|
|
197
|
+
all_hard_pass: env.summary.allHardPass,
|
|
198
|
+
soft_warnings: env.results
|
|
199
|
+
.filter((r) => r.hardness === 'soft' && r.status === 'missing')
|
|
200
|
+
.map((r) => r.id),
|
|
201
|
+
};
|
|
202
|
+
try {
|
|
203
|
+
await writeFile(resolve(cachePath), `${JSON.stringify(payload, null, 2)}\n`);
|
|
204
|
+
} catch {
|
|
205
|
+
// Cache is a best-effort optimization; a write failure (read-only FS,
|
|
206
|
+
// missing parent dir) must never break the preflight itself.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function manifestPathForPlugin(pluginName, pkgRoot = process.cwd()) {
|
|
211
|
+
return resolve(pkgRoot, 'plugins', pluginName, 'dependencies.json');
|
|
189
212
|
}
|
|
190
213
|
|
|
191
|
-
|
|
192
|
-
|
|
214
|
+
// Shared runnable for both the `maude preflight` command and the direct
|
|
215
|
+
// `node cli/lib/preflight.mjs` entry. `pkgRoot` is where `plugins/<x>/
|
|
216
|
+
// dependencies.json` lives (the maude package root) — resolved independently
|
|
217
|
+
// of the caller's cwd, so the command works from any target repo. (DDR-061:
|
|
218
|
+
// the marketplace install has no sibling cli/, so callers reach this through
|
|
219
|
+
// the on-PATH `maude` binary, which passes its own package root as pkgRoot.)
|
|
220
|
+
export async function runPreflight({ args, pkgRoot = process.cwd() }) {
|
|
221
|
+
const flags = parseFlags(args);
|
|
193
222
|
if (!flags.plugin) {
|
|
194
|
-
process.stderr.write('preflight
|
|
223
|
+
process.stderr.write('preflight: pass --plugin <design|flow>\n');
|
|
195
224
|
process.exit(2);
|
|
196
225
|
}
|
|
197
|
-
const env = await checkAll(manifestPathForPlugin(flags.plugin));
|
|
226
|
+
const env = await checkAll(manifestPathForPlugin(flags.plugin, pkgRoot));
|
|
227
|
+
if (flags.cache) await writeCache(flags.cache, env);
|
|
198
228
|
if (flags.mode === 'json') {
|
|
199
229
|
process.stdout.write(`${JSON.stringify(env, null, 2)}\n`);
|
|
200
230
|
} else if (flags.mode === 'shell-export') {
|
|
@@ -223,9 +253,10 @@ async function main(argv) {
|
|
|
223
253
|
process.exit(env.summary.allHardPass ? 0 : 1);
|
|
224
254
|
}
|
|
225
255
|
|
|
226
|
-
// Entry guard — only run
|
|
256
|
+
// Entry guard — only run when invoked directly (`node cli/lib/preflight.mjs`).
|
|
257
|
+
// The package root is the cwd here (preflight.sh `cd`s to it before exec).
|
|
227
258
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
228
|
-
|
|
259
|
+
runPreflight({ args: process.argv.slice(2), pkgRoot: process.cwd() }).catch((err) => {
|
|
229
260
|
process.stderr.write(`preflight: ${err.message}\n`);
|
|
230
261
|
process.exit(1);
|
|
231
262
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -41,13 +41,13 @@
|
|
|
41
41
|
"prepublishOnly": "bash scripts/check-version-parity.sh && bash plugins/design/dev-server/bin/check-runtime-bundles.sh"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@1agh/maude-darwin-arm64": "0.
|
|
45
|
-
"@1agh/maude-darwin-x64": "0.
|
|
46
|
-
"@1agh/maude-linux-arm64": "0.
|
|
47
|
-
"@1agh/maude-linux-arm64-musl": "0.
|
|
48
|
-
"@1agh/maude-linux-x64": "0.
|
|
49
|
-
"@1agh/maude-linux-x64-musl": "0.
|
|
50
|
-
"@1agh/maude-win32-x64": "0.
|
|
44
|
+
"@1agh/maude-darwin-arm64": "0.25.0",
|
|
45
|
+
"@1agh/maude-darwin-x64": "0.25.0",
|
|
46
|
+
"@1agh/maude-linux-arm64": "0.25.0",
|
|
47
|
+
"@1agh/maude-linux-arm64-musl": "0.25.0",
|
|
48
|
+
"@1agh/maude-linux-x64": "0.25.0",
|
|
49
|
+
"@1agh/maude-linux-x64-musl": "0.25.0",
|
|
50
|
+
"@1agh/maude-win32-x64": "0.25.0"
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
53
|
"cli",
|
|
@@ -94,10 +94,38 @@
|
|
|
94
94
|
"preferred": "npm i -g playwright && npx playwright install chromium"
|
|
95
95
|
},
|
|
96
96
|
"autoInstall": false,
|
|
97
|
-
"fallbackBehavior": "Screenshot scripts use playwright as a fallback when agent-browser is missing.
|
|
98
|
-
"usedBy": [
|
|
97
|
+
"fallbackBehavior": "Screenshot scripts use playwright as a fallback when agent-browser is missing. The rendered export formats (PNG/PDF/SVG/HTML/PPTX/Canva) require it outright. NOTE: the `--version` check above only proves the npm package resolves — the Chromium BINARY is a separate install (`npx playwright install chromium`). If it's missing, exports fail at runtime with an actionable message from dev-server/bin/_pw-launch.mjs; screenshot helpers fall back to agent-browser.",
|
|
98
|
+
"usedBy": [
|
|
99
|
+
"dev-server/bin/_pw-launch.mjs",
|
|
100
|
+
"dev-server/bin/_screenshot-playwright.mjs",
|
|
101
|
+
"dev-server/bin/_png-playwright.mjs",
|
|
102
|
+
"dev-server/bin/_pdf-playwright.mjs",
|
|
103
|
+
"dev-server/bin/_svg-playwright.mjs",
|
|
104
|
+
"dev-server/bin/_html-playwright.mjs",
|
|
105
|
+
"dev-server/bin/_pptx-playwright.mjs",
|
|
106
|
+
"dev-server/bin/_enumerate-artboards-playwright.mjs",
|
|
107
|
+
"dev-server/bin/screenshot.sh"
|
|
108
|
+
],
|
|
99
109
|
"docsUrl": "https://playwright.dev"
|
|
100
110
|
},
|
|
111
|
+
{
|
|
112
|
+
"id": "svg2pptx",
|
|
113
|
+
"type": "cli",
|
|
114
|
+
"hardness": "soft",
|
|
115
|
+
"check": {
|
|
116
|
+
"command": "svg2pptx --version",
|
|
117
|
+
"expectExit": 0
|
|
118
|
+
},
|
|
119
|
+
"install": {
|
|
120
|
+
"preferred": "pip install svg2pptx",
|
|
121
|
+
"darwin": "pipx install svg2pptx || python3 -m pip install svg2pptx",
|
|
122
|
+
"linux": "pipx install svg2pptx || python3 -m pip install svg2pptx"
|
|
123
|
+
},
|
|
124
|
+
"autoInstall": false,
|
|
125
|
+
"fallbackBehavior": "PPTX/Canva export uses svg2pptx (Python) to emit NATIVE, editable PowerPoint shapes + text boxes from the artboard SVG — editable in PowerPoint / Keynote / Canva. Without it (or python3), export falls back to a non-editable PNG-per-slide deck (faithful, universal, Canva-safe). Set MAUDE_SVG2PPTX to a custom svg2pptx executable (e.g. a venv path, or `python3 -m svg2pptx`) when it isn't on PATH. See DDR-069.",
|
|
126
|
+
"usedBy": ["dev-server/exporters/pptx.ts"],
|
|
127
|
+
"docsUrl": "https://github.com/benouinirachid/svg2pptx"
|
|
128
|
+
},
|
|
101
129
|
{
|
|
102
130
|
"id": "maude",
|
|
103
131
|
"type": "cli",
|
|
@@ -41,8 +41,8 @@ const BANNER_CSS = `
|
|
|
41
41
|
align-items: center;
|
|
42
42
|
gap: 10px;
|
|
43
43
|
background: var(--maude-hud-accent-tint, oklch(92% 0.040 55));
|
|
44
|
-
color: var(--fg-0, #111);
|
|
45
|
-
border: 1px solid var(--border
|
|
44
|
+
color: var(--maude-chrome-fg-0, #111);
|
|
45
|
+
border: 1px solid var(--maude-chrome-border, rgba(0,0,0,0.16));
|
|
46
46
|
border-radius: var(--radius-md, 4px);
|
|
47
47
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
|
48
48
|
font-family: var(--font-sans, system-ui, -apple-system, sans-serif);
|