@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.
- package/LICENSE +21 -0
- package/bundled/github/agents/archipelago-build.agent.md +298 -0
- package/bundled/github/agents/island-create.agent.md +261 -0
- package/bundled/github/agents/island-extract.agent.md +360 -0
- package/bundled/github/agents/island-locate.agent.md +208 -0
- package/bundled/github/agents/motu-checks.agent.md +158 -0
- package/bundled/github/agents/motu-evidence.agent.md +267 -0
- package/bundled/github/agents/motu-regions.agent.md +304 -0
- package/bundled/github/host-rules.md +43 -0
- package/bundled/github/prompts/archipelago-build.prompt.md +19 -0
- package/bundled/github/prompts/island-create.prompt.md +37 -0
- package/bundled/github/prompts/island-extract.prompt.md +59 -0
- package/bundled/github/prompts/island-locate.prompt.md +43 -0
- package/bundled/packages/adapters/angularjs/vite.mjs +43 -0
- package/bundled/packages/adapters/next/lagoon/next-stubs.tsx +108 -0
- package/bundled/packages/adapters/next/vite.mjs +121 -0
- package/bundled/packages/adapters/vite/vite.mjs +179 -0
- package/package.json +64 -3
- package/src/alias-loader.mjs +63 -0
- package/src/alias-register.mjs +28 -0
- package/src/asset-stub.cjs +3 -0
- package/src/asset-stub.mjs +5 -0
- package/src/cli.mjs +78 -0
- package/src/commands/adopt-root.mjs +194 -0
- package/src/commands/archipelago-init.mjs +314 -0
- package/src/commands/archipelago.mjs +138 -0
- package/src/commands/assurance.mjs +40 -0
- package/src/commands/check.mjs +346 -0
- package/src/commands/codegen.mjs +29 -0
- package/src/commands/coverage.mjs +358 -0
- package/src/commands/create.mjs +277 -0
- package/src/commands/defaults.mjs +129 -0
- package/src/commands/diff.mjs +595 -0
- package/src/commands/fixtures.mjs +153 -0
- package/src/commands/init.mjs +473 -0
- package/src/commands/integrate.mjs +208 -0
- package/src/commands/integration.mjs +1349 -0
- package/src/commands/lagoon-try.mjs +272 -0
- package/src/commands/lagoon.mjs +1419 -0
- package/src/commands/link.mjs +110 -0
- package/src/commands/region-coverage.mjs +604 -0
- package/src/commands/removal-check.mjs +878 -0
- package/src/commands/skills.mjs +156 -0
- package/src/commands/snapshot.mjs +456 -0
- package/src/commands/sync.mjs +38 -0
- package/src/commands/verify/island-runtime.mjs +581 -0
- package/src/commands/verify/island-static.mjs +586 -0
- package/src/commands/verify/island.mjs +135 -0
- package/src/commands/verify/reach.mjs +426 -0
- package/src/commands/verify/region-composition.mjs +391 -0
- package/src/commands/verify/region-config.mjs +330 -0
- package/src/commands/verify/region-flows.mjs +760 -0
- package/src/commands/verify/region-sources.mjs +516 -0
- package/src/commands/verify/region.mjs +271 -0
- package/src/commands/verify/report.mjs +338 -0
- package/src/commands/verify/screens-runtime.mjs +123 -0
- package/src/commands/verify.mjs +18 -0
- package/src/lagoon-build.mjs +25 -0
- package/src/lagoon-dev.mjs +32 -0
- package/src/lib/alias-core.mjs +49 -0
- package/src/lib/archipelagos.mjs +111 -0
- package/src/lib/assurance.mjs +144 -0
- package/src/lib/baselines.mjs +107 -0
- package/src/lib/bundlability.mjs +123 -0
- package/src/lib/changed.mjs +157 -0
- package/src/lib/component-props.mjs +145 -0
- package/src/lib/config.mjs +451 -0
- package/src/lib/contracts.mjs +244 -0
- package/src/lib/data-flow.mjs +47 -0
- package/src/lib/declared-sources.mjs +101 -0
- package/src/lib/dock-inject.mjs +47 -0
- package/src/lib/effects.mjs +97 -0
- package/src/lib/eject.mjs +715 -0
- package/src/lib/evidence-json.mjs +74 -0
- package/src/lib/expect-calls.mjs +53 -0
- package/src/lib/finding-memory.mjs +0 -0
- package/src/lib/flow-calls.mjs +53 -0
- package/src/lib/framework.mjs +98 -0
- package/src/lib/host-rules.mjs +55 -0
- package/src/lib/host-sources.mjs +121 -0
- package/src/lib/island-placement.mjs +64 -0
- package/src/lib/island-render.mjs +105 -0
- package/src/lib/islands.mjs +112 -0
- package/src/lib/lagoon-bundle.mjs +138 -0
- package/src/lib/lagoon-declares.mjs +553 -0
- package/src/lib/lagoon-materialize.mjs +217 -0
- package/src/lib/lagoon-vite.mjs +788 -0
- package/src/lib/live-url.mjs +53 -0
- package/src/lib/node-aliases.mjs +73 -0
- package/src/lib/operations.mjs +145 -0
- package/src/lib/provenance-plugin.mjs +74 -0
- package/src/lib/region-refs.mjs +40 -0
- package/src/lib/remote.mjs +171 -0
- package/src/lib/scaffold.mjs +606 -0
- package/src/lib/screens.mjs +266 -0
- package/src/lib/snapshots.mjs +85 -0
- package/src/lib/stubs.mjs +114 -0
- package/src/lib/ts-project.mjs +42 -0
- package/src/lib/util.mjs +626 -0
- package/src/lib/worlds.mjs +236 -0
- package/src/lib/write-if-changed.mjs +35 -0
- package/src/lib/writes-coverage.mjs +32 -0
- package/src/playwright-lagoon.mjs +2305 -0
- package/src/run.mjs +385 -0
- package/src/runtime-harness.mjs +225 -0
- 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