@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
@@ -0,0 +1,217 @@
1
+ // The lagoon's ENTRIES, rendered by the framework into `.motu/cache` — when the project does not own
2
+ // them.
3
+ //
4
+ // The rule is ownership, not generation: **if the project has its own `index.html`, the project owns
5
+ // its lagoon root and nothing here runs.** That is not a courtesy. The reference ocean's `main.tsx` is
6
+ // ~180 lines of real composition — transport switching between MockTransport and a session-authenticated
7
+ // HttpTransport, an AngularJS host stood up for extracted islands, per-archipelago channels and seeds,
8
+ // tide-line stations — none of which a template could produce. A project whose entries ARE the template
9
+ // (every `motu init --host next` project) carries them for no reason, and those are the ones this
10
+ // deletes.
11
+ //
12
+ // So `motu lagoon eject` is not a special code path: it is this module writing into the project
13
+ // instead of into the cache.
14
+ import { existsSync, mkdirSync, readFileSync, readdirSync } from 'node:fs';
15
+ import { resolve, relative, dirname } from 'node:path';
16
+ import {
17
+ render,
18
+ LAGOON_INDEX_HTML,
19
+ LAGOON_FOCUS_HTML,
20
+ LAGOON_FIXTURES,
21
+ LAGOON_FOCUS_ENTRY,
22
+ LAGOON_GALLERY_ENTRY,
23
+ LAGOON_OVERRIDES,
24
+ ENV_SHIM,
25
+ } from './scaffold.mjs';
26
+ // Every entry below is re-rendered on EVERY lagoon build and is almost always identical. See the
27
+ // module's own doc for why an identical write is not free: Vite watches these, and a touch full-reloads
28
+ // every open lagoon tab.
29
+ import { writeIfChanged } from './write-if-changed.mjs';
30
+
31
+ /** POSIX-style relative path for generated code (never a Windows backslash in a module specifier). */
32
+ function relPosix(from, to) {
33
+ const r = relative(from, to).split('\\').join('/');
34
+ return r === '' ? '.' : r.startsWith('.') ? r : './' + r;
35
+ }
36
+
37
+ /** The host application's own global stylesheet — without it the lagoon has no theme tokens and every
38
+ * island renders unstyled, a preview that lies about how the component looks in the app. */
39
+ const HOST_GLOBAL_CSS = ['app/globals.css', 'src/app/globals.css', 'styles/globals.css', 'src/styles/globals.css'];
40
+
41
+ /**
42
+ * The stylesheet that DECLARES Tailwind, found by content when it is not at a conventional path.
43
+ *
44
+ * Tailwind v4 moved the config into CSS (`@import "tailwindcss"`, `@theme { … }`) and dropped
45
+ * `tailwind.config.js` — so a v4 project has no config file to detect AND commonly keeps its global
46
+ * stylesheet outside the four paths above. Measured on a cold adoption of a v4 Next app whose CSS
47
+ * lives at `modules/ui/globals.css`: motu found neither, wired no styles, and the lagoon rendered
48
+ * every island with correct text, structure and coupling and NO styling at all — silently. The agent
49
+ * who hit it could not tell a preview bug from an application bug, which is the one thing a preview
50
+ * must never make ambiguous.
51
+ *
52
+ * Bounded, shallow, and skipping the obvious noise: this runs on every lagoon build.
53
+ */
54
+ function findTailwindCss(hostRoot, depth = 4) {
55
+ const SKIP = new Set(['node_modules', '.next', '.git', 'dist', 'build', 'coverage', '.motu', '.turbo']);
56
+ const queue = [{ dir: hostRoot, d: 0 }];
57
+ while (queue.length) {
58
+ const { dir, d } = queue.shift();
59
+ let entries;
60
+ try {
61
+ entries = readdirSync(dir, { withFileTypes: true });
62
+ } catch {
63
+ continue;
64
+ }
65
+ for (const e of entries) {
66
+ if (e.isDirectory()) {
67
+ if (d < depth && !SKIP.has(e.name) && !e.name.startsWith('.')) queue.push({ dir: resolve(dir, e.name), d: d + 1 });
68
+ continue;
69
+ }
70
+ if (!e.name.endsWith('.css')) continue;
71
+ const abs = resolve(dir, e.name);
72
+ try {
73
+ const css = readFileSync(abs, 'utf8');
74
+ if (/@import\s+['"]tailwindcss|@tailwind\s+(base|utilities)/.test(css)) return abs;
75
+ } catch {}
76
+ }
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function hostGlobalCssImport(fromDir, hostRoot) {
82
+ for (const rel of HOST_GLOBAL_CSS) {
83
+ const abs = resolve(hostRoot, rel);
84
+ if (existsSync(abs)) return `import '${relPosix(fromDir, abs)}';\n`;
85
+ }
86
+ const found = findTailwindCss(hostRoot);
87
+ if (found) return `import '${relPosix(fromDir, found)}';\n`;
88
+ return '';
89
+ }
90
+
91
+ /** Does this project own its lagoon entries? `index.html` is the marker: it is what Vite's root means. */
92
+ export function projectOwnsLagoon(paths) {
93
+ return existsSync(resolve(paths.lagoonDir, 'index.html'));
94
+ }
95
+
96
+ /**
97
+ * Render the entries into `outDir`, wiring every import back to the files the PROJECT still owns
98
+ * (`lagoon.config.json`, `src/lagoon.ts`, `src/host-stubs/`, the islands' fixtures, the host's global
99
+ * CSS). Returns the directory to use as Vite's root.
100
+ */
101
+ export function materializeLagoon(paths, outDir) {
102
+ const src = resolve(outDir, 'src');
103
+ mkdirSync(src, { recursive: true });
104
+
105
+ // The project keeps its overrides (region seeds). A project without them gets an empty stub, so the
106
+ // generated gallery entry's import resolves either way.
107
+ // `.tsx` when the project's overrides carry JSX — a region layout does.
108
+ const overrideCandidates = ['src/lagoon.tsx', 'src/lagoon.ts'].map((f) => resolve(paths.lagoonDir, f));
109
+ const projectOverrides = overrideCandidates.find((f) => existsSync(f)) ?? overrideCandidates[1];
110
+ if (!existsSync(projectOverrides)) writeIfChanged(resolve(src, 'lagoon.ts'), LAGOON_OVERRIDES);
111
+
112
+ const vars = {
113
+ appPackage: paths.appPackage,
114
+ tagPrefix: paths.tagPrefix,
115
+ // A glob cannot go through a tsconfig alias reliably, so it is baked relative to the file.
116
+ // Both evidence layouts. Vite accepts an array of patterns, so a project can migrate island by
117
+ // island without the lagoon losing sight of the ones that have not moved yet.
118
+ fixturesGlob: `['${relPosix(src, paths.islandsDir)}/*.evidence.ts', '${relPosix(src, paths.islandsDir)}/*/fixtures.mock.ts']`,
119
+ // A REGION's evidence, kept in its own glob: it holds the flows a URL can address, and its
120
+ // `fixtures` must NOT join the mock transport's corpus (that would change what every existing
121
+ // check replays, from a change that was supposed to only add an address).
122
+ flowsGlob: `'${relPosix(src, paths.archipelagosDir)}/*/*.evidence.ts'`,
123
+ framesGlob: relPosix(src, resolve(paths.lagoonDir, 'src/frames')) + '/*.css',
124
+ lagoonConfigImport: relPosix(src, resolve(paths.lagoonDir, 'lagoon.config.json')),
125
+ overridesImport: existsSync(projectOverrides) ? relPosix(src, projectOverrides) : './lagoon.js',
126
+ fixturesImport: './fixtures.js',
127
+ galleryEntrySrc: '/src/main.tsx',
128
+ focusEntrySrc: '/src/lagoon.tsx',
129
+ hostGlobalCss: hostGlobalCssImport(src, paths.hostRoot),
130
+ // FIRST import in both entries: host modules read process.env while being imported, so the shim
131
+ // has to land before any of them is evaluated. An ocean has no such modules and needs no shim.
132
+ envShim: paths.host === 'angularjs' ? '' : "import './env.js';\n",
133
+ hostImport: paths.host === 'angularjs' ? "import { angularHostScopeChannel } from '@motujs/adapter-angularjs';\n" : '',
134
+ hostOption: '',
135
+ };
136
+
137
+ writeIfChanged(resolve(outDir, 'index.html'), render(LAGOON_INDEX_HTML, vars));
138
+ writeIfChanged(resolve(outDir, 'lagoon.html'), render(LAGOON_FOCUS_HTML, vars));
139
+ writeIfChanged(resolve(src, 'fixtures.ts'), render(LAGOON_FIXTURES, vars));
140
+ writeIfChanged(resolve(src, 'main.tsx'), render(LAGOON_GALLERY_ENTRY, vars));
141
+ writeIfChanged(resolve(src, 'lagoon.tsx'), render(LAGOON_FOCUS_ENTRY, vars));
142
+ if (paths.host !== 'angularjs') writeIfChanged(resolve(src, 'env.ts'), render(ENV_SHIM, vars));
143
+
144
+ return outDir;
145
+ }
146
+
147
+ /** Vite's root for this project: its own lagoon dir if it owns one, else a freshly rendered cache. */
148
+ export function resolveLagoonRoot(paths) {
149
+ if (projectOwnsLagoon(paths)) return paths.lagoonDir;
150
+ return materializeLagoon(paths, resolve(paths.cacheDir, 'lagoon'));
151
+ }
152
+
153
+ /**
154
+ * WHAT AN EJECTED ENTRY IS MISSING, compared with the one motu would write today.
155
+ *
156
+ * Ejecting hands the project its entry and stops the materializer: "the project now owns these —
157
+ * motu will not regenerate them." That is the point of the escape hatch, and it has a cost nobody was
158
+ * told about. Every capability added to the scaffold afterwards reaches an ejected project only if
159
+ * somebody edits that file by hand, and nothing said when they had not.
160
+ *
161
+ * It has already happened twice in this repo's own demo-app: `evidence` (so `?scenario=` addressed a
162
+ * catalogue that was never handed over — every declared state unreachable in a browser) and
163
+ * `lens.onPicked` (so the crosshair could not scope). Both were silent: the entry compiled, the
164
+ * lagoon booted, and the feature simply was not there.
165
+ *
166
+ * DERIVED FROM THE TEMPLATE, never a hand-kept list, or this check becomes the next thing that goes
167
+ * stale. Compares the option keys `startLagoon({...})` is given, and the keys of the `lens` object
168
+ * inside it — the two places options actually arrive.
169
+ */
170
+ export function ejectedEntryGaps(paths) {
171
+ const entry = resolve(paths.lagoonDir, 'src', 'main.tsx');
172
+ if (!projectOwnsLagoon(paths) || !existsSync(entry)) return null;
173
+
174
+ // NAME-PRESENCE, not shape. Parsing both sides was the first attempt and it was wrong twice over:
175
+ // it read an `import.meta.glob` options object as lagoon options, and it missed demo-app's
176
+ // `lens: __MOTU_DEBUG__ ? {…} : undefined` because that is not a literal `lens: {`. An ejected entry
177
+ // is a file somebody has been editing — it may wrap, rename or conditionalise anything — so the
178
+ // only question worth asking cheaply is whether the capability is MENTIONED at all. That is enough
179
+ // to catch what actually goes wrong here (an option added upstream that nobody copied down), and it
180
+ // cannot cry wolf over formatting.
181
+ // FULL-LINE COMMENTS ONLY. Stripping block comments with a regex ate real code here (a `/*` in a
182
+ // string closed against a later `*/`), and prose is where a key name would otherwise be mistaken
183
+ // for a use — this file's own comments mention `target` and `scenario` while explaining them.
184
+ const strip = (t) => t.replace(/^\s*\/\/.*$/gm, '');
185
+ const mine = strip(readFileSync(entry, 'utf8'));
186
+ const template = strip(LAGOON_GALLERY_ENTRY);
187
+
188
+ const optionNames = (text, opener) => {
189
+ const at = text.indexOf(opener);
190
+ if (at === -1) return [];
191
+ const out = new Set();
192
+ let depth = 0;
193
+ for (let i = at + opener.length - 1; i < text.length; i++) {
194
+ const c = text[i];
195
+ if (c === '{' || c === '(' || c === '[') depth++;
196
+ else if (c === '}' || c === ')' || c === ']') {
197
+ if (--depth === 0) break;
198
+ } else if (depth === 1 && /[A-Za-z_$]/.test(c)) {
199
+ const before = text.slice(0, i).replace(/\s+$/, '').slice(-1);
200
+ const m = /^[A-Za-z_$][\w$]*/.exec(text.slice(i));
201
+ if (m && (before === '{' || before === ',')) {
202
+ const after = text.slice(i + m[0].length).replace(/^\s+/, '').slice(0, 1);
203
+ if (after === ':' || after === ',' || after === '}') out.add(m[0]);
204
+ i += m[0].length - 1;
205
+ }
206
+ }
207
+ }
208
+ return [...out];
209
+ };
210
+
211
+ const mentions = (name) => new RegExp(`\\b${name}\\b`).test(mine);
212
+ const expected = [...optionNames(template, 'startLagoon({'), ...optionNames(template, 'lens: {')];
213
+ return {
214
+ entry: relative(process.cwd(), entry),
215
+ missing: [...new Set(expected)].filter((k) => !mentions(k)).sort(),
216
+ };
217
+ }