@1agh/maude 0.22.2 → 0.24.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.
Files changed (125) hide show
  1. package/README.md +34 -1
  2. package/cli/bin/maude.mjs +3 -0
  3. package/cli/commands/cache.mjs +181 -0
  4. package/cli/commands/cache.test.mjs +166 -0
  5. package/cli/commands/design-link.test.mjs +84 -2
  6. package/cli/commands/design.mjs +95 -4
  7. package/cli/commands/design.test.mjs +56 -0
  8. package/cli/commands/help.mjs +34 -0
  9. package/cli/commands/hub.mjs +255 -30
  10. package/cli/commands/hub.test.mjs +135 -10
  11. package/cli/commands/init.mjs +3 -0
  12. package/cli/commands/preflight.mjs +11 -0
  13. package/cli/commands/preflight.test.mjs +46 -0
  14. package/cli/commands/scenario-report.mjs +45 -0
  15. package/cli/lib/cache.mjs +407 -0
  16. package/cli/lib/cache.test.mjs +303 -0
  17. package/cli/lib/design-link.mjs +222 -11
  18. package/cli/lib/flow-design-integration.test.mjs +165 -0
  19. package/cli/lib/gitignore-block.mjs +90 -0
  20. package/cli/lib/gitignore-block.test.mjs +123 -0
  21. package/cli/lib/hubs-config.mjs +42 -4
  22. package/cli/lib/plugin-cli-reachability.test.mjs +56 -0
  23. package/cli/lib/preflight.mjs +41 -10
  24. package/package.json +8 -8
  25. package/plugins/design/dev-server/ai-banner.tsx +2 -2
  26. package/plugins/design/dev-server/annotations-context-toolbar.tsx +349 -112
  27. package/plugins/design/dev-server/annotations-layer.tsx +906 -137
  28. package/plugins/design/dev-server/api.ts +109 -4
  29. package/plugins/design/dev-server/bin/preflight.sh +21 -10
  30. package/plugins/design/dev-server/bin/prep.sh +211 -0
  31. package/plugins/design/dev-server/bin/scenario-report.mjs +209 -0
  32. package/plugins/design/dev-server/bin/screenshot.sh +18 -1
  33. package/plugins/design/dev-server/bin/smoke.sh +94 -11
  34. package/plugins/design/dev-server/build.ts +69 -3
  35. package/plugins/design/dev-server/canvas-comment-mount.tsx +384 -0
  36. package/plugins/design/dev-server/canvas-cursors.ts +125 -0
  37. package/plugins/design/dev-server/canvas-icons.tsx +130 -0
  38. package/plugins/design/dev-server/canvas-lib.tsx +47 -27
  39. package/plugins/design/dev-server/canvas-meta.schema.json +20 -0
  40. package/plugins/design/dev-server/canvas-shell.tsx +358 -245
  41. package/plugins/design/dev-server/client/app.jsx +191 -21
  42. package/plugins/design/dev-server/client/styles/3-shell.css +10 -0
  43. package/plugins/design/dev-server/collab/awareness-bridge.ts +77 -0
  44. package/plugins/design/dev-server/collab/index.ts +87 -9
  45. package/plugins/design/dev-server/collab/persistence.ts +34 -3
  46. package/plugins/design/dev-server/collab/registry.ts +131 -2
  47. package/plugins/design/dev-server/collab/room.ts +21 -8
  48. package/plugins/design/dev-server/config.schema.json +20 -0
  49. package/plugins/design/dev-server/context-menu.tsx +167 -23
  50. package/plugins/design/dev-server/context.ts +31 -0
  51. package/plugins/design/dev-server/contextual-toolbar.tsx +7 -7
  52. package/plugins/design/dev-server/dist/client.bundle.js +144 -22
  53. package/plugins/design/dev-server/dist/comment-mount.js +1868 -0
  54. package/plugins/design/dev-server/dist/styles.css +16 -0
  55. package/plugins/design/dev-server/dom-selection.ts +156 -0
  56. package/plugins/design/dev-server/equal-spacing-handles.tsx +1 -1
  57. package/plugins/design/dev-server/export-dialog.tsx +19 -17
  58. package/plugins/design/dev-server/fs-watch.ts +1 -0
  59. package/plugins/design/dev-server/hmr-broadcast.ts +51 -20
  60. package/plugins/design/dev-server/http.ts +260 -20
  61. package/plugins/design/dev-server/input-router.tsx +125 -64
  62. package/plugins/design/dev-server/participants-chrome.tsx +10 -10
  63. package/plugins/design/dev-server/server.ts +141 -1
  64. package/plugins/design/dev-server/sync/agent.ts +418 -0
  65. package/plugins/design/dev-server/sync/atomic-write.ts +103 -0
  66. package/plugins/design/dev-server/sync/codec.ts +324 -0
  67. package/plugins/design/dev-server/sync/connection-state.ts +203 -0
  68. package/plugins/design/dev-server/sync/echo-guard.ts +108 -0
  69. package/plugins/design/dev-server/sync/fs-mirror.ts +160 -0
  70. package/plugins/design/dev-server/sync/hubs-config.ts +87 -0
  71. package/plugins/design/dev-server/sync/index.ts +918 -0
  72. package/plugins/design/dev-server/sync/materialize.ts +62 -0
  73. package/plugins/design/dev-server/sync/migrate-seed.ts +163 -0
  74. package/plugins/design/dev-server/sync/origins.ts +57 -0
  75. package/plugins/design/dev-server/sync/projection.ts +368 -0
  76. package/plugins/design/dev-server/sync/status.ts +115 -0
  77. package/plugins/design/dev-server/sync/untrusted.ts +153 -0
  78. package/plugins/design/dev-server/test/_helpers.ts +6 -2
  79. package/plugins/design/dev-server/test/annotations-layer.test.ts +231 -0
  80. package/plugins/design/dev-server/test/annotations-roundtrip.test.ts +276 -0
  81. package/plugins/design/dev-server/test/canvas-cursors.test.ts +73 -0
  82. package/plugins/design/dev-server/test/canvas-origin-gate.test.ts +76 -0
  83. package/plugins/design/dev-server/test/collab-awareness-bridge.test.ts +223 -0
  84. package/plugins/design/dev-server/test/collab-reseed-guard.test.ts +78 -0
  85. package/plugins/design/dev-server/test/collab-room.test.ts +21 -10
  86. package/plugins/design/dev-server/test/comment-mount.test.ts +87 -0
  87. package/plugins/design/dev-server/test/csp-canvas-shell.test.ts +46 -0
  88. package/plugins/design/dev-server/test/fixtures/phase-20-annotations.svg +1 -0
  89. package/plugins/design/dev-server/test/hmr-classify.test.ts +70 -0
  90. package/plugins/design/dev-server/test/input-router.test.ts +21 -0
  91. package/plugins/design/dev-server/test/sanitize-annotation-svg.test.ts +100 -0
  92. package/plugins/design/dev-server/test/shared-doc-convergence.test.ts +265 -0
  93. package/plugins/design/dev-server/test/shared-doc-foundation.test.ts +63 -0
  94. package/plugins/design/dev-server/test/shared-doc-migrate.test.ts +160 -0
  95. package/plugins/design/dev-server/test/shared-doc-projection.test.ts +238 -0
  96. package/plugins/design/dev-server/test/sync-agent.test.ts +278 -0
  97. package/plugins/design/dev-server/test/sync-atomic-write.test.ts +63 -0
  98. package/plugins/design/dev-server/test/sync-codec.test.ts +165 -0
  99. package/plugins/design/dev-server/test/sync-connection-state.test.ts +146 -0
  100. package/plugins/design/dev-server/test/sync-echo-guard.test.ts +96 -0
  101. package/plugins/design/dev-server/test/sync-fs-mirror.test.ts +182 -0
  102. package/plugins/design/dev-server/test/sync-hardening.test.ts +520 -0
  103. package/plugins/design/dev-server/test/sync-hubs-config.test.ts +130 -0
  104. package/plugins/design/dev-server/test/sync-meta-codec.test.ts +123 -0
  105. package/plugins/design/dev-server/test/sync-runtime.test.ts +812 -0
  106. package/plugins/design/dev-server/test/sync-status.test.ts +80 -0
  107. package/plugins/design/dev-server/test/sync-untrusted.test.ts +104 -0
  108. package/plugins/design/dev-server/test/use-collab.test.ts +0 -0
  109. package/plugins/design/dev-server/test/use-tool-mode.test.tsx +38 -10
  110. package/plugins/design/dev-server/tool-palette.tsx +18 -16
  111. package/plugins/design/dev-server/undo-hud.tsx +4 -4
  112. package/plugins/design/dev-server/undo-stack.ts +20 -4
  113. package/plugins/design/dev-server/use-annotation-resize.tsx +16 -5
  114. package/plugins/design/dev-server/use-collab.tsx +157 -13
  115. package/plugins/design/dev-server/use-selection-set.tsx +12 -0
  116. package/plugins/design/dev-server/use-tool-mode.tsx +27 -12
  117. package/plugins/design/dev-server/ws.ts +40 -1
  118. package/plugins/design/templates/_shell.html +63 -5
  119. package/plugins/design/templates/canvas.tsx.template +13 -0
  120. package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +15 -7
  121. package/plugins/flow/.claude-plugin/config.schema.json +5 -0
  122. package/plugins/flow/templates/ai-skeleton/README.md +1 -1
  123. package/plugins/flow/templates/ai-skeleton/gitignore +10 -0
  124. package/plugins/flow/templates/ai-skeleton/scenarios/README.md +3 -1
  125. package/plugins/flow/templates/ai-skeleton/workflows.config.json +2 -1
@@ -3,12 +3,14 @@
3
3
 
4
4
  import assert from 'node:assert/strict';
5
5
  import { spawnSync } from 'node:child_process';
6
- import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
6
+ import { existsSync, mkdtempSync, readFileSync, rmSync } from 'node:fs';
7
7
  import { tmpdir } from 'node:os';
8
8
  import { dirname, join, resolve } from 'node:path';
9
9
  import { test } from 'node:test';
10
10
  import { fileURLToPath } from 'node:url';
11
11
 
12
+ import { readTokens } from '../../plugins/design/hub/src/tokens.mjs';
13
+
12
14
  const BIN = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'maude.mjs');
13
15
 
14
16
  function runCli(args, { cwd } = {}) {
@@ -30,8 +32,10 @@ function withDataDir(fn) {
30
32
  test('hub help prints subcommand summary on stdout', () => {
31
33
  const res = runCli(['hub', 'help']);
32
34
  assert.equal(res.status, 0, res.stderr);
33
- assert.match(res.stdout, /maude hub <serve\|token\|status>/);
35
+ assert.match(res.stdout, /maude hub <serve\|token\|status\|deploy>/);
34
36
  assert.match(res.stdout, /token generate --label NAME/);
37
+ assert.match(res.stdout, /token rotate --label NAME/);
38
+ assert.match(res.stdout, /deploy <fly\|docker>/);
35
39
  });
36
40
 
37
41
  test('hub token generate without --label exits 2', () => {
@@ -40,7 +44,7 @@ test('hub token generate without --label exits 2', () => {
40
44
  assert.match(res.stderr, /--label <name> is required/);
41
45
  });
42
46
 
43
- test('hub token generate writes tokens.json + prints the connect command', () => {
47
+ test('hub token generate persists to the store + prints the connect command', () => {
44
48
  withDataDir((dataDir) => {
45
49
  const res = runCli(['hub', 'token', 'generate', '--label', 'alice', '--data', dataDir]);
46
50
  assert.equal(res.status, 0, res.stderr);
@@ -48,11 +52,11 @@ test('hub token generate writes tokens.json + prints the connect command', () =>
48
52
  assert.match(res.stdout, /value:\s+mau_[0-9a-f]{32}/);
49
53
  assert.match(res.stdout, /maude design link/);
50
54
 
51
- const raw = readFileSync(join(dataDir, 'tokens.json'), 'utf8');
52
- const parsed = JSON.parse(raw);
53
- assert.equal(parsed.tokens.length, 1);
54
- assert.equal(parsed.tokens[0].label, 'alice');
55
- assert.match(parsed.tokens[0].value, /^mau_[0-9a-f]{32}$/);
55
+ const { tokens } = readTokens(dataDir);
56
+ assert.equal(tokens.length, 1);
57
+ assert.equal(tokens[0].label, 'alice');
58
+ // The raw value is never persisted — only labels/metadata are listable.
59
+ assert.equal(tokens[0].value, undefined);
56
60
  });
57
61
  });
58
62
 
@@ -61,8 +65,7 @@ test('hub token generate --label is idempotent (overwrites in place)', () => {
61
65
  runCli(['hub', 'token', 'generate', '--label', 'alice', '--data', dataDir]);
62
66
  runCli(['hub', 'token', 'generate', '--label', 'alice', '--data', dataDir]);
63
67
 
64
- const parsed = JSON.parse(readFileSync(join(dataDir, 'tokens.json'), 'utf8'));
65
- assert.equal(parsed.tokens.length, 1);
68
+ assert.equal(readTokens(dataDir).tokens.length, 1);
66
69
  });
67
70
  });
68
71
 
@@ -74,6 +77,128 @@ test('hub token generate --dev uses mau_dev_ prefix', () => {
74
77
  });
75
78
  });
76
79
 
80
+ test("hub token generate --scope '*' mints a hub-wide (wildcard) token", () => {
81
+ withDataDir((dataDir) => {
82
+ // Array-arg spawn (no shell) → '*' is passed literally, not glob-expanded.
83
+ const res = runCli([
84
+ 'hub',
85
+ 'token',
86
+ 'generate',
87
+ '--label',
88
+ 'peer',
89
+ '--scope',
90
+ '*',
91
+ '--data',
92
+ dataDir,
93
+ ]);
94
+ assert.equal(res.status, 0, res.stderr);
95
+ assert.match(res.stdout, /scope:\s+\*/);
96
+ const { tokens } = readTokens(dataDir);
97
+ assert.equal(tokens.length, 1);
98
+ // Wildcard is stored as NULL and surfaced as '*' — authorizes any documentName.
99
+ assert.equal(tokens[0].scope, '*');
100
+ });
101
+ });
102
+
103
+ test('hub token generate --scope <value> binds the token to that documentName prefix', () => {
104
+ withDataDir((dataDir) => {
105
+ const res = runCli([
106
+ 'hub',
107
+ 'token',
108
+ 'generate',
109
+ '--label',
110
+ 'peer',
111
+ '--scope',
112
+ 'team-x',
113
+ '--data',
114
+ dataDir,
115
+ ]);
116
+ assert.equal(res.status, 0, res.stderr);
117
+ assert.match(res.stdout, /scope:\s+team-x/);
118
+ assert.equal(readTokens(dataDir).tokens[0].scope, 'team-x');
119
+ });
120
+ });
121
+
122
+ test('hub token generate without --scope defaults the scope to the label (DDR-053)', () => {
123
+ withDataDir((dataDir) => {
124
+ runCli(['hub', 'token', 'generate', '--label', 'alice', '--data', dataDir]);
125
+ assert.equal(readTokens(dataDir).tokens[0].scope, 'alice');
126
+ });
127
+ });
128
+
129
+ test('hub token rotate mints a fresh value for an existing label', () => {
130
+ withDataDir((dataDir) => {
131
+ const gen = runCli(['hub', 'token', 'generate', '--label', 'alice', '--data', dataDir]);
132
+ const firstValue = /value:\s+(mau_[0-9a-f]{32})/.exec(gen.stdout)?.[1];
133
+ assert.ok(firstValue, 'generate printed a value');
134
+
135
+ const rot = runCli(['hub', 'token', 'rotate', '--label', 'alice', '--data', dataDir]);
136
+ assert.equal(rot.status, 0, rot.stderr);
137
+ const rotatedValue = /value:\s+(mau_[0-9a-f]{32})/.exec(rot.stdout)?.[1];
138
+ assert.ok(rotatedValue, 'rotate printed a value');
139
+ assert.notEqual(rotatedValue, firstValue, 'rotate changed the value');
140
+ assert.match(rot.stdout, /rotated in/);
141
+ // Still exactly one token under that label.
142
+ assert.equal(readTokens(dataDir).tokens.length, 1);
143
+ });
144
+ });
145
+
146
+ test('hub token rotate on a missing label exits 1', () => {
147
+ withDataDir((dataDir) => {
148
+ const res = runCli(['hub', 'token', 'rotate', '--label', 'ghost', '--data', dataDir]);
149
+ assert.equal(res.status, 1);
150
+ assert.match(res.stderr, /no token with label "ghost"/);
151
+ });
152
+ });
153
+
154
+ test('hub deploy fly emits fly.toml + Dockerfile with substituted placeholders', () => {
155
+ withDataDir((outDir) => {
156
+ const res = runCli([
157
+ 'hub',
158
+ 'deploy',
159
+ 'fly',
160
+ '--name',
161
+ 'maude-hub-test',
162
+ '--region',
163
+ 'fra',
164
+ '--out',
165
+ outDir,
166
+ ]);
167
+ assert.equal(res.status, 0, res.stderr);
168
+ const flyToml = readFileSync(join(outDir, 'fly.toml'), 'utf8');
169
+ assert.match(flyToml, /app = "maude-hub-test"/);
170
+ assert.match(flyToml, /primary_region = "fra"/);
171
+ assert.doesNotMatch(flyToml, /\{\{/, 'no unsubstituted placeholders');
172
+ assert.ok(existsSync(join(outDir, 'Dockerfile')), 'Dockerfile copied alongside');
173
+ });
174
+ });
175
+
176
+ test('hub deploy docker emits compose + Caddyfile with the image tag', () => {
177
+ withDataDir((outDir) => {
178
+ const res = runCli(['hub', 'deploy', 'docker', '--tag', 'v9.9.9', '--out', outDir]);
179
+ assert.equal(res.status, 0, res.stderr);
180
+ const compose = readFileSync(join(outDir, 'docker-compose.yml'), 'utf8');
181
+ assert.match(compose, /ghcr\.io\/1agh\/maude-hub:v9\.9\.9/);
182
+ assert.doesNotMatch(compose, /\{\{/, 'no unsubstituted placeholders');
183
+ assert.ok(existsSync(join(outDir, 'Caddyfile')), 'Caddyfile emitted');
184
+ });
185
+ });
186
+
187
+ test('hub deploy refuses to overwrite without --force', () => {
188
+ withDataDir((outDir) => {
189
+ runCli(['hub', 'deploy', 'docker', '--out', outDir]);
190
+ const res = runCli(['hub', 'deploy', 'docker', '--out', outDir]);
191
+ assert.equal(res.status, 1);
192
+ assert.match(res.stderr, /already exists.*--force/s);
193
+ });
194
+ });
195
+
196
+ test('hub deploy with an unknown target exits 2', () => {
197
+ const res = runCli(['hub', 'deploy', 'kubernetes']);
198
+ assert.equal(res.status, 2);
199
+ assert.match(res.stderr, /target must be one of/);
200
+ });
201
+
77
202
  test('hub status against an unreachable URL exits 1 with diagnostic', () => {
78
203
  const res = runCli(['hub', 'status', 'http://127.0.0.1:1']);
79
204
  assert.equal(res.status, 1);
@@ -74,6 +74,9 @@ export async function run({ args, pkgRoot }) {
74
74
  const result = await copyTree(skeleton, aiDir, {
75
75
  force: !!flags.force,
76
76
  dryRun: !!flags['dry-run'],
77
+ // npm strips `.gitignore` from published tarballs, so the cache-ignore
78
+ // template ships as `gitignore` and is renamed to `.gitignore` on copy.
79
+ rename: (name) => (name === 'gitignore' ? '.gitignore' : name),
77
80
  transformMatch: (p) => TEMPLATED.some((t) => p.endsWith(t)),
78
81
  transform: ({ srcPath, contents }) => {
79
82
  let out = contents.replaceAll(PLACEHOLDER, projectName);
@@ -0,0 +1,11 @@
1
+ import { runPreflight } from '../lib/preflight.mjs';
2
+
3
+ // `maude preflight --plugin <design|flow> [--json|--shell-export|--warn-only|--quiet] [--cache PATH]`
4
+ // Thin command over cli/lib/preflight.mjs. Plugin bin scripts + hooks call this
5
+ // instead of shelling to a relative `$PKG_ROOT/cli/lib/preflight.mjs` — in a
6
+ // marketplace install the plugin is copied alone (no sibling cli/), so the
7
+ // only reachable entry is the on-PATH `maude` binary. See DDR-061.
8
+
9
+ export async function run({ args, pkgRoot }) {
10
+ return runPreflight({ args, pkgRoot });
11
+ }
@@ -0,0 +1,46 @@
1
+ // `maude preflight` resolves the plugin manifest from the maude PACKAGE root,
2
+ // never the caller's cwd. This is the DDR-061 regression: in a marketplace
3
+ // install the plugin is copied alone (no sibling cli/), and bin scripts reach
4
+ // the check through the on-PATH `maude` binary — invoked from an arbitrary
5
+ // target-repo cwd. Spawning from a temp cwd that has no `plugins/` proves the
6
+ // resolution doesn't depend on cwd.
7
+
8
+ import assert from 'node:assert/strict';
9
+ import { spawnSync } from 'node:child_process';
10
+ import { mkdtempSync, rmSync } from 'node:fs';
11
+ import { tmpdir } from 'node:os';
12
+ import { dirname, join, resolve } from 'node:path';
13
+ import { test } from 'node:test';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const BIN = resolve(dirname(fileURLToPath(import.meta.url)), '..', 'bin', 'maude.mjs');
17
+
18
+ function runFromTmp(args) {
19
+ const cwd = mkdtempSync(join(tmpdir(), 'maude-preflight-'));
20
+ try {
21
+ return spawnSync(process.execPath, [BIN, 'preflight', ...args], { cwd, encoding: 'utf8' });
22
+ } finally {
23
+ rmSync(cwd, { recursive: true, force: true });
24
+ }
25
+ }
26
+
27
+ test('preflight resolves the manifest from pkgRoot, not cwd (marketplace-layout regression)', () => {
28
+ // cwd has no plugins/ — if resolution used cwd this would error "manifest not found".
29
+ const res = runFromTmp(['--plugin', 'design', '--shell-export']);
30
+ assert.equal(res.status === 0 || res.status === 1, true, res.stderr); // 0/1 = ran; 2 = bad args
31
+ assert.match(res.stdout, /DEPS_PLUGIN="design"/);
32
+ });
33
+
34
+ test('preflight --json emits a structured envelope for the named plugin', () => {
35
+ const res = runFromTmp(['--plugin', 'flow', '--json']);
36
+ const env = JSON.parse(res.stdout);
37
+ assert.equal(env.plugin, 'flow');
38
+ assert.ok(Array.isArray(env.results));
39
+ assert.ok(env.summary && typeof env.summary.total === 'number');
40
+ });
41
+
42
+ test('preflight without --plugin exits 2', () => {
43
+ const res = runFromTmp([]);
44
+ assert.equal(res.status, 2);
45
+ assert.match(res.stderr, /--plugin/);
46
+ });
@@ -0,0 +1,45 @@
1
+ import { writeFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { pathToFileURL } from 'node:url';
4
+
5
+ // `maude scenario-report <run-dir> [--out <path>]` — deterministic cross-platform
6
+ // scenario report generator (Phase C / DDR-061). Thin wrapper that resolves the
7
+ // bundled generator from maude's OWN package root (NOT $CLAUDE_PLUGIN_ROOT — the
8
+ // flow plugin can't reach the design plugin's dev-server dir; only the on-PATH
9
+ // `maude` binary resolves it reliably across install shapes — see DDR-062), runs
10
+ // it, and writes report.md. Output stdout = the written path (capture idiom).
11
+
12
+ export async function run({ args, pkgRoot }) {
13
+ const runDir = args.find((a) => !a.startsWith('--'));
14
+ if (!runDir || runDir === 'help') {
15
+ process.stdout.write(
16
+ 'maude scenario-report <run-dir> [--out <path>]\n\n' +
17
+ ' Walk a scenario run directory and emit the mechanical sections of report.md\n' +
18
+ ' (TL;DR, counter-delta, per-step pivot, path listing). Leaves the prose sections\n' +
19
+ ' ("What surprised us", "Recommended follow-ups") as placeholders for the LLM.\n' +
20
+ ' Default output: <run-dir>/report.md.\n'
21
+ );
22
+ return;
23
+ }
24
+
25
+ const generatorPath = resolve(
26
+ pkgRoot,
27
+ 'plugins',
28
+ 'design',
29
+ 'dev-server',
30
+ 'bin',
31
+ 'scenario-report.mjs'
32
+ );
33
+ const { generateReport } = await import(pathToFileURL(generatorPath).href);
34
+
35
+ const absRunDir = resolve(process.cwd(), runDir);
36
+ const outIdx = args.indexOf('--out');
37
+ const outPath =
38
+ outIdx >= 0 && args[outIdx + 1]
39
+ ? resolve(process.cwd(), args[outIdx + 1])
40
+ : join(absRunDir, 'report.md');
41
+
42
+ const md = generateReport(absRunDir);
43
+ writeFileSync(outPath, md, 'utf8');
44
+ process.stdout.write(`${outPath}\n`);
45
+ }