@motujs/cli 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// `motu fixtures record <island>` — capture real (or mock) backend responses into request-keyed
|
|
2
|
+
// fixtures. It boots the focused lagoon, drives the island's declared `scenarios` through the
|
|
3
|
+
// archipelago boundary, and records each contract call's request + response at the `call()` seam.
|
|
4
|
+
// With `--transport http` it records the REAL backend (the point of it); default records the mock
|
|
5
|
+
// (a self-consistency check of the pipeline). This closes the "fixtures are hand-written / can't
|
|
6
|
+
// capture semantic fidelity" gap — the recorder replays real truth, keyed by request, without
|
|
7
|
+
// reimplementing any backend logic.
|
|
8
|
+
import { parseEvidence } from '../lib/evidence-json.mjs';
|
|
9
|
+
import { existsSync, writeFileSync } from 'node:fs';
|
|
10
|
+
import { spawnSync } from 'node:child_process';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
import { dirname, resolve } from 'node:path';
|
|
13
|
+
import { paths, names, color } from '../lib/util.mjs';
|
|
14
|
+
import { recordLagoon } from '../playwright-lagoon.mjs';
|
|
15
|
+
import { scenarioInputs } from '../lib/data-flow.mjs';
|
|
16
|
+
import { CHROMIUM_INSTALL } from '../lib/framework.mjs';
|
|
17
|
+
|
|
18
|
+
const HARNESS = resolve(dirname(fileURLToPath(import.meta.url)), '../runtime-harness.mjs');
|
|
19
|
+
const CLI_PKG = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
|
|
21
|
+
/** Read the island's declared `scenarios` via the harness (loads only the fixtures file). */
|
|
22
|
+
function readScenarios(fixturesPath) {
|
|
23
|
+
if (!fixturesPath || !existsSync(fixturesPath)) return [];
|
|
24
|
+
const args = ['--import', 'tsx', HARNESS, '', fixturesPath, 'native', 'scenarios'];
|
|
25
|
+
const res = spawnSync(process.execPath, args, { encoding: 'utf8', cwd: CLI_PKG });
|
|
26
|
+
if (res.status !== 0) return [];
|
|
27
|
+
const line = (res.stdout || '').trim().split('\n').filter(Boolean).pop();
|
|
28
|
+
// See lib/evidence-json.mjs — the harness marks Dates so they survive this hop.
|
|
29
|
+
return parseEvidence(line)?.scenarios ?? [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Stable JSON (sorted object keys) for dedup keys. */
|
|
33
|
+
function stable(v) {
|
|
34
|
+
return JSON.stringify(v, (_k, val) =>
|
|
35
|
+
val && typeof val === 'object' && !Array.isArray(val)
|
|
36
|
+
? Object.fromEntries(Object.keys(val).sort().map((k) => [k, val[k]]))
|
|
37
|
+
: val,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function fixturesRecordCommand(argv) {
|
|
42
|
+
const name = argv._[0];
|
|
43
|
+
if (!name) {
|
|
44
|
+
console.error('usage: motu fixtures record <island> [--transport http|mock] [--out <path>]');
|
|
45
|
+
process.exit(2);
|
|
46
|
+
}
|
|
47
|
+
const { kebab, tag } = names(name);
|
|
48
|
+
const fixturesPath = paths.fixturesFile(kebab);
|
|
49
|
+
const scenarios = readScenarios(fixturesPath);
|
|
50
|
+
if (scenarios.length === 0) {
|
|
51
|
+
console.error(color.red(`✗ ${kebab} declares no \`scenarios\` in fixtures.mock.ts — add at least one { seed } to record against.`));
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const transport = argv.transport === 'http' ? 'http' : undefined; // default: mock
|
|
56
|
+
console.log(color.dim(`recording ${tag} against the ${transport ?? 'mock'} transport over ${scenarios.length} scenario(s)…`));
|
|
57
|
+
const port = 5400 + Math.floor(Math.random() * 300);
|
|
58
|
+
|
|
59
|
+
let result;
|
|
60
|
+
try {
|
|
61
|
+
result = await recordLagoon({ tag, port, scenarios, transport });
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const msg = String(err?.message || err).split('\n')[0];
|
|
64
|
+
if (/Executable doesn't exist|playwright install/i.test(msg)) {
|
|
65
|
+
console.error(color.red(`✗ Chromium not installed — run ${CHROMIUM_INSTALL}`));
|
|
66
|
+
} else {
|
|
67
|
+
console.error(color.red(`✗ recording failed: ${msg}`));
|
|
68
|
+
}
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
if (!result.mounted) {
|
|
72
|
+
console.error(color.red('✗ island did not mount — nothing recorded'));
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Dedupe captured calls by (service, method, args).
|
|
77
|
+
const seen = new Set();
|
|
78
|
+
const unique = [];
|
|
79
|
+
for (const c of result.calls) {
|
|
80
|
+
const key = `${c.service}.${c.method}(${stable(c.args)})`;
|
|
81
|
+
if (seen.has(key)) continue;
|
|
82
|
+
seen.add(key);
|
|
83
|
+
unique.push(c);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Reduce host-fed store writes (channels + provide) to a last-wins seed, MINUS the keys the
|
|
87
|
+
// scenarios themselves drove (those are inputs, not host config).
|
|
88
|
+
const scenarioKeys = new Set(scenarios.flatMap((s) => Object.keys(scenarioInputs(s))));
|
|
89
|
+
const seed = {};
|
|
90
|
+
for (const w of result.seedWrites ?? []) {
|
|
91
|
+
if (scenarioKeys.has(w.key)) continue;
|
|
92
|
+
seed[w.key] = w.value;
|
|
93
|
+
}
|
|
94
|
+
const seedKeys = Object.keys(seed);
|
|
95
|
+
|
|
96
|
+
if (unique.length === 0 && seedKeys.length === 0) {
|
|
97
|
+
console.error(color.red('✗ nothing captured — no contract calls and no host-fed store writes'));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const outPath = argv.out ? resolve(process.cwd(), argv.out) : resolve(paths.islandDir(kebab), 'fixtures.recorded.ts');
|
|
102
|
+
writeFileSync(outPath, renderFixtures(kebab, unique, seed, transport));
|
|
103
|
+
console.log(color.green(`✓ wrote ${unique.length} fixture(s)${seedKeys.length ? ` + ${seedKeys.length} seed key(s)` : ''}`) + color.dim(` -> ${outPath}`));
|
|
104
|
+
for (const c of unique) {
|
|
105
|
+
console.log(' ' + color.dim(`${c.service}.${c.method} match=${stable(c.args)}${c.status ? ` (status ${c.status})` : ''}`));
|
|
106
|
+
}
|
|
107
|
+
for (const k of seedKeys) console.log(' ' + color.dim(`seed.${k} = ${preview(seed[k])}`));
|
|
108
|
+
console.log(color.dim('\nReview, then merge into fixtures.mock.ts (fixtures) and the lagoon seed (seed) to replay this session offline.'));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A short one-line preview of a captured value. */
|
|
112
|
+
function preview(v) {
|
|
113
|
+
try {
|
|
114
|
+
const s = JSON.stringify(v);
|
|
115
|
+
return s && s.length > 60 ? s.slice(0, 57) + '…' : String(s);
|
|
116
|
+
} catch {
|
|
117
|
+
return String(v);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function renderFixtures(kebab, calls, seed, transport) {
|
|
122
|
+
const rows = calls.map((c) => {
|
|
123
|
+
const match = JSON.stringify(c.args);
|
|
124
|
+
// A CAPTURED FAILURE IS A FIXTURE NOW. This emitted the status as a commented line with
|
|
125
|
+
// `response: null`, because `Fixture` could not express a failing call — so recording a real 500
|
|
126
|
+
// through `--transport http` produced something nobody could run. `FixtureFailure` is the form it
|
|
127
|
+
// was reaching for.
|
|
128
|
+
if (c.status) {
|
|
129
|
+
return ` { service: ${JSON.stringify(c.service)}, method: ${JSON.stringify(c.method)}, match: ${match}, status: ${c.status} },`;
|
|
130
|
+
}
|
|
131
|
+
const response = indent(JSON.stringify(c.response, null, 2), 4);
|
|
132
|
+
return ` { service: ${JSON.stringify(c.service)}, method: ${JSON.stringify(c.method)}, match: ${match}, response: ${response} },`;
|
|
133
|
+
});
|
|
134
|
+
const seedBlock =
|
|
135
|
+
Object.keys(seed).length > 0
|
|
136
|
+
? `\n// Host-fed store values (channels + provide) captured this session — pass as the lagoon seed so\n// the island receives REAL host config offline, not a hand-written stub.\nexport const seed: Record<string, unknown> = ${indent(JSON.stringify(seed, null, 2), 0)};\n`
|
|
137
|
+
: '';
|
|
138
|
+
return `// RECORDED fixtures for x-${kebab} — captured from the ${transport ?? 'mock'} transport by \`motu fixtures record\`.
|
|
139
|
+
// Request-keyed (\`match\` = the exact call args). Merge the ones you want into fixtures.mock.ts.
|
|
140
|
+
import type { Fixture } from '@motujs/runtime/mock';
|
|
141
|
+
|
|
142
|
+
export const fixtures: Fixture[] = [
|
|
143
|
+
${rows.join('\n')}
|
|
144
|
+
];
|
|
145
|
+
${seedBlock}`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Re-indent a multi-line JSON blob so nested lines align under the fixture row. */
|
|
149
|
+
function indent(json, spaces) {
|
|
150
|
+
if (json == null) return 'null';
|
|
151
|
+
const pad = ' '.repeat(spaces);
|
|
152
|
+
return json.split('\n').map((l, i) => (i === 0 ? l : pad + l)).join('\n');
|
|
153
|
+
}
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
// `motu init [dir]` — bootstrap a project so `motu island create` / `verify` work immediately.
|
|
2
|
+
//
|
|
3
|
+
// It writes motu.config.json (the layout declaration the rest of the CLI reads), the semantic roots +
|
|
4
|
+
// registries + barrel that islands slot into, AND the lagoon root. That last part is the point: the
|
|
5
|
+
// lagoon is where `motu island verify` closes the loop, so a project scaffolded without one gets the
|
|
6
|
+
// static checks and none of the verification. Init used to stop before it, on the grounds that a
|
|
7
|
+
// composition root is "a project concern" — but the result was a documented happy path (init ->
|
|
8
|
+
// island create -> island verify) that could not reach green without hand-porting the demo app.
|
|
9
|
+
//
|
|
10
|
+
// `--host` picks the stack the islands embed INTO, which decides what the lagoon has to speak:
|
|
11
|
+
// angularjs the reference ocean — legacy fit gates apply, AngularJS adapter available
|
|
12
|
+
// next a Next.js app — the lagoon inherits the host's '@/…' alias + Tailwind, stubs next/*
|
|
13
|
+
// none plain React — nothing host-specific
|
|
14
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
15
|
+
import { basename, dirname, relative, resolve } from 'node:path';
|
|
16
|
+
import { color } from '../lib/util.mjs';
|
|
17
|
+
import { applyHostRules } from '../lib/host-rules.mjs';
|
|
18
|
+
import { installSkills } from './skills.mjs';
|
|
19
|
+
import { CLI_VERSION, INSTALLED } from '../lib/framework.mjs';
|
|
20
|
+
import {
|
|
21
|
+
render,
|
|
22
|
+
ISLANDS_REGISTRY,
|
|
23
|
+
ARCHIPELAGOS_REGISTRY,
|
|
24
|
+
BARREL,
|
|
25
|
+
APP_PACKAGE_JSON,
|
|
26
|
+
SHARED_STYLES,
|
|
27
|
+
GITIGNORE,
|
|
28
|
+
LAGOON_PACKAGE_JSON,
|
|
29
|
+
LAGOON_INDEX_HTML,
|
|
30
|
+
LAGOON_FOCUS_HTML,
|
|
31
|
+
LAGOON_FIXTURES,
|
|
32
|
+
LAGOON_FOCUS_ENTRY,
|
|
33
|
+
LAGOON_GALLERY_ENTRY,
|
|
34
|
+
LAGOON_CONFIG,
|
|
35
|
+
LAGOON_CHROME_SHADCN,
|
|
36
|
+
LAGOON_OVERRIDES,
|
|
37
|
+
NEXT_STUBS,
|
|
38
|
+
ENV_SHIM,
|
|
39
|
+
NEXT_VITE_IMPORTS,
|
|
40
|
+
NEXT_VITE_ALIASES,
|
|
41
|
+
NEXT_VITE_CSS,
|
|
42
|
+
NEXT_TAILWIND_CONFIG,
|
|
43
|
+
} from '../lib/scaffold.mjs';
|
|
44
|
+
|
|
45
|
+
/** What a host's Tailwind config can be called. Mirrors @motujs/adapter-next's own list. */
|
|
46
|
+
const TAILWIND_CONFIGS = ['tailwind.config.ts', 'tailwind.config.js', 'tailwind.config.mjs', 'tailwind.config.cjs'];
|
|
47
|
+
|
|
48
|
+
/** Framework entry points, mapped by module specifier — @motujs/* are raw TS with no build step. */
|
|
49
|
+
const MOTU_ENTRIES = {
|
|
50
|
+
// The ONE motu module application code may import — types only, so it erases at compile time and
|
|
51
|
+
// the app still ships byte-identical output without motu.
|
|
52
|
+
'@motujs/types': 'packages/types/src/index.ts',
|
|
53
|
+
'@motujs/core': 'packages/core/src/index.ts',
|
|
54
|
+
'@motujs/react': 'packages/react/src/index.ts',
|
|
55
|
+
'@motujs/react/define': 'packages/react/src/defineReactElement.ts',
|
|
56
|
+
'@motujs/runtime/mock': 'packages/runtime/src/mock.ts',
|
|
57
|
+
'@motujs/runtime': 'packages/runtime/src/index.ts',
|
|
58
|
+
'@motujs/debug-overlay': 'packages/debug-overlay/src/index.ts',
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
const HOST_ADAPTER_ENTRY = {
|
|
62
|
+
angularjs: ['@motujs/adapter-angularjs', 'packages/adapters/angularjs/src/index.ts'],
|
|
63
|
+
next: ['@motujs/adapter-next', 'packages/adapters/next/src/index.ts'],
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Vite aliases resolving the framework straight to THIS checkout's sources.
|
|
68
|
+
*
|
|
69
|
+
* The @motujs/* packages are unpublished workspace packages whose entry point is raw TypeScript, so an
|
|
70
|
+
* existing project cannot simply depend on them — that is the single thing that blocks adopting motu
|
|
71
|
+
* outside its own repo. The lagoon is a Vite app and transpiles TS anyway, so pointing it at the
|
|
72
|
+
* sources removes the package-manager problem entirely: `motu init` produces a lagoon that boots with
|
|
73
|
+
* no install step. Longest specifier first, so '@motujs/runtime/mock' is not eaten by '@motujs/runtime'.
|
|
74
|
+
*/
|
|
75
|
+
/** Escape a module specifier for use inside a RegExp literal. */
|
|
76
|
+
function reEscape(spec) {
|
|
77
|
+
return spec.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** An anchored, exact-match alias pattern for one module specifier. */
|
|
81
|
+
function exact(spec) {
|
|
82
|
+
return `/^${reEscape(spec)}$/`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function motuAliases(lagoonDir, appRoot, appPackage, host) {
|
|
86
|
+
const entries = Object.entries(MOTU_ENTRIES);
|
|
87
|
+
const adapter = HOST_ADAPTER_ENTRY[host];
|
|
88
|
+
if (adapter) entries.push([adapter[0], adapter[1]]);
|
|
89
|
+
const lines = entries.map(([spec, rel]) => ` { find: ${exact(spec)}, replacement: motu('${rel}') },`);
|
|
90
|
+
// The project's own barrel + stylesheet, imported by name so generated code never carries a
|
|
91
|
+
// relative path into the app. The stylesheet pattern is intentionally NOT end-anchored: it is
|
|
92
|
+
// imported as '<pkg>/styles.css?inline', and the query has to survive the rewrite.
|
|
93
|
+
lines.push(
|
|
94
|
+
` { find: /^${reEscape(appPackage)}\\/styles\\.css/, replacement: resolve(__dirname, '${relPosix(lagoonDir, resolve(appRoot, 'src/shared/styles.css'))}') },`,
|
|
95
|
+
);
|
|
96
|
+
lines.push(
|
|
97
|
+
` { find: ${exact(appPackage)}, replacement: resolve(__dirname, '${relPosix(lagoonDir, resolve(appRoot, 'src/index.ts'))}') },`,
|
|
98
|
+
);
|
|
99
|
+
return lines.join('\n') + '\n';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Where a Next/React app conventionally keeps the global stylesheet Tailwind is pulled in through. */
|
|
103
|
+
const HOST_GLOBAL_CSS = ['app/globals.css', 'src/app/globals.css', 'styles/globals.css', 'src/styles/globals.css'];
|
|
104
|
+
|
|
105
|
+
/** `import '<host globals.css>';` for the lagoon entries, or '' when the host has no such file. */
|
|
106
|
+
function hostGlobalCssImport(lagoonDir, hostRoot) {
|
|
107
|
+
for (const rel of HOST_GLOBAL_CSS) {
|
|
108
|
+
const abs = resolve(hostRoot, rel);
|
|
109
|
+
if (existsSync(abs)) return `import '${relPosix(resolve(lagoonDir, 'src'), abs)}';\n`;
|
|
110
|
+
}
|
|
111
|
+
return '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// 'vite' was supported everywhere EXCEPT here: the adapter exists (packages/adapters/vite/vite.mjs),
|
|
115
|
+
// `hostContribution` resolves it, `loadMotuConfig` accepts it, and Twenty runs on it — but the one
|
|
116
|
+
// command that starts a project rejected it, so the only way onto a vite host was to hand-write
|
|
117
|
+
// motu.config.json. Found on the first greenfield init after adoption stopped needing a checkout path.
|
|
118
|
+
const HOSTS = new Set(['angularjs', 'next', 'none', 'vite']);
|
|
119
|
+
|
|
120
|
+
/** Hosts with a legacy skin an island must be proven to survive (mirrors lib/config.mjs). */
|
|
121
|
+
const LEGACY_FIT_HOSTS = new Set(['angularjs']);
|
|
122
|
+
|
|
123
|
+
/** POSIX-style relative path for generated code (never a Windows backslash in a module specifier). */
|
|
124
|
+
function relPosix(from, to) {
|
|
125
|
+
const r = relative(from, to).split('\\').join('/');
|
|
126
|
+
return r === '' ? '.' : r.startsWith('.') ? r : './' + r;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Say when the app's OWN package.json was kept and the `@motujs/*` block therefore never landed.
|
|
131
|
+
*
|
|
132
|
+
* `writeNew` is non-destructive per file, which is right — but it made the one file EVERY real
|
|
133
|
+
* adoption already has, `package.json`, silently skip the dependency block with it. The scaffold is
|
|
134
|
+
* written for a greenfield project; an existing app is the normal case, and there the block is not
|
|
135
|
+
* "kept", it is ABSENT. Measured on a cold adoption of a Next monorepo: no `@motujs/adapter-next`
|
|
136
|
+
* anywhere, `nextHostBridge` unresolvable, and `init` printed a green success line.
|
|
137
|
+
*
|
|
138
|
+
* A notice rather than a merge: editing an application's package.json is the host's call, and a
|
|
139
|
+
* printed block a human can paste is honest about which side owns the file.
|
|
140
|
+
*/
|
|
141
|
+
function keptPackageJsonNotice(appRoot, skipped, motuDependencies) {
|
|
142
|
+
// NO WORKSPACE TO SATISFY `@motujs/*` — say what DOES resolve them, because nothing else will.
|
|
143
|
+
//
|
|
144
|
+
// On a single-package project the dependency block is deliberately not written (a `workspace:*`
|
|
145
|
+
// range nothing can resolve breaks the next `npm install` outright). The consequence, measured on a
|
|
146
|
+
// cold adoption of an npm project: `motu init` changed ZERO lines of package.json, so the only
|
|
147
|
+
// record of motu's presence was raw symlinks in `node_modules/@motujs/*` — which a fresh
|
|
148
|
+
// `npm install` deletes, leaving nothing to say what was needed or why the build then fails.
|
|
149
|
+
// The CLI re-creates them on every invocation, so this is legibility, not breakage.
|
|
150
|
+
if (!motuDependencies) {
|
|
151
|
+
// THERE IS A FIX NOW, so this stops describing the hazard and names it.
|
|
152
|
+
//
|
|
153
|
+
// This used to end with "after `npm install` wipes them, any motu command puts them back", which
|
|
154
|
+
// is true and is not a fix: the person who runs `npm install` is usually not the person who runs
|
|
155
|
+
// a motu command next. Measured on shlink — `npm install` printed `removed 9 packages` and the
|
|
156
|
+
// host build then failed with `Cannot find module '@motujs/react'`, an error pointing nowhere near
|
|
157
|
+
// the cause. CI and a fresh clone hit it every time.
|
|
158
|
+
console.log(color.yellow(' ! nothing in package.json records @motujs/*, and `npm install` deletes them as extraneous.'));
|
|
159
|
+
console.log(color.dim(' Record them as `file:` paths, which npm creates itself and never prunes:'));
|
|
160
|
+
console.log('');
|
|
161
|
+
console.log(' motu link');
|
|
162
|
+
console.log('');
|
|
163
|
+
console.log(color.dim(' (`motu link --check` asserts it, for CI. A workspace project needs neither.)'));
|
|
164
|
+
console.log('');
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const pkgPath = resolve(appRoot, 'package.json');
|
|
168
|
+
if (!skipped.includes(pkgPath)) return;
|
|
169
|
+
let already = false;
|
|
170
|
+
try {
|
|
171
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
172
|
+
already = Object.keys(pkg.dependencies ?? {}).some((d) => d.startsWith('@motujs/'));
|
|
173
|
+
} catch {}
|
|
174
|
+
if (already) return;
|
|
175
|
+
console.log(color.yellow(' ! package.json was kept, so the @motujs/* dependencies were NOT added.'));
|
|
176
|
+
console.log(color.dim(' The lagoon and tsconfig resolve @motujs/* without them, and the CLI links'));
|
|
177
|
+
console.log(color.dim(" node_modules/@motujs/* on every run — so this is not fatal. It matters when the"));
|
|
178
|
+
console.log(color.dim(' HOST bundler resolves them (a host adapter import). Add to its "dependencies":'));
|
|
179
|
+
console.log('');
|
|
180
|
+
for (const line of motuDependencies.split('\n').slice(1, -2)) console.log(color.dim(' ' + line.trim()));
|
|
181
|
+
console.log('');
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Add the `@motujs/*` an installed project needs to its package.json, never touching one it already
|
|
186
|
+
* declares. Returns `"<field>: <name>@<range>"` for each line added.
|
|
187
|
+
*
|
|
188
|
+
* Runtime packages go in `dependencies` — islands ship in the app's bundle. `@motujs/debug-overlay` is
|
|
189
|
+
* only ever imported by the lagoon, so it is a devDependency, beside @motujs/cli itself.
|
|
190
|
+
*/
|
|
191
|
+
function recordInstalledDeps(pkgPath, range, host) {
|
|
192
|
+
let pkg;
|
|
193
|
+
try {
|
|
194
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
|
|
195
|
+
} catch {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
const adapter = host === 'angularjs' ? '@motujs/adapter-angularjs' : host === 'next' ? '@motujs/adapter-next' : null;
|
|
199
|
+
const wanted = {
|
|
200
|
+
dependencies: ['@motujs/core', '@motujs/react', '@motujs/runtime', ...(adapter ? [adapter] : [])],
|
|
201
|
+
devDependencies: ['@motujs/debug-overlay'],
|
|
202
|
+
};
|
|
203
|
+
const added = [];
|
|
204
|
+
for (const [field, names] of Object.entries(wanted)) {
|
|
205
|
+
for (const name of names) {
|
|
206
|
+
if (pkg.dependencies?.[name] || pkg.devDependencies?.[name] || pkg.peerDependencies?.[name]) continue;
|
|
207
|
+
(pkg[field] ??= {})[name] = range;
|
|
208
|
+
added.push(`${field}: ${name}@${range}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (added.length) writeFileSync(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
212
|
+
return added;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function writeNew(path, contents, created, skipped) {
|
|
216
|
+
if (existsSync(path)) {
|
|
217
|
+
skipped.push(path);
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
mkdirSync(resolve(path, '..'), { recursive: true });
|
|
221
|
+
writeFileSync(path, contents);
|
|
222
|
+
created.push(path);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export async function initCommand(argv) {
|
|
227
|
+
const root = resolve(process.cwd(), argv._[0] ?? '.');
|
|
228
|
+
const host = argv.host === true || argv.host === undefined ? 'none' : String(argv.host);
|
|
229
|
+
if (!HOSTS.has(host)) {
|
|
230
|
+
console.error(color.red(`✗ unknown --host '${host}' — expected one of: ${[...HOSTS].join(', ')}`));
|
|
231
|
+
process.exit(2);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const configPath = resolve(root, 'motu.config.json');
|
|
235
|
+
if (existsSync(configPath) && !argv.force) {
|
|
236
|
+
console.error(color.red(`✗ ${basename(root)}/motu.config.json already exists — use --force to overwrite`));
|
|
237
|
+
process.exit(1);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Layout. `app` is the sub-package holding islands/ui/archipelagos; `hostRoot` is where the host
|
|
241
|
+
// application lives (for `next`, the Next app whose aliases/Tailwind the lagoon borrows).
|
|
242
|
+
const appRel = argv.app ? String(argv.app) : '.';
|
|
243
|
+
const hostRel = argv.hostRoot ? String(argv.hostRoot) : appRel;
|
|
244
|
+
const appRoot = resolve(root, appRel);
|
|
245
|
+
const hostRoot = resolve(root, hostRel);
|
|
246
|
+
const lagoonRel = 'roots/lagoon';
|
|
247
|
+
const lagoonDir = resolve(appRoot, lagoonRel);
|
|
248
|
+
const appPackage = argv.appPackage ? String(argv.appPackage) : basename(appRoot) === '.' ? 'motu-app' : basename(appRoot);
|
|
249
|
+
|
|
250
|
+
// Islands mount directly in a React host — shadow DOM would cut them off from the host's own
|
|
251
|
+
// stylesheet (Tailwind included), which is the opposite of what a Next island wants.
|
|
252
|
+
const isolation = argv.isolation ? String(argv.isolation) : host === 'angularjs' ? 'shadow' : 'light';
|
|
253
|
+
|
|
254
|
+
const created = [];
|
|
255
|
+
const skipped = [];
|
|
256
|
+
mkdirSync(root, { recursive: true });
|
|
257
|
+
|
|
258
|
+
const config = {
|
|
259
|
+
app: appRel,
|
|
260
|
+
host,
|
|
261
|
+
hostRoot: hostRel,
|
|
262
|
+
islands: 'src/islands',
|
|
263
|
+
ui: 'src/ui',
|
|
264
|
+
archipelagos: 'src/archipelagos',
|
|
265
|
+
shared: 'src/shared',
|
|
266
|
+
contract: 'contract/src',
|
|
267
|
+
lagoon: lagoonRel,
|
|
268
|
+
...(host === 'angularjs' ? { bridge: 'roots/bridge' } : {}),
|
|
269
|
+
appPackage,
|
|
270
|
+
tagPrefix: 'x-',
|
|
271
|
+
isolation,
|
|
272
|
+
// NO `motuRoot`, EVER. It used to be written here as "the only machine-specific path in the
|
|
273
|
+
// project" — a committed relative path to somebody's checkout, which is exactly what breaks on the
|
|
274
|
+
// second machine and in CI. The CLI derives it from the binary that is running (see
|
|
275
|
+
// FRAMEWORK_ROOT in lib/config.mjs), so the correct value is never worth committing.
|
|
276
|
+
//
|
|
277
|
+
// It used to still be written when $MOTU_ROOT was set for this init, on the reasoning that a
|
|
278
|
+
// deliberate arrangement is worth recording. That was backwards: it took an override that was
|
|
279
|
+
// correct for ONE machine and froze it into the file every other machine reads. $MOTU_ROOT stays
|
|
280
|
+
// in the environment, where whoever needs it sets it.
|
|
281
|
+
};
|
|
282
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n');
|
|
283
|
+
created.push(configPath);
|
|
284
|
+
|
|
285
|
+
// --- the app package: registries, barrel, the shared island sheet ---------------------------
|
|
286
|
+
// INSTALLED FROM NPM, the range is the CLI's own version: the set is released in lockstep, so the
|
|
287
|
+
// CLI that is running is the statement of which `@motujs/*` it was built against.
|
|
288
|
+
const motuDep = argv.motuDep ? String(argv.motuDep) : INSTALLED ? `^${CLI_VERSION}` : 'workspace:*';
|
|
289
|
+
// `workspace:*` IS A LIE IN A STANDALONE PROJECT, and an expensive one: the generated package.json
|
|
290
|
+
// listed three @motujs packages that no workspace resolves, so `bun install` — and therefore
|
|
291
|
+
// `bun add react` — failed outright in a freshly-initialised project. A greenfield adopter could
|
|
292
|
+
// not install their first dependency.
|
|
293
|
+
//
|
|
294
|
+
// The packages do not belong there anyway. Nothing resolves @motujs/* through node: the lagoon aliases
|
|
295
|
+
// them and tsconfig `paths` maps them, which is the whole "no install step" posture. So they are
|
|
296
|
+
// declared only when a workspace above the project can actually satisfy them.
|
|
297
|
+
const inWorkspace = (() => {
|
|
298
|
+
let dir = appRoot;
|
|
299
|
+
for (let up = 0; up < 6; up++) {
|
|
300
|
+
const parent = dirname(dir);
|
|
301
|
+
if (parent === dir) break;
|
|
302
|
+
dir = parent;
|
|
303
|
+
if (existsSync(resolve(dir, 'pnpm-workspace.yaml'))) return true;
|
|
304
|
+
try {
|
|
305
|
+
const pkg = JSON.parse(readFileSync(resolve(dir, 'package.json'), 'utf8'));
|
|
306
|
+
if (pkg.workspaces) return true;
|
|
307
|
+
} catch {}
|
|
308
|
+
}
|
|
309
|
+
return false;
|
|
310
|
+
})();
|
|
311
|
+
const motuDependencies =
|
|
312
|
+
inWorkspace || argv.motuDep || INSTALLED
|
|
313
|
+
? ` "dependencies": {\n "@motujs/core": "${motuDep}",\n "@motujs/react": "${motuDep}",` +
|
|
314
|
+
`\n "@motujs/runtime": "${motuDep}"${adapterDepFor(motuDep, host)}\n },\n`
|
|
315
|
+
: '';
|
|
316
|
+
// 'vite' contributes at BUILD time (packages/adapters/vite/vite.mjs), not as a runtime package,
|
|
317
|
+
// so it needs no adapter dependency — the same as 'none'.
|
|
318
|
+
const adapterPkg = host === 'angularjs' ? '@motujs/adapter-angularjs' : host === 'next' ? '@motujs/adapter-next' : '';
|
|
319
|
+
const adapterDep = adapterPkg ? `,\n "${adapterPkg}": "${motuDep}"` : '';
|
|
320
|
+
function adapterDepFor(dep, h) {
|
|
321
|
+
const pkg = h === 'angularjs' ? '@motujs/adapter-angularjs' : h === 'next' ? '@motujs/adapter-next' : '';
|
|
322
|
+
return pkg ? `,\n "${pkg}": "${dep}"` : '';
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
writeNew(
|
|
326
|
+
resolve(appRoot, 'package.json'),
|
|
327
|
+
render(APP_PACKAGE_JSON, { appPackage, motuDep, adapterDep, motuDependencies }),
|
|
328
|
+
created,
|
|
329
|
+
skipped,
|
|
330
|
+
);
|
|
331
|
+
// INSTALLED, AN EXISTING package.json IS EDITED, not just reported on. In checkout mode the block
|
|
332
|
+
// could only ever be a `workspace:*` or a path into somebody's clone, so a printed notice was the
|
|
333
|
+
// honest move (see keptPackageJsonNotice). Installed, it is `^<version>` of packages that are on the
|
|
334
|
+
// registry — exactly what `npm i` would have written — and the adopter has just run
|
|
335
|
+
// `npm i -D @motujs/cli`, which already said yes to motu in this manifest. Leaving them out is what
|
|
336
|
+
// would break: the islands import `@motujs/react` from the app's own code, and under pnpm (or after
|
|
337
|
+
// npm stops hoisting a transitive) nothing the app declares would provide it.
|
|
338
|
+
const recorded = INSTALLED ? recordInstalledDeps(resolve(appRoot, 'package.json'), motuDep, host) : null;
|
|
339
|
+
writeNew(resolve(appRoot, 'src/islands/registry.ts'), ISLANDS_REGISTRY, created, skipped);
|
|
340
|
+
writeNew(resolve(appRoot, 'src/archipelagos/registry.ts'), ARCHIPELAGOS_REGISTRY, created, skipped);
|
|
341
|
+
writeNew(resolve(appRoot, 'src/index.ts'), BARREL, created, skipped);
|
|
342
|
+
writeNew(resolve(appRoot, 'src/shared/styles.css'), SHARED_STYLES, created, skipped);
|
|
343
|
+
writeNew(resolve(appRoot, 'src/ui/.gitkeep'), '', created, skipped);
|
|
344
|
+
writeNew(resolve(root, '.gitignore'), GITIGNORE, created, skipped);
|
|
345
|
+
|
|
346
|
+
// --- the lagoon root: where `motu island verify` closes the loop ------------------------------
|
|
347
|
+
if (argv.lagoon !== false) {
|
|
348
|
+
const vars = {
|
|
349
|
+
appPackage,
|
|
350
|
+
motuDep,
|
|
351
|
+
adapterDep,
|
|
352
|
+
appDep: argv.motuDep ? relPosix(lagoonDir, appRoot) : 'workspace:*',
|
|
353
|
+
tagPrefix: 'x-',
|
|
354
|
+
// Baked at scaffold time: a glob cannot go through a tsconfig alias reliably.
|
|
355
|
+
fixturesGlob: `['${relPosix(resolve(lagoonDir, 'src'), resolve(appRoot, 'src/islands'))}/*.evidence.ts', '${relPosix(resolve(lagoonDir, 'src'), resolve(appRoot, 'src/islands'))}/*/fixtures.mock.ts']`,
|
|
356
|
+
configFromLagoon: relPosix(lagoonDir, configPath),
|
|
357
|
+
hostRootFromLagoon: relPosix(lagoonDir, hostRoot),
|
|
358
|
+
// Tailwind/autoprefixer are the host's toolchain, but the lagoon runs its own postcss pass.
|
|
359
|
+
lagoonExtraDevDeps: host === 'next' ? ',\n "autoprefixer": "^10.4.20",\n "tailwindcss": "^3.4.0"' : '',
|
|
360
|
+
viteHostImports: host === 'next' ? NEXT_VITE_IMPORTS : '',
|
|
361
|
+
// The app's own global stylesheet. Without it the lagoon has no Tailwind base/utilities and no
|
|
362
|
+
// theme tokens, so islands render unstyled — a preview that lies about how they look in the app.
|
|
363
|
+
hostGlobalCss: hostGlobalCssImport(lagoonDir, hostRoot),
|
|
364
|
+
// FIRST import in both entries: host modules read process.env while being imported.
|
|
365
|
+
// Only where there IS a shim to import: `src/env.ts` is written for the Next host alone, so
|
|
366
|
+
// every other host got an entry file importing a module that was never created.
|
|
367
|
+
envShim: host === 'next' ? "import '{{envImport}}';\n" : '',
|
|
368
|
+
// A Next/shadcn app exposes --primary / --primary-foreground; borrow them so motu's chrome
|
|
369
|
+
// wears the application's colour rather than motu's own.
|
|
370
|
+
lagoonChrome: host === 'next' ? LAGOON_CHROME_SHADCN : '',
|
|
371
|
+
// A React host renders islands in its own tree; the lagoon must do the same or it verifies a
|
|
372
|
+
// mount path the project never ships. An ocean keeps the custom elements.
|
|
373
|
+
lagoonMount: host === 'angularjs' ? '' : ' "mount": "react",\n',
|
|
374
|
+
motuAliases: motuAliases(lagoonDir, appRoot, appPackage, host),
|
|
375
|
+
hostAliases: host === 'next' ? render(NEXT_VITE_ALIASES, { hostRootFromLagoon: relPosix(lagoonDir, hostRoot) }) : '',
|
|
376
|
+
viteCss: host === 'next' ? render(NEXT_VITE_CSS, { hostRootFromLagoon: relPosix(lagoonDir, hostRoot) }) : '',
|
|
377
|
+
// The AngularJS ocean needs a host present for islands that read host scope; the React hosts
|
|
378
|
+
// need nothing — the store is the only seam.
|
|
379
|
+
hostImport: host === 'angularjs' ? "import { angularHostScopeChannel } from '@motujs/adapter-angularjs';\n" : '',
|
|
380
|
+
// Paths the entries import by. `init` writes into the lagoon root, so these are the plain
|
|
381
|
+
// relative forms; `motu lagoon dev|build` renders the SAME templates into .motu/cache and
|
|
382
|
+
// overrides them with paths computed from there (see lib/lagoon-materialize.mjs).
|
|
383
|
+
lagoonConfigImport: '../lagoon.config.json',
|
|
384
|
+
overridesImport: './lagoon.js',
|
|
385
|
+
fixturesImport: './fixtures.js',
|
|
386
|
+
framesGlob: './frames/*.css',
|
|
387
|
+
galleryEntrySrc: '/src/main.tsx',
|
|
388
|
+
focusEntrySrc: '/src/lagoon.tsx',
|
|
389
|
+
envImport: './env.js',
|
|
390
|
+
hostOption: '',
|
|
391
|
+
hostOptionInline: '',
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// ENTRIES ARE NOT SCAFFOLDED, and the reason is a name collision the old layout could not survive.
|
|
395
|
+
//
|
|
396
|
+
// It wrote the focus ENTRY to `src/lagoon.tsx` and the OVERRIDES stub to `src/lagoon.ts`, and
|
|
397
|
+
// writing `index.html` made the project "own" its lagoon — which turns off materialization, so
|
|
398
|
+
// those two files became the ones Vite actually serves. But a region's `layout` override returns
|
|
399
|
+
// JSX, so overrides must live in a `.tsx`, which is the entry's name. Following the scaffold
|
|
400
|
+
// produced a lagoon that served the overrides file as its entry and rendered an empty div.
|
|
401
|
+
//
|
|
402
|
+
// acme never hit it because acme has no index.html: it materializes, and its `src/lagoon.tsx` is
|
|
403
|
+
// the overrides. That is the working shape, so it is now the shape `init` creates — entries,
|
|
404
|
+
// index.html and the vite config are rendered into `.motu/cache` by `motu lagoon dev|build`,
|
|
405
|
+
// which is where generated code belongs anyway.
|
|
406
|
+
// NEITHER IS `src/fixtures.ts`, for the same reason, and it used to be written here anyway.
|
|
407
|
+
// The materializer renders it — with `fixturesGlob` AND `flowsGlob`, which only it computes — so
|
|
408
|
+
// init's copy was a duplicate that also could not parse: `vars` here has no `flowsGlob`, so the
|
|
409
|
+
// file went out carrying a literal `{{flowsGlob}}` and any project that typechecked its lagoon
|
|
410
|
+
// directory failed on it. Neither acme nor the review console has the file at all, which is why
|
|
411
|
+
// nothing noticed. `motu lagoon eject` materializes into the project's own lagoon dir, so the
|
|
412
|
+
// rendered version is what a project gets the moment it wants one on disk.
|
|
413
|
+
// Declared, not coded: what the lagoon IS lives here; what it DOES lives in @motujs/react.
|
|
414
|
+
writeNew(resolve(lagoonDir, 'lagoon.config.json'), render(LAGOON_CONFIG, vars), created, skipped);
|
|
415
|
+
writeNew(resolve(lagoonDir, 'src/lagoon.tsx'), LAGOON_OVERRIDES, created, skipped);
|
|
416
|
+
if (host === 'next') {
|
|
417
|
+
writeNew(resolve(lagoonDir, 'src/next-stubs.tsx'), NEXT_STUBS, created, skipped);
|
|
418
|
+
// RENDERED, like every other template here. This one was written RAW, so the scaffolded
|
|
419
|
+
// `env.ts` kept its `{{lagoonConfigImport}}` placeholder verbatim and the file could not
|
|
420
|
+
// compile — invisible until something typechecked `roots/`, which nothing did.
|
|
421
|
+
writeNew(
|
|
422
|
+
resolve(lagoonDir, 'src/env.ts'),
|
|
423
|
+
render(ENV_SHIM, { lagoonConfigImport: '../lagoon.config.json' }),
|
|
424
|
+
created,
|
|
425
|
+
skipped,
|
|
426
|
+
);
|
|
427
|
+
// ONLY IF THE HOST HAS TAILWIND. This file's whole body is `import hostConfig from
|
|
428
|
+
// '../../../tailwind.config'`, so writing it for a Next app that styles itself any other way
|
|
429
|
+
// scaffolds a module that cannot resolve. @motujs/adapter-next makes the same check before it
|
|
430
|
+
// installs the postcss plugin; a Next host is not a Tailwind host.
|
|
431
|
+
if (TAILWIND_CONFIGS.some((f) => existsSync(resolve(hostRoot, f))))
|
|
432
|
+
writeNew(resolve(lagoonDir, 'tailwind.config.ts'), render(NEXT_TAILWIND_CONFIG, vars), created, skipped);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// The rules the HOST's agent has to follow — shipped with motu so they cannot drift from what the
|
|
437
|
+
// CLI enforces, written into the instruction files the repo already keeps.
|
|
438
|
+
//
|
|
439
|
+
// BEFORE the `--json` branch, not after. It used to sit below it, so `motu init --json` scaffolded
|
|
440
|
+
// the project and silently skipped the rules — and `--json` is exactly the invocation a script or a
|
|
441
|
+
// CI image uses, which is the one place nobody reads the output to notice they are missing.
|
|
442
|
+
const rules = applyHostRules(hostRoot);
|
|
443
|
+
// AND THE SKILLS THEY POINT AT. The rules block is short on purpose — the invariants, and which skill
|
|
444
|
+
// holds the detail (motu-regions, motu-evidence, motu-checks). A project that got the block without
|
|
445
|
+
// the skills would be told to load something it does not have. Same root as the rules: the host's,
|
|
446
|
+
// where its agent reads both. Never over a skill the project changed (reported, not overwritten).
|
|
447
|
+
const { out: skillsOut } = installSkills(hostRoot);
|
|
448
|
+
|
|
449
|
+
if (argv.json) {
|
|
450
|
+
console.log(JSON.stringify({ root, host, appPackage, created, skipped, rules, skills: skillsOut }, null, 2));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
console.log(color.green(`✓ initialized motu project in ${basename(root)}/`) + color.dim(` (host: ${host})`));
|
|
455
|
+
for (const p of created) console.log(' ' + color.dim(relPosix(root, p).replace(/^\.\//, '')));
|
|
456
|
+
for (const p of skipped) console.log(' ' + color.dim(relPosix(root, p).replace(/^\.\//, '')) + color.yellow(' (kept)'));
|
|
457
|
+
for (const p of rules) console.log(' ' + color.dim(p) + color.dim(' (motu rules block)'));
|
|
458
|
+
const skillFiles = skillsOut.created.length + skillsOut.updated.length;
|
|
459
|
+
if (skillFiles) console.log(' ' + color.dim(`${skillFiles} skill file(s) — the detail the rules block points at (.claude/skills, .github/agents)`));
|
|
460
|
+
for (const p of skillsOut.skipped) console.log(' ' + color.dim(p) + color.yellow(' (kept — differs from motu\'s; `motu skills install --force` to update)'));
|
|
461
|
+
console.log('');
|
|
462
|
+
if (recorded?.length) {
|
|
463
|
+
console.log(color.yellow(` ! recorded ${recorded.length} @motujs/* package(s) in package.json — run your package manager once:`));
|
|
464
|
+
for (const r of recorded) console.log(color.dim(` ${r}`));
|
|
465
|
+
console.log('');
|
|
466
|
+
console.log(' npm install');
|
|
467
|
+
console.log('');
|
|
468
|
+
} else if (!INSTALLED) keptPackageJsonNotice(appRoot, skipped, motuDependencies);
|
|
469
|
+
if (!LEGACY_FIT_HOSTS.has(host)) {
|
|
470
|
+
console.log(color.dim(' legacy fit is off for this host — islands mount directly, there is no legacy skin to fit.'));
|
|
471
|
+
}
|
|
472
|
+
console.log('Next: ' + color.bold('motu archipelago init <id> --page <file>') + color.dim(' then ') + color.bold('motu island create <name>'));
|
|
473
|
+
}
|