@motujs/cli 0.0.0-stage → 0.1.1

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 (106) hide show
  1. package/LICENSE +21 -0
  2. package/bundled/github/agents/archipelago-build.agent.md +298 -0
  3. package/bundled/github/agents/island-create.agent.md +261 -0
  4. package/bundled/github/agents/island-extract.agent.md +360 -0
  5. package/bundled/github/agents/island-locate.agent.md +208 -0
  6. package/bundled/github/agents/motu-checks.agent.md +158 -0
  7. package/bundled/github/agents/motu-evidence.agent.md +267 -0
  8. package/bundled/github/agents/motu-regions.agent.md +304 -0
  9. package/bundled/github/host-rules.md +43 -0
  10. package/bundled/github/prompts/archipelago-build.prompt.md +19 -0
  11. package/bundled/github/prompts/island-create.prompt.md +37 -0
  12. package/bundled/github/prompts/island-extract.prompt.md +59 -0
  13. package/bundled/github/prompts/island-locate.prompt.md +43 -0
  14. package/bundled/packages/adapters/angularjs/vite.mjs +43 -0
  15. package/bundled/packages/adapters/next/lagoon/next-stubs.tsx +108 -0
  16. package/bundled/packages/adapters/next/vite.mjs +121 -0
  17. package/bundled/packages/adapters/vite/vite.mjs +179 -0
  18. package/package.json +64 -3
  19. package/src/alias-loader.mjs +63 -0
  20. package/src/alias-register.mjs +28 -0
  21. package/src/asset-stub.cjs +3 -0
  22. package/src/asset-stub.mjs +5 -0
  23. package/src/cli.mjs +78 -0
  24. package/src/commands/adopt-root.mjs +194 -0
  25. package/src/commands/archipelago-init.mjs +314 -0
  26. package/src/commands/archipelago.mjs +138 -0
  27. package/src/commands/assurance.mjs +40 -0
  28. package/src/commands/check.mjs +346 -0
  29. package/src/commands/codegen.mjs +29 -0
  30. package/src/commands/coverage.mjs +358 -0
  31. package/src/commands/create.mjs +277 -0
  32. package/src/commands/defaults.mjs +129 -0
  33. package/src/commands/diff.mjs +595 -0
  34. package/src/commands/fixtures.mjs +153 -0
  35. package/src/commands/init.mjs +473 -0
  36. package/src/commands/integrate.mjs +208 -0
  37. package/src/commands/integration.mjs +1349 -0
  38. package/src/commands/lagoon-try.mjs +272 -0
  39. package/src/commands/lagoon.mjs +1419 -0
  40. package/src/commands/link.mjs +110 -0
  41. package/src/commands/region-coverage.mjs +604 -0
  42. package/src/commands/removal-check.mjs +878 -0
  43. package/src/commands/skills.mjs +156 -0
  44. package/src/commands/snapshot.mjs +456 -0
  45. package/src/commands/sync.mjs +38 -0
  46. package/src/commands/verify/island-runtime.mjs +581 -0
  47. package/src/commands/verify/island-static.mjs +586 -0
  48. package/src/commands/verify/island.mjs +135 -0
  49. package/src/commands/verify/reach.mjs +426 -0
  50. package/src/commands/verify/region-composition.mjs +391 -0
  51. package/src/commands/verify/region-config.mjs +330 -0
  52. package/src/commands/verify/region-flows.mjs +760 -0
  53. package/src/commands/verify/region-sources.mjs +516 -0
  54. package/src/commands/verify/region.mjs +271 -0
  55. package/src/commands/verify/report.mjs +338 -0
  56. package/src/commands/verify/screens-runtime.mjs +123 -0
  57. package/src/commands/verify.mjs +18 -0
  58. package/src/lagoon-build.mjs +25 -0
  59. package/src/lagoon-dev.mjs +32 -0
  60. package/src/lib/alias-core.mjs +49 -0
  61. package/src/lib/archipelagos.mjs +111 -0
  62. package/src/lib/assurance.mjs +144 -0
  63. package/src/lib/baselines.mjs +107 -0
  64. package/src/lib/bundlability.mjs +123 -0
  65. package/src/lib/changed.mjs +157 -0
  66. package/src/lib/component-props.mjs +145 -0
  67. package/src/lib/config.mjs +451 -0
  68. package/src/lib/contracts.mjs +244 -0
  69. package/src/lib/data-flow.mjs +47 -0
  70. package/src/lib/declared-sources.mjs +101 -0
  71. package/src/lib/dock-inject.mjs +47 -0
  72. package/src/lib/effects.mjs +97 -0
  73. package/src/lib/eject.mjs +715 -0
  74. package/src/lib/evidence-json.mjs +74 -0
  75. package/src/lib/expect-calls.mjs +53 -0
  76. package/src/lib/finding-memory.mjs +0 -0
  77. package/src/lib/flow-calls.mjs +53 -0
  78. package/src/lib/framework.mjs +98 -0
  79. package/src/lib/host-rules.mjs +55 -0
  80. package/src/lib/host-sources.mjs +121 -0
  81. package/src/lib/island-placement.mjs +64 -0
  82. package/src/lib/island-render.mjs +105 -0
  83. package/src/lib/islands.mjs +112 -0
  84. package/src/lib/lagoon-bundle.mjs +138 -0
  85. package/src/lib/lagoon-declares.mjs +553 -0
  86. package/src/lib/lagoon-materialize.mjs +217 -0
  87. package/src/lib/lagoon-vite.mjs +788 -0
  88. package/src/lib/live-url.mjs +53 -0
  89. package/src/lib/node-aliases.mjs +73 -0
  90. package/src/lib/operations.mjs +145 -0
  91. package/src/lib/provenance-plugin.mjs +74 -0
  92. package/src/lib/region-refs.mjs +40 -0
  93. package/src/lib/remote.mjs +171 -0
  94. package/src/lib/scaffold.mjs +606 -0
  95. package/src/lib/screens.mjs +266 -0
  96. package/src/lib/snapshots.mjs +85 -0
  97. package/src/lib/stubs.mjs +114 -0
  98. package/src/lib/ts-project.mjs +42 -0
  99. package/src/lib/util.mjs +626 -0
  100. package/src/lib/worlds.mjs +236 -0
  101. package/src/lib/write-if-changed.mjs +35 -0
  102. package/src/lib/writes-coverage.mjs +32 -0
  103. package/src/playwright-lagoon.mjs +2305 -0
  104. package/src/run.mjs +385 -0
  105. package/src/runtime-harness.mjs +225 -0
  106. package/README.md +0 -3
package/src/run.mjs ADDED
@@ -0,0 +1,385 @@
1
+ #!/usr/bin/env node
2
+ // motu — the agentic CLI for the island / archipelago workflow.
3
+ //
4
+ // motu island create <name> scaffold a new island (component + registry row + fixtures)
5
+ // motu island verify <name> run the deterministic island rules + lagoon mount (the moat)
6
+ // `--all` sweeps every island; one line each, findings only where there are any
7
+ //
8
+ // Kept dependency-light and plain-ESM (like @motujs/codegen) so it runs with a bare `node`.
9
+ import { createCommand } from './commands/create.mjs';
10
+ import { verifyCommand, archipelagoVerifyCommand } from './commands/verify.mjs';
11
+ import { checkCommand } from './commands/check.mjs';
12
+ import { snapshotCommand, archipelagoSnapshotCommand } from './commands/snapshot.mjs';
13
+ import { integrateCommand } from './commands/integrate.mjs';
14
+ import { integrateCheckCommand } from './commands/integration.mjs';
15
+ import { archipelagoInitCommand } from './commands/archipelago-init.mjs';
16
+ import { regionCoverageCommand } from './commands/region-coverage.mjs';
17
+ import { coverageCommand } from './commands/coverage.mjs';
18
+ import { diffCommand, flowsTraceCommand } from './commands/diff.mjs';
19
+ import { assuranceCommand } from './commands/assurance.mjs';
20
+ import { screenVerifyCommand } from './commands/verify/screens-runtime.mjs';
21
+ import { closeLagoonPool } from './playwright-lagoon.mjs';
22
+ import { archipelagoAdoptRootCommand } from './commands/adopt-root.mjs';
23
+ import { codegenCommand } from './commands/codegen.mjs';
24
+ import { fixturesRecordCommand } from './commands/fixtures.mjs';
25
+ import { islandDefaultsCommand } from './commands/defaults.mjs';
26
+ import { syncCommand } from './commands/sync.mjs';
27
+ import { removalCheckCommand } from './commands/removal-check.mjs';
28
+ import { lagoonPublishCommand, lagoonServeCommand, lagoonEjectCommand, lagoonStatesCommand } from './commands/lagoon.mjs';
29
+ import { lagoonTryCommand } from './commands/lagoon-try.mjs';
30
+ import { loadMotuConfig, assertProject } from './lib/config.mjs';
31
+ import { initCommand } from './commands/init.mjs';
32
+ import { skillsInstallCommand, skillsListCommand } from './commands/skills.mjs';
33
+ import { color, ensureNoInstallLinks, REPO_ROOT, MOTU_CHECKOUT } from './lib/util.mjs';
34
+
35
+ /** Minimal argv parser: positionals in `_`, `--flag`/`--no-flag`/`--key value` in the rest. */
36
+ function parse(args) {
37
+ const out = { _: [] };
38
+ for (let i = 0; i < args.length; i++) {
39
+ const a = args[i];
40
+ if (a.startsWith('--no-')) out[a.slice(5)] = false;
41
+ else if (a.startsWith('--')) {
42
+ const key = a.slice(2);
43
+ const next = args[i + 1];
44
+ if (next !== undefined && !next.startsWith('--')) {
45
+ out[key] = next;
46
+ i++;
47
+ } else out[key] = true;
48
+ } else out._.push(a);
49
+ }
50
+ return out;
51
+ }
52
+
53
+ const USAGE = `${color.bold('motu')} — island / archipelago CLI
54
+
55
+ ${color.bold('Usage:')}
56
+ motu init [dir] --host next|angularjs|none scaffold config + registries + a WORKING lagoon root
57
+ motu link [--check] record @motujs/* in package.json (npm deletes bare links)
58
+ motu island create <name> scaffold a new island (component, registry, fixtures)
59
+ motu island create <name> --from <specifier> ...over a component the app ALREADY owns (React hosts)
60
+ motu check every island + every region + removal, one verdict
61
+ motu island verify <name|--all> the island rules (static; --runtime adds the browser)
62
+ motu island snapshot <name|--all> [--update] visual baselines, per scenario × viewport
63
+ motu island snapshot --all --remote ...stored on the lagoon host instead of in git
64
+ motu island snapshot --all --remote --changed ...only what this branch touched
65
+ motu island snapshot --accept <name> move the accepted baseline — a decision, not a write
66
+ motu island defaults [name] classify declared defaults: component default, or evidence?
67
+ motu sync regenerate every derived file (registry, contracts, region module)
68
+ motu island integrate <name> --archipelago <id> make the island a member of an archipelago
69
+ motu integrate check [region] does the HOST compose, place and read the region?
70
+ motu archipelago init <id> --page <file> scaffold everything a page needs before its 1st island
71
+ motu archipelago init <id> ...a NEW region with no page yet: the archipelago, root first
72
+ motu archipelago verify <id|--all> boot the whole region in the lagoon + config checks
73
+ motu archipelago snapshot <id|--all> --remote picture the COMPOSED page — catches arrangement
74
+ motu archipelago coverage <id> [--corpus <f>] states production reached that no flow previews
75
+ motu diff --base <ref> [--explain <file>] [--json] what the change did to the flows: keys, renders, calls, declarations — each to explain or fix
76
+ motu assurance [id] [--json] one row per declared thing (slot, write, key, calls) and the strongest evidence holding it
77
+ motu screen verify [id] [--json] open each declared screen (or one), hold its calls to declarations, replay screen flows
78
+ motu coverage init [--mode forward|log] wire production coverage: the route, the ingest token, the one step left
79
+ motu coverage doctor --url <origin> prove the deployed route reaches the host; names the hop that fails
80
+ motu coverage pull --from <log|-> upload \`motu-coverage\` lines from a log-mode server's log
81
+ --json --ids --accept <id> --fail-above <n> machine-readable · print ids · accept one · gate
82
+ --forget <id> | --forget-all remove a state the instrument recorded wrongly
83
+ motu fixtures record <island> capture backend responses into request-keyed fixtures
84
+ motu lagoon states [island|region] every state the lagoon can be OPENED in, as a URL
85
+ motu lagoon try <region> [step…] "what happens if…?" — each step's keys, slots and calls
86
+ motu lagoon eject write the framework's lagoon entries into the project
87
+ motu lagoon publish [island] build the lagoon as one self-contained page to publish
88
+ motu lagoon publish --remote <url> ...or upload it to a lagoon host as chunks (see motu-host)
89
+ motu lagoon serve [island] build that same page and serve it (preview it in a browser)
90
+ motu lagoon serve --watch [--host] ...and keep it current: rebuild on save, reload viewers (the loop)
91
+ motu lagoon serve --watch --detach | --stop ...left running after the shell exits, and stopped
92
+ motu removal-check [--force] prove motu is removable from the host app (C2)
93
+ motu codegen [manifest] [outDir] regenerate @motujs/contract from motu-manifest.json
94
+ motu skills install [dir] install the motu agent skills into a repo (Copilot + Claude Code)
95
+ motu skills list list the skills this motu checkout ships
96
+
97
+ ${color.bold('snapshot flags:')}
98
+ --update record the baselines instead of checking them (a deliberate, separate act)
99
+ --all every island
100
+ --json machine-readable report
101
+ ${color.dim('baselines live in <islands>/<kebab>.snapshots/, one per scenario × viewport; commit them.')}
102
+
103
+ ${color.bold('removal-check flags:')}
104
+ --force re-prove it even when nothing it depends on has changed
105
+ ${color.dim('the proof is cached against the files it touches + the islands/archipelagos it generates from;')}
106
+ ${color.dim('an unchanged repo skips the application typecheck, which is nearly all of the cost.')}
107
+
108
+ ${color.bold('check flags:')}
109
+ --runtime also drive the lagoon in a browser (mount, data-flow, viewports, axe, live wiring)
110
+ --verbose name each runtime step as it runs, with what it cost
111
+ --json machine-readable verdict: islands, regions, removal
112
+ ${color.dim('motu check runs MOTU\'s gates only. Your typecheck/linter are yours: `<host build> && motu check`.')}
113
+
114
+ ${color.bold('verify flags:')}
115
+ --all every island / every region, one after another; one line each, findings where there are any
116
+ --runtime add the lagoon checks: mount, data-flow, viewports, axe, live wiring
117
+ ${color.dim('opt-in: it drives a real browser per scenario × viewport. Static verify answers drift, which is the common question.')}
118
+ --verbose name each runtime step as it runs, with what it cost
119
+ --fast use the in-process happy-dom lagoon mount instead of the real browser (Playwright)
120
+ --standalone the island is intentionally not in an archipelago (no membership warning)
121
+ --json machine-readable report
122
+
123
+ ${color.bold('lagoon states:')}
124
+ ${color.dim('an island scenario and a region flow are ADDRESSES — open the lagoon directly on one:')}
125
+ ${color.dim(' /?target=island:x-week-actions&scenario=a%20week%20to%20answer')}
126
+ ${color.dim(' /?flow=marking%20a%20mission%20done&step=2')}
127
+ ${color.dim(' both against the gallery — the entry `serve` and `publish` build, and the one a person opens')}
128
+ --base <url> print absolute URLs against a running lagoon instead of paths
129
+ --json machine-readable catalogue
130
+ ${color.dim('a name that resolves to nothing REFUSES to render — it never falls back to the default state.')}
131
+
132
+ ${color.bold('lagoon try:')}
133
+ ${color.dim('explore a region before you know what to assert — the same verbs a flow has, nothing else:')}
134
+ ${color.dim(" motu lagoon try review 'click Accept' 'emit shot-list:shot-selected {\"island\":\"x\",\"shot\":\"a@desktop\"}'")}
135
+ in <world> · seed <key>=<json> · look · click <name> · emit <slot>:<event> [json] · provide <key>=<json>
136
+ shows <slot>: <text> · hides <slot>: <text> · holds <key>=<json> ${color.dim('assertions — yours, never copied from the output')}
137
+ --world <id> run in a declared world --shot <dir> a PNG per step
138
+ --keep <name> append it to <id>.evidence.ts — only with assertions, only if they all held
139
+ --json machine-readable
140
+
141
+ ${color.bold('lagoon serve flags:')}
142
+ --port <n> port to listen on (default 8817)
143
+ --host also serve on your LAN, to open it on a phone on the same wifi
144
+ --no-build serve the last published artifact instead of rebuilding
145
+ --watch rebuild on every source change and live-reload open viewers (phones included)
146
+ --live-url <url> the address the HOST should fetch this from, when it is not on this machine
147
+ --live-push send the built page to the host after every save, instead of being fetched
148
+ --single build one self-contained page instead of chunks (rebuilds and pushes send it whole)
149
+ ${color.dim('takes the same target/--fit flags as publish. Ctrl-C to stop.')}
150
+ ${color.dim('--live-url needs --host, and the host must ALLOW the name (MOTU_LIVE_ALLOW there):')}
151
+ ${color.dim(' motu lagoon serve --watch --host --live-url http://192.168.1.20:8901')}
152
+ ${color.dim('the address is whatever the HOST can reach: a LAN IP, a tailnet peer, a tunnel. An')}
153
+ ${color.dim('ingress/funnel name usually is NOT — those publish one port, and not this one.')}
154
+ ${color.dim('--live-push needs nothing to reach you at all: each save uploads the chunks it changed')}
155
+ ${color.dim('and the manifest, and the host serves them. Use it when there is no route back to you.')}
156
+
157
+ ${color.bold('lagoon publish flags:')}
158
+ --archipelago <id> publish one archipelago instead of an island
159
+ --fit <native|legacy> legacy-fit strategy for a single-island target
160
+ --out <path> write somewhere other than .motu/publish/ (keep it stable to keep one URL)
161
+ --title <text> name this lagoon in the host's listing (default: derived from the target)
162
+ --remote [url] upload it to a lagoon host (or $MOTU_HOST_URL) as chunks, and print the URLs —
163
+ only the chunks the host does not already have are sent
164
+ --single with --remote: upload one self-contained page instead (a host without chunks)
165
+ --token <secret> the host's upload token (or $MOTU_HOST_TOKEN)
166
+ --json machine-readable report
167
+ ${color.dim('no target => every archipelago, with the switcher. Always mock-backed: an artifact has no backend.')}
168
+
169
+ ${color.bold('island create flags:')}
170
+ --from <specifier> wrap a component the application already owns instead of scaffolding one under
171
+ ui/ — the React-host case, where copying it would fork it. Use the specifier
172
+ the app itself uses (an alias like @/components/foo, or a relative path).
173
+ --export <name> the component's name INSIDE that module, when it is not the island's Pascal name
174
+ --force overwrite an existing scaffolded component
175
+
176
+ ${color.bold('island snapshot flags:')}
177
+ --update record baselines as FILES beside the evidence (the in-repo model)
178
+ --remote [url] compare against the host's ACCEPTED baseline; nothing is written to git
179
+ --accept [name] move the accepted pointer to what was last rendered
180
+ --changed [base] only islands/regions this branch touched — widens back to everything, loudly,
181
+ when a changed file belongs to no single one
182
+ ${color.dim('a page, a screen, an app-side source, the lagoon map or a shared evidence module belongs to no')}
183
+ ${color.dim('island, so a real session usually widens. To check ONE region while you work, name it:')}
184
+ ${color.dim(' motu archipelago verify <id> --runtime · motu island verify <name> --runtime')}
185
+ --json machine-readable report
186
+
187
+ ${color.bold('integrate flags:')}
188
+ --archipelago <id> (required) target archipelago id
189
+ --slot <slot> marker slot name (defaults to the island's kebab name)
190
+
191
+ ${color.bold('skills install flags:')}
192
+ --only <both|claude|copilot> which agent format(s) to write (default: both)
193
+ --force overwrite files that already exist with different contents
194
+ --json machine-readable report
195
+
196
+ ${color.bold('create flags:')}
197
+ --force overwrite existing files
198
+ `;
199
+
200
+ async function main() {
201
+ // RESTORE THE NO-INSTALL LINKS FIRST, on every run.
202
+ //
203
+ // `motu init` symlinks node_modules/@motujs/* into the checkout, which is what lets a project depend
204
+ // on nothing. `npm install` then deletes them as extraneous — measured on an Angular app, where the
205
+ // next build failed with `Cannot find module '@motujs/react'`, an error pointing nowhere near the
206
+ // cause. (bun and pnpm left them alone, so this is invisible on the machines motu grew up on.)
207
+ // Re-linking is a handful of existsSync calls and makes the mechanism survive the host's package
208
+ // manager instead of losing to it.
209
+ try {
210
+ ensureNoInstallLinks(REPO_ROOT, MOTU_CHECKOUT);
211
+ } catch {
212
+ // Not a motu project, or a read-only checkout — the commands that need the links say so themselves.
213
+ }
214
+ const [, , group, sub, ...rest] = process.argv;
215
+ const argv = parse(rest);
216
+
217
+ if (!group || group === '--help' || group === '-h' || group === 'help') {
218
+ console.log(USAGE);
219
+ process.exit(group ? 0 : 1);
220
+ }
221
+
222
+ // `--help` ON A SUBCOMMAND. It used to be parsed as an ordinary flag, so `motu island create --help`
223
+ // reached the command with no positional, printed its usage line, and exited 2 — the code that means
224
+ // "a check could not run, retry, do NOT repair". Asking for help is neither a failure nor an
225
+ // environment problem, and this was the FIRST command two cold-start agents typed for each verb.
226
+ // Handled once, here, so every subcommand answers the same way instead of each remembering to.
227
+ // `sub` IS CHECKED TOO, and leaving it out was worse than the bug it was fixing. On a top-level verb
228
+ // (`motu init --help`) the flag lands in `sub`, not in `rest`, so `argv.help` was false, dispatch
229
+ // fell through, and `init` treated the missing positional as "." — `motu init --help` SCAFFOLDED
230
+ // MOTU INTO THE CURRENT DIRECTORY and exited 0. Found by a cold-start agent typing it in its repo,
231
+ // where the existing config refused the overwrite and only an exit 1 showed; reproduced in an empty
232
+ // directory, where it silently created a project.
233
+ const askedForHelp = ['--help', '-h', 'help'].includes(sub) || argv.help || rest.includes('-h');
234
+ if (askedForHelp) {
235
+ console.log(USAGE);
236
+ console.log(color.dim(`\n(\`motu ${group}${sub ? ' ' + sub : ''}\` — see the usage block above; each command also prints its own usage when required arguments are missing.)`));
237
+ process.exit(0);
238
+ }
239
+
240
+ if (group === 'init') {
241
+ // Top-level verb: sub is an optional target dir positional.
242
+ return initCommand(parse([sub, ...rest].filter((x) => x !== undefined)));
243
+ }
244
+
245
+ // AM I EVEN IN A PROJECT? Asked once, here, after the verbs that legitimately run outside one:
246
+ // `init` creates the config, `skills install` copies agent skills into a repo motu has not touched,
247
+ // and `link` repairs a package.json the package manager stripped. Every other verb reads a layout,
248
+ // and reading the WRONG layout is silent — an empty islands glob is not an error, it is a check
249
+ // that examined nothing and passed. See `assertProject`.
250
+ if (!['skills', 'link'].includes(group)) assertProject(loadMotuConfig(), { color });
251
+
252
+ // `motu link` — record @motujs/* in package.json so the package manager stops deleting them.
253
+ // `--check` asserts instead of writing, which is the CI half of the same question.
254
+ if (group === 'sync') return syncCommand(parse([sub, ...rest].filter((x) => x !== undefined)));
255
+
256
+ if (group === 'link') {
257
+ const { linkCommand } = await import('./commands/link.mjs');
258
+ return linkCommand(parse([sub, ...rest].filter((x) => x !== undefined)));
259
+ }
260
+
261
+ // The last mile, and its own verb because it asks about the HOST, not about motu's files:
262
+ // `motu integrate check [region]`.
263
+ if (group === 'integrate') {
264
+ // `rest` is already everything after the sub-verb, so the region positional is argv._[0].
265
+ if (sub === 'check') return integrateCheckCommand(argv);
266
+ console.error(color.red(`unknown: motu integrate ${sub ?? ''}`));
267
+ console.log(USAGE);
268
+ process.exit(2);
269
+ }
270
+
271
+ if (group === 'island') {
272
+ if (sub === 'create') return createCommand(argv);
273
+ if (sub === 'verify') return verifyCommand(argv);
274
+ if (sub === 'snapshot') return snapshotCommand(argv);
275
+ if (sub === 'defaults') return islandDefaultsCommand(argv);
276
+ // The two halves of what is now `motu sync`. Kept answering, because every generated file in an
277
+ // adopting repo carried "run `motu island sync`" in its header until it was regenerated.
278
+ if (sub === 'sync') return syncCommand(argv);
279
+ if (sub === 'integrate') return integrateCommand(argv);
280
+ console.error(color.red(`unknown: motu island ${sub ?? ''}`));
281
+ console.log(USAGE);
282
+ process.exit(2);
283
+ }
284
+
285
+ if (group === 'archipelago') {
286
+ // `snapshot` takes an id positional, so re-parse with `rest` shifted.
287
+ if (sub === 'snapshot') return archipelagoSnapshotCommand(parse(rest));
288
+ if (sub === 'create') {
289
+ console.error(color.red('`motu archipelago create` is now `motu archipelago init <id>`') + color.dim(' — add --page <file> when the region has a page'));
290
+ process.exit(1);
291
+ }
292
+ // Everything a page needs before its FIRST island — the step that makes adoption feel expensive.
293
+ if (sub === 'init') return archipelagoInitCommand(argv);
294
+ // What the region DOES, against what it previews. The only check here that compares motu to
295
+ // reality rather than to a declaration — see the command's own header.
296
+ if (sub === 'coverage') return regionCoverageCommand(argv);
297
+ // The migration `region-root` points at: derive `root` + `slots` from the frame, hand back the rest.
298
+ if (sub === 'adopt-root') return archipelagoAdoptRootCommand(parse(rest));
299
+ if (sub === 'verify') return archipelagoVerifyCommand(argv);
300
+ if (sub === 'sync') return syncCommand(argv);
301
+ console.error(color.red(`unknown: motu archipelago ${sub ?? ''}`));
302
+ console.log(USAGE);
303
+ process.exit(2);
304
+ }
305
+
306
+ if (group === 'fixtures') {
307
+ if (sub === 'record') return fixturesRecordCommand(argv);
308
+ console.error(color.red(`unknown: motu fixtures ${sub ?? ''}`));
309
+ console.log(USAGE);
310
+ process.exit(2);
311
+ }
312
+
313
+ if (group === 'skills') {
314
+ // `install` takes an optional target-dir positional, so re-parse with `rest` shifted.
315
+ if (sub === 'install') return skillsInstallCommand(parse(rest));
316
+ if (sub === 'list') return skillsListCommand(argv);
317
+ console.error(color.red(`unknown: motu skills ${sub ?? ''}`));
318
+ console.log(USAGE);
319
+ process.exit(2);
320
+ }
321
+
322
+ if (group === 'lagoon') {
323
+ // REMOVED, and said so with the replacement rather than as an unknown verb: two servers for one
324
+ // loop was one too many, and `serve --watch` is the one that builds what `publish` ships.
325
+ if (sub === 'dev') {
326
+ console.error(color.red('`motu lagoon dev` is gone — use `motu lagoon serve --watch`') + color.dim(' (--detach / --stop / --no-live / --as carry over)'));
327
+ process.exit(1);
328
+ }
329
+ if (sub === 'states') return lagoonStatesCommand(argv);
330
+ if (sub === 'try') return lagoonTryCommand(argv);
331
+ if (sub === 'eject') return lagoonEjectCommand(argv);
332
+ if (sub === 'publish') return lagoonPublishCommand(argv);
333
+ if (sub === 'serve') return lagoonServeCommand(argv);
334
+ console.error(color.red(`unknown: motu lagoon ${sub ?? ''}`));
335
+ console.log(USAGE);
336
+ process.exit(2);
337
+ }
338
+
339
+ if (group === 'assurance') {
340
+ process.exit(assuranceCommand(parse([sub, ...rest].filter(Boolean))) ?? 0);
341
+ }
342
+ if (group === 'diff') {
343
+ const code = await diffCommand(parse([sub, ...rest].filter(Boolean)));
344
+ process.exit(code ?? 0);
345
+ }
346
+ // INTERNAL: one side of `motu diff` (it runs this in a base worktree, then here).
347
+ if (group === 'flows' && sub === 'trace') {
348
+ const code = await flowsTraceCommand(argv);
349
+ process.exit(code ?? 0);
350
+ }
351
+
352
+ if (group === 'screen') {
353
+ if (sub === 'verify') {
354
+ const code = await screenVerifyCommand(parse(rest));
355
+ await closeLagoonPool().catch(() => {});
356
+ process.exit(code ?? 0);
357
+ }
358
+ console.error(color.red(`unknown: motu screen ${sub ?? ''}`) + color.dim(' — verify [id]'));
359
+ process.exit(2);
360
+ }
361
+
362
+ if (group === 'coverage') {
363
+ const code = await coverageCommand(sub, argv);
364
+ process.exit(code ?? 0);
365
+ }
366
+
367
+ // Single-word verbs: their flags land in `sub`, since `argv` is parsed from what follows it.
368
+ if (group === 'removal-check') return removalCheckCommand(parse([sub, ...rest].filter(Boolean)));
369
+
370
+ if (group === 'check') return checkCommand(parse([sub, ...rest].filter(Boolean)));
371
+
372
+ if (group === 'codegen') {
373
+ // Top-level verb: sub + rest are positional args ([manifest, outDir]).
374
+ return codegenCommand(parse([sub, ...rest].filter((x) => x !== undefined)));
375
+ }
376
+
377
+ console.error(color.red(`unknown command: motu ${group}`));
378
+ console.log(USAGE);
379
+ process.exit(2);
380
+ }
381
+
382
+ main().catch((err) => {
383
+ console.error(color.red(err?.stack || String(err)));
384
+ process.exit(1);
385
+ });
@@ -0,0 +1,225 @@
1
+ // Runtime lagoon check, run in-process under happy-dom (no browser download, no vite). It mounts ONE
2
+ // island by tag against MockTransport fixtures and reports whether it produced a non-empty shadow DOM.
3
+ // Run via `node --import tsx runtime-harness.mjs <tag> <fixturesPath> <fit> [mode]`; prints a single
4
+ // JSON line. mode 'mount' (default) = the render/remount check; mode 'differentiate' = mount once per
5
+ // declared `scenario` seed and report whether distinct inputs produced distinct output (data-flow).
6
+ import { GlobalRegistrator } from '@happy-dom/global-registrator';
7
+ import * as React from 'react';
8
+ import { stringifyEvidence } from './lib/evidence-json.mjs';
9
+ import { installIslandRender } from './lib/island-render.mjs';
10
+ import { judgeRenders } from './lib/data-flow.mjs';
11
+
12
+ /**
13
+ * GENERATED IDS ARE MEANT TO DIFFER between mounts. React's `useId` counter is module state and Radix
14
+ * builds `aria-controls` from it, so comparing raw HTML across two mounts compares a counter: the
15
+ * browser reloads and the counter resets (pass), node reuses the registry (fail). Neither was testing
16
+ * what `remount-stable` claims, and the failure it printed — "likely accidental module-level state" —
17
+ * named the one thing that was not the cause.
18
+ */
19
+ function withoutGeneratedIds(html) {
20
+ return String(html)
21
+ .replace(/(_r_|:r|«r)[0-9a-z]+(_|:|»)/gi, '<id>')
22
+ .replace(/\bradix-[\w:-]+/gi, 'radix-<id>');
23
+ }
24
+
25
+ async function main() {
26
+ const [, , tag, fixturesPath, fit = 'native', mode = 'mount'] = process.argv;
27
+
28
+ // 'scenarios' mode: print the fixtures' declared scenarios as JSON and exit BEFORE importing the DOM
29
+ // or the element registry (which may pull in vite-only `?raw` imports). Lets the plain-node verify
30
+ // CLI read scenarios without a happy-dom mount.
31
+ if (mode === 'scenarios') {
32
+ let scenarios = [];
33
+ if (fixturesPath) {
34
+ try {
35
+ const mod = await import(fixturesPath);
36
+ scenarios = mod.scenarios ?? [];
37
+ } catch (err) {
38
+ process.stderr.write(`harness: could not load scenarios ${fixturesPath}: ${err?.message}\n`);
39
+ }
40
+ }
41
+ // stringifyEvidence, not JSON.stringify: a Date in a seed has to reach the CLI as a Date.
42
+ // See lib/evidence-json.mjs — this hop silently flattened them for as long as it existed.
43
+ process.stdout.write(stringifyEvidence({ scenarios }) + '\n');
44
+ process.exit(0);
45
+ }
46
+
47
+ // 'catalogue' mode: resolve a catalogue region's declared members against the app's own capture.
48
+ // Runs here rather than in the CLI for one reason: `@motujs/core` is TS source, so `checkCatalogue`
49
+ // is importable under tsx and nowhere else — and a second copy of that set arithmetic in the CLI
50
+ // would drift from the one the framework actually ships.
51
+ if (mode === 'catalogue') {
52
+ const declared = JSON.parse(process.env.MOTU_CATALOGUE_DECLARED ?? '[]');
53
+ try {
54
+ const { capture } = await import(fixturesPath);
55
+ if (!capture) {
56
+ process.stdout.write(JSON.stringify({ error: 'no `capture` export' }) + '\n');
57
+ process.exit(0);
58
+ }
59
+ const { checkCatalogue } = await import('@motujs/core');
60
+ const present = (typeof capture.present === 'function' ? capture.present() : capture.present) ?? [];
61
+ const universe = typeof capture.universe === 'function' ? capture.universe() : capture.universe;
62
+ process.stdout.write(JSON.stringify({ report: checkCatalogue({ declared, universe, present }), present: present.length }) + '\n');
63
+ } catch (err) {
64
+ process.stdout.write(JSON.stringify({ error: err?.message ?? String(err) }) + '\n');
65
+ }
66
+ process.exit(0);
67
+ }
68
+
69
+ GlobalRegistrator.register();
70
+ // The islands rely on the JSX transform and never `import React`. tsx/esbuild compiles the classic
71
+ // runtime (React.createElement) here, so expose React as a free global for the island modules.
72
+ globalThis.React = React;
73
+
74
+ // Diagnostics: any console.error the island emits (incl. React warnings) or unhandled rejection.
75
+ const diagnostics = [];
76
+ const realConsoleError = console.error.bind(console);
77
+ console.error = (...args) => {
78
+ diagnostics.push('console.error: ' + args.map((a) => (a instanceof Error ? a.message : String(a))).join(' '));
79
+ };
80
+ process.on('unhandledRejection', (reason) => {
81
+ diagnostics.push('unhandledrejection: ' + String(reason instanceof Error ? reason.stack || reason.message : reason));
82
+ });
83
+
84
+ // Imported AFTER the DOM globals exist so custom elements / shadow roots / constructable stylesheets
85
+ // resolve against happy-dom.
86
+ const { configure } = await import('@motujs/runtime');
87
+ const { MockTransport } = await import('@motujs/runtime/mock');
88
+ const { defineLagoon } = await import('@motujs/react');
89
+ // Import the app barrel by resolved path (from motu.config.json), not by a hardcoded package name,
90
+ // so the harness works for any project layout.
91
+ const { pathToFileURL } = await import('node:url');
92
+ const { paths } = await import('./lib/util.mjs');
93
+ const { ELEMENT_REGISTRY } = await import(pathToFileURL(paths.barrel).href);
94
+
95
+ let fixtures = [];
96
+ let roles = [];
97
+ let scenarios = [];
98
+ if (fixturesPath) {
99
+ try {
100
+ const mod = await import(fixturesPath);
101
+ fixtures = mod.fixtures ?? [];
102
+ roles = mod.roles ?? [];
103
+ scenarios = mod.scenarios ?? [];
104
+ } catch (err) {
105
+ // Missing/broken fixtures shouldn't crash the mount; the default render doesn't need them.
106
+ process.stderr.write(`harness: could not load fixtures ${fixturesPath}: ${err?.message}\n`);
107
+ }
108
+ }
109
+
110
+ configure(new MockTransport(fixtures, roles));
111
+
112
+ const mountOne = (seed = { criteria: {} }) => {
113
+ const node = defineLagoon({ kind: 'island', tag, fit }, { elements: ELEMENT_REGISTRY, seed });
114
+ document.body.appendChild(node);
115
+ return node;
116
+ };
117
+
118
+ // The lagoon wraps every island target in a one-island <motu-archipelago> that owns the shadow
119
+ // boundary; the island renders LIGHT inside it, so pierce the archipelago shadow to find the tag and
120
+ // read its light DOM (a standalone island would instead have its own shadowRoot).
121
+ const findIsland = () => {
122
+ let node = document.querySelector(tag);
123
+ if (!node) {
124
+ for (const arch of document.querySelectorAll('motu-archipelago')) {
125
+ const hit = arch.shadowRoot && arch.shadowRoot.querySelector(tag);
126
+ if (hit) {
127
+ node = hit;
128
+ break;
129
+ }
130
+ }
131
+ }
132
+ return node;
133
+ };
134
+ // The same reading of "what the island rendered" as the browser lane: its own DOM plus what its own
135
+ // mount portaled to <body> (lib/island-render.mjs) — so `--fast` and the browser agree on a dialog.
136
+ installIslandRender(window);
137
+ const rendered = (node) => window.__motuRendered(node);
138
+
139
+ // DIFFERENTIATION mode: mount once per declared scenario seed and diff the rendered output. Distinct
140
+ // inputs producing distinct output prove data flows criteria -> contract -> render (not just wiring).
141
+ if (mode === 'differentiate') {
142
+ const result = { differentiates: null, scenarioCount: scenarios.length, diagnostics };
143
+ if (scenarios.length >= 2) {
144
+ const outputs = [];
145
+ const portals = [];
146
+ for (const scenario of scenarios) {
147
+ document.body.innerHTML = '';
148
+ const mounted = mountOne(scenario.seed ?? {});
149
+ await new Promise((r) => setTimeout(r, 80));
150
+ // `afterMount`: provided once the island is up, as the browser lane does.
151
+ if (scenario.afterMount && typeof mounted?.provide === 'function') {
152
+ for (const [k, v] of Object.entries(scenario.afterMount)) mounted.provide(k, v);
153
+ await new Promise((r) => setTimeout(r, 80));
154
+ }
155
+ const island = findIsland();
156
+ outputs.push(rendered(island));
157
+ portals.push(island ? window.__motuPortals(island).length : 0);
158
+ }
159
+ result.portals = portals;
160
+ // The same judgement as the browser lane (lib/data-flow.mjs): every scenario rendered something
161
+ // or DECLARED it would not (`blank`), and not every output is identical.
162
+ Object.assign(result, judgeRenders(outputs, scenarios));
163
+ // WHICH scenarios collide, not just whether any two do. `size > 1` passes a set where two of
164
+ // three render identically, while the report says "distinct inputs produce distinct output" —
165
+ // claiming more than it tested, and leaving one scenario that is fake evidence by the project's
166
+ // own definition. Named here so the caller can say so.
167
+ result.distinctOutputs = new Set(outputs).size;
168
+ }
169
+ console.error = realConsoleError;
170
+ process.stdout.write(JSON.stringify(result) + '\n');
171
+ process.exit(0);
172
+ }
173
+
174
+ const el = mountOne();
175
+
176
+ // Let the custom-element upgrade + React render flush.
177
+ await new Promise((r) => setTimeout(r, 60));
178
+
179
+ const island = findIsland();
180
+ const renderedHtml = rendered(island);
181
+ const ok = Boolean(island) && renderedHtml.trim().length > 0;
182
+
183
+ // Re-mount identical: dispose this element and mount a fresh one, diff the rendered output.
184
+ //
185
+ // BOTH RENDERS MUST BE SETTLED, or this compares a finished render against a half-finished one. The
186
+ // first mount had 60ms plus whatever the module graph took; the second got a flat 60ms, so any
187
+ // island that loads asynchronously "changed on remount" — reported as accidental module-level state,
188
+ // which is a specific and alarming accusation to make on a timing artefact.
189
+ let remountIdentical = null;
190
+ if (ok) {
191
+ document.body.removeChild(el); // disconnectedCallback -> __motuDispose
192
+ mountOne();
193
+ let previous = null;
194
+ let stable = 0;
195
+ const deadline = Date.now() + 2000;
196
+ for (;;) {
197
+ await new Promise((r) => setTimeout(r, 60));
198
+ const now = rendered(findIsland());
199
+ stable = now === previous ? stable + 1 : 0;
200
+ previous = now;
201
+ if (stable >= 2 || Date.now() > deadline) break;
202
+ }
203
+ remountIdentical = withoutGeneratedIds(previous) === withoutGeneratedIds(renderedHtml);
204
+ }
205
+
206
+ console.error = realConsoleError;
207
+
208
+ process.stdout.write(
209
+ JSON.stringify({
210
+ ok,
211
+ fit,
212
+ mounted: Boolean(island),
213
+ shadowLength: renderedHtml.length,
214
+ diagnostics,
215
+ remountIdentical,
216
+ portals: island ? window.__motuPortals(island).length : 0,
217
+ }) + '\n',
218
+ );
219
+ process.exit(0);
220
+ }
221
+
222
+ main().catch((err) => {
223
+ process.stderr.write(`harness-fatal: ${err?.stack || err}\n`);
224
+ process.exit(3);
225
+ });
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.