@winstonfassett/webdev-gateway 0.1.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +21 -0
  3. package/README.md +78 -0
  4. package/dist/adapter-helpers.d.ts +64 -0
  5. package/dist/adapter-helpers.d.ts.map +1 -0
  6. package/dist/adapter-helpers.js +297 -0
  7. package/dist/adapter-helpers.js.map +1 -0
  8. package/dist/admin/assets/index-DEDI8OIx.css +2 -0
  9. package/dist/admin/assets/index-DaI40ww1.js +70 -0
  10. package/dist/admin/assets/tinykeys.module-CjuTRcEz.js +1 -0
  11. package/dist/admin/index.html +13 -0
  12. package/dist/admin-rpc.d.ts +27 -0
  13. package/dist/admin-rpc.d.ts.map +1 -0
  14. package/dist/admin-rpc.js +147 -0
  15. package/dist/admin-rpc.js.map +1 -0
  16. package/dist/admin.d.ts +10 -0
  17. package/dist/admin.d.ts.map +1 -0
  18. package/dist/admin.js +202 -0
  19. package/dist/admin.js.map +1 -0
  20. package/dist/auto-register.d.ts +10 -0
  21. package/dist/auto-register.d.ts.map +1 -0
  22. package/dist/auto-register.js +145 -0
  23. package/dist/auto-register.js.map +1 -0
  24. package/dist/cdp-relay.d.ts +110 -0
  25. package/dist/cdp-relay.d.ts.map +1 -0
  26. package/dist/cdp-relay.js +616 -0
  27. package/dist/cdp-relay.js.map +1 -0
  28. package/dist/cli.d.ts +3 -0
  29. package/dist/cli.d.ts.map +1 -0
  30. package/dist/cli.js +95 -0
  31. package/dist/cli.js.map +1 -0
  32. package/dist/doctor.d.ts +6 -0
  33. package/dist/doctor.d.ts.map +1 -0
  34. package/dist/doctor.js +149 -0
  35. package/dist/doctor.js.map +1 -0
  36. package/dist/element-grab-client.js +305 -0
  37. package/dist/element-grab.d.ts +15 -0
  38. package/dist/element-grab.d.ts.map +1 -0
  39. package/dist/element-grab.js +102 -0
  40. package/dist/element-grab.js.map +1 -0
  41. package/dist/gateway.d.ts +5 -0
  42. package/dist/gateway.d.ts.map +1 -0
  43. package/dist/gateway.js +534 -0
  44. package/dist/gateway.js.map +1 -0
  45. package/dist/installer.d.ts +48 -0
  46. package/dist/installer.d.ts.map +1 -0
  47. package/dist/installer.js +637 -0
  48. package/dist/installer.js.map +1 -0
  49. package/dist/libs/element-source.js +35 -0
  50. package/dist/libs/modern-screenshot.js +14 -0
  51. package/dist/log-reader.d.ts +30 -0
  52. package/dist/log-reader.d.ts.map +1 -0
  53. package/dist/log-reader.js +174 -0
  54. package/dist/log-reader.js.map +1 -0
  55. package/dist/mcp-server.d.ts +22 -0
  56. package/dist/mcp-server.d.ts.map +1 -0
  57. package/dist/mcp-server.js +115 -0
  58. package/dist/mcp-server.js.map +1 -0
  59. package/dist/mcp-tools-core.d.ts +30 -0
  60. package/dist/mcp-tools-core.d.ts.map +1 -0
  61. package/dist/mcp-tools-core.js +375 -0
  62. package/dist/mcp-tools-core.js.map +1 -0
  63. package/dist/mcp-tools-full.d.ts +4 -0
  64. package/dist/mcp-tools-full.d.ts.map +1 -0
  65. package/dist/mcp-tools-full.js +141 -0
  66. package/dist/mcp-tools-full.js.map +1 -0
  67. package/dist/playwright-commands.d.ts +33 -0
  68. package/dist/playwright-commands.d.ts.map +1 -0
  69. package/dist/playwright-commands.js +356 -0
  70. package/dist/playwright-commands.js.map +1 -0
  71. package/dist/registry.d.ts +83 -0
  72. package/dist/registry.d.ts.map +1 -0
  73. package/dist/registry.js +205 -0
  74. package/dist/registry.js.map +1 -0
  75. package/dist/rpc-server.d.ts +54 -0
  76. package/dist/rpc-server.d.ts.map +1 -0
  77. package/dist/rpc-server.js +207 -0
  78. package/dist/rpc-server.js.map +1 -0
  79. package/dist/session.d.ts +13 -0
  80. package/dist/session.d.ts.map +1 -0
  81. package/dist/session.js +61 -0
  82. package/dist/session.js.map +1 -0
  83. package/dist/types.d.ts +76 -0
  84. package/dist/types.d.ts.map +1 -0
  85. package/dist/types.js +2 -0
  86. package/dist/types.js.map +1 -0
  87. package/dist/webdev-client.js +20 -0
  88. package/dist/writers/base.d.ts +24 -0
  89. package/dist/writers/base.d.ts.map +1 -0
  90. package/dist/writers/base.js +98 -0
  91. package/dist/writers/base.js.map +1 -0
  92. package/dist/writers/console.d.ts +8 -0
  93. package/dist/writers/console.d.ts.map +1 -0
  94. package/dist/writers/console.js +14 -0
  95. package/dist/writers/console.js.map +1 -0
  96. package/dist/writers/dev-events.d.ts +28 -0
  97. package/dist/writers/dev-events.d.ts.map +1 -0
  98. package/dist/writers/dev-events.js +53 -0
  99. package/dist/writers/dev-events.js.map +1 -0
  100. package/dist/writers/errors.d.ts +8 -0
  101. package/dist/writers/errors.d.ts.map +1 -0
  102. package/dist/writers/errors.js +14 -0
  103. package/dist/writers/errors.js.map +1 -0
  104. package/dist/writers/network.d.ts +9 -0
  105. package/dist/writers/network.d.ts.map +1 -0
  106. package/dist/writers/network.js +17 -0
  107. package/dist/writers/network.js.map +1 -0
  108. package/dist/writers/server-console.d.ts +8 -0
  109. package/dist/writers/server-console.d.ts.map +1 -0
  110. package/dist/writers/server-console.js +14 -0
  111. package/dist/writers/server-console.js.map +1 -0
  112. package/package.json +79 -0
@@ -0,0 +1,637 @@
1
+ import { existsSync, readFileSync, readdirSync, writeFileSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { intro, outro, log, note, spinner, select, multiselect, isCancel, cancel } from '@clack/prompts';
4
+ import { detect } from 'package-manager-detector';
5
+ import { execa } from 'execa';
6
+ import pc from 'picocolors';
7
+ import { autoRegister } from './auto-register.js';
8
+ const VITE_PLUGIN_PKG = '@winstonfassett/webdev-vite';
9
+ const ASTRO_PKG = '@winstonfassett/webdev-astro';
10
+ const NEXTJS_PKG = '@winstonfassett/webdev-nextjs';
11
+ const NEXTJS_INIT_PATH = '@winstonfassett/webdev-nextjs/init';
12
+ const GATEWAY_PKG = '@winstonfassett/webdev-gateway';
13
+ const VITE_PLUGIN_NAME = 'webdev';
14
+ const ASTRO_NAME = 'webdev';
15
+ const NEXTJS_WRAP = 'withWebdev';
16
+ const NEXTJS_INIT_COMPONENT = 'WebdevInit';
17
+ const STORYBOOK_PRESET = '@winstonfassett/webdev-vite/storybook';
18
+ export const ADAPTER_PACKAGES = {
19
+ vite: VITE_PLUGIN_PKG,
20
+ storybook: VITE_PLUGIN_PKG,
21
+ astro: ASTRO_PKG,
22
+ next: NEXTJS_PKG,
23
+ };
24
+ export { VITE_PLUGIN_PKG, ASTRO_PKG, NEXTJS_PKG, NEXTJS_INIT_PATH, GATEWAY_PKG };
25
+ export async function runInit(opts) {
26
+ intro(pc.cyan('webdev init'));
27
+ const frameworks = await detectFrameworks(opts.cwd, { yes: opts.yes });
28
+ if (frameworks.length === 0) {
29
+ log.error('No supported framework detected. Looked for: vite.config.*, .storybook/main.*, astro.config.*, next.config.*');
30
+ outro(pc.red('Aborted.'));
31
+ process.exitCode = 1;
32
+ return;
33
+ }
34
+ log.info(`Detected: ${frameworks.map((f) => pc.green(f.name)).join(', ')}`);
35
+ for (const fw of frameworks) {
36
+ const wire = wireFramework(fw);
37
+ const rel = relPath(opts.cwd, fw.configPath);
38
+ if (wire.status === 'already') {
39
+ log.info(`${fw.name}: already wired (${pc.dim(rel)})`);
40
+ }
41
+ else if (wire.status === 'edited') {
42
+ log.success(`${fw.name}: edited ${pc.dim(rel)}`);
43
+ }
44
+ else {
45
+ log.warn(`${fw.name}: could not safely edit ${pc.dim(rel)}`);
46
+ note(wire.manualSteps, `Manual steps for ${fw.name}`);
47
+ }
48
+ }
49
+ if (!opts.skipInstall) {
50
+ await installAdapters(opts.cwd, frameworks);
51
+ }
52
+ else {
53
+ log.info(pc.dim('Skipped npm install (--skip-install)'));
54
+ }
55
+ if (!opts.skipMcp) {
56
+ const mcpUrl = `http://localhost:${opts.port}/__mcp/sse`;
57
+ const parseFailures = [];
58
+ const permissionFailures = [];
59
+ const registered = autoRegister(opts.cwd, mcpUrl, {
60
+ onParseError: (path, err) => parseFailures.push({ path, reason: err.message }),
61
+ onPermissionError: (path, err) => permissionFailures.push({ path, reason: err.message }),
62
+ });
63
+ if (parseFailures.length > 0) {
64
+ log.warn('Could not parse existing MCP config files (skipped):');
65
+ for (const f of parseFailures)
66
+ log.warn(` ${pc.dim(f.path)}: ${f.reason}`);
67
+ }
68
+ if (permissionFailures.length > 0) {
69
+ log.warn('Permission denied writing MCP config files:');
70
+ for (const f of permissionFailures)
71
+ log.warn(` ${pc.dim(f.path)}: ${f.reason}`);
72
+ }
73
+ if (registered.length === 0) {
74
+ log.warn('No MCP client configs were written.');
75
+ }
76
+ else {
77
+ note(registered.map((p) => pc.green(`✓ ${p}`)).join('\n'), 'Registered with');
78
+ }
79
+ }
80
+ else {
81
+ log.info(pc.dim('Skipped MCP registration (--skip-mcp)'));
82
+ }
83
+ outro(pc.green(`Done. Run your dev server, then ${pc.cyan('npx webdev doctor')} to verify.`));
84
+ }
85
+ async function detectFrameworks(cwd, ctx = {}) {
86
+ const direct = await detectFrameworksIn(cwd);
87
+ if (direct.length > 0)
88
+ return direct;
89
+ const subprojects = await scanMonorepoSubprojects(cwd);
90
+ if (subprojects.length === 0)
91
+ return [];
92
+ if (subprojects.length === 1) {
93
+ const sp = subprojects[0];
94
+ const rel = relPath(cwd, sp.dir);
95
+ if (ctx.yes) {
96
+ log.info(`Auto-accepted: wire ${sp.frameworks.map((f) => f.name).join(', ')} in ${rel}`);
97
+ return sp.frameworks;
98
+ }
99
+ const confirm = await select({
100
+ message: `Found framework in ${rel}. Wire it?`,
101
+ options: [
102
+ { value: 'yes', label: `Yes — wire ${sp.frameworks.map((f) => f.name).join(', ')} in ${rel}` },
103
+ { value: 'no', label: 'No, abort' },
104
+ ],
105
+ initialValue: 'yes',
106
+ });
107
+ if (isCancel(confirm) || confirm === 'no') {
108
+ cancel('Aborted.');
109
+ process.exit(0);
110
+ }
111
+ return sp.frameworks;
112
+ }
113
+ if (ctx.yes) {
114
+ log.info(`Auto-accepted: wiring ${subprojects.length} sub-projects (${subprojects.map((sp) => relPath(cwd, sp.dir)).join(', ')})`);
115
+ return subprojects.flatMap((sp) => sp.frameworks);
116
+ }
117
+ const selectedDirs = await multiselect({
118
+ message: 'Multiple sub-projects with framework configs found. Which to wire?',
119
+ options: subprojects.map((sp) => ({
120
+ value: sp.dir,
121
+ label: `${relPath(cwd, sp.dir)} (${sp.frameworks.map((f) => f.name).join(', ')})`,
122
+ })),
123
+ initialValues: subprojects.map((sp) => sp.dir),
124
+ required: true,
125
+ });
126
+ if (isCancel(selectedDirs)) {
127
+ cancel('Aborted.');
128
+ process.exit(0);
129
+ }
130
+ return subprojects
131
+ .filter((sp) => selectedDirs.includes(sp.dir))
132
+ .flatMap((sp) => sp.frameworks);
133
+ }
134
+ export async function detectFrameworksIn(dir) {
135
+ const found = [];
136
+ const nextPath = firstExisting(dir, ['next.config.ts', 'next.config.js', 'next.config.mjs', 'next.config.mts']);
137
+ if (nextPath) {
138
+ const bundler = await detectNextBundler(dir);
139
+ const layoutPath = firstExisting(dir, ['app/layout.tsx', 'app/layout.jsx', 'src/app/layout.tsx', 'src/app/layout.jsx']);
140
+ found.push({ name: 'next', projectDir: dir, configPath: nextPath, bundler, layoutPath });
141
+ }
142
+ else {
143
+ const astroPath = firstExisting(dir, ['astro.config.mjs', 'astro.config.js', 'astro.config.ts', 'astro.config.mts']);
144
+ if (astroPath) {
145
+ found.push({ name: 'astro', projectDir: dir, configPath: astroPath });
146
+ }
147
+ else {
148
+ const vitePath = firstExisting(dir, ['vite.config.ts', 'vite.config.js', 'vite.config.mts', 'vite.config.mjs']);
149
+ if (vitePath)
150
+ found.push({ name: 'vite', projectDir: dir, configPath: vitePath });
151
+ }
152
+ }
153
+ const sbPath = firstExisting(dir, ['.storybook/main.ts', '.storybook/main.js', '.storybook/main.mts', '.storybook/main.mjs']);
154
+ if (sbPath)
155
+ found.push({ name: 'storybook', projectDir: dir, configPath: sbPath });
156
+ return found;
157
+ }
158
+ /** Walk apps/* packages/* services/* examples/* for sub-projects with framework configs. */
159
+ async function scanMonorepoSubprojects(cwd) {
160
+ const out = [];
161
+ const candidateRoots = ['apps', 'packages', 'services', 'examples'];
162
+ for (const root of candidateRoots) {
163
+ const rootPath = join(cwd, root);
164
+ if (!existsSync(rootPath))
165
+ continue;
166
+ let entries = [];
167
+ try {
168
+ entries = readdirSync(rootPath);
169
+ }
170
+ catch {
171
+ continue;
172
+ }
173
+ for (const entry of entries) {
174
+ const subdir = join(rootPath, entry);
175
+ try {
176
+ if (!statSync(subdir).isDirectory())
177
+ continue;
178
+ }
179
+ catch {
180
+ continue;
181
+ }
182
+ const fws = await detectFrameworksIn(subdir);
183
+ if (fws.length > 0)
184
+ out.push({ dir: subdir, frameworks: fws });
185
+ }
186
+ }
187
+ return out;
188
+ }
189
+ /**
190
+ * Detect Next.js bundler. Cascade per Sentry-wizard pattern + Next 16 default flip:
191
+ * 1. only `--webpack` in any script → webpack
192
+ * 2. only `--turbopack`/`--turbo` in any script → turbopack
193
+ * 3. BOTH flags in different scripts → ask the user (default to whatever the
194
+ * bare `dev` script uses)
195
+ * 4. neither flag, installed next version >= 16 → turbopack (default)
196
+ * 5. neither flag, installed next version < 16 → webpack (default)
197
+ * 6. unknown → assume turbopack
198
+ */
199
+ async function detectNextBundler(cwd) {
200
+ const pkgJsonPath = join(cwd, 'package.json');
201
+ let pkgJson = null;
202
+ if (existsSync(pkgJsonPath)) {
203
+ try {
204
+ pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
205
+ }
206
+ catch { /* fall through */ }
207
+ }
208
+ const scripts = pkgJson?.scripts ?? {};
209
+ const allScriptText = Object.values(scripts).join(' ');
210
+ const hasWebpack = allScriptText.includes('--webpack');
211
+ const hasTurbopack = allScriptText.includes('--turbopack') || allScriptText.includes('--turbo');
212
+ if (hasWebpack && hasTurbopack) {
213
+ const devScript = scripts.dev ?? '';
214
+ const devPrefers = devScript.includes('--webpack') ? 'webpack' :
215
+ devScript.includes('--turbopack') || devScript.includes('--turbo') ? 'turbopack' :
216
+ 'turbopack';
217
+ const choice = await select({
218
+ message: 'Both --webpack and --turbopack found in your scripts. Which to wire?',
219
+ options: [
220
+ { value: 'turbopack', label: 'turbopack — wraps next.config + adds <WebdevInit /> to layout' },
221
+ { value: 'webpack', label: 'webpack — wraps next.config only (entry injection is automatic)' },
222
+ ],
223
+ initialValue: devPrefers,
224
+ });
225
+ if (isCancel(choice)) {
226
+ cancel('Aborted.');
227
+ process.exit(0);
228
+ }
229
+ return choice;
230
+ }
231
+ if (hasWebpack)
232
+ return 'webpack';
233
+ if (hasTurbopack)
234
+ return 'turbopack';
235
+ const nextVersion = pkgJson?.dependencies?.next ?? pkgJson?.devDependencies?.next;
236
+ if (typeof nextVersion === 'string') {
237
+ const major = parseInt(nextVersion.replace(/^[^\d]*/, ''), 10);
238
+ if (!Number.isNaN(major))
239
+ return major >= 16 ? 'turbopack' : 'webpack';
240
+ }
241
+ return 'turbopack';
242
+ }
243
+ function firstExisting(cwd, files) {
244
+ for (const f of files) {
245
+ const p = join(cwd, f);
246
+ if (existsSync(p))
247
+ return p;
248
+ }
249
+ return null;
250
+ }
251
+ function wireFramework(fw) {
252
+ if (fw.name === 'vite')
253
+ return wireVite(fw.configPath);
254
+ if (fw.name === 'storybook')
255
+ return wireStorybook(fw.configPath);
256
+ if (fw.name === 'astro')
257
+ return wireAstro(fw.configPath);
258
+ if (fw.name === 'next')
259
+ return wireNext(fw);
260
+ return { status: 'manual', manualSteps: 'Unknown framework' };
261
+ }
262
+ function wireVite(configPath) {
263
+ const source = readFileSync(configPath, 'utf8');
264
+ if (hasRealImport(source, VITE_PLUGIN_PKG) && hasCallExpression(source, VITE_PLUGIN_NAME)) {
265
+ return { status: 'already' };
266
+ }
267
+ const withPlugin = insertIntoArrayField(source, 'plugins', `${VITE_PLUGIN_NAME}()`);
268
+ if (withPlugin == null) {
269
+ return { status: 'manual', manualSteps: viteManualSteps(configPath) };
270
+ }
271
+ const importLine = `import { ${VITE_PLUGIN_NAME} } from '${VITE_PLUGIN_PKG}'`;
272
+ const withImport = hasRealImport(withPlugin, VITE_PLUGIN_PKG)
273
+ ? withPlugin
274
+ : insertImportAfterLastImport(withPlugin, importLine);
275
+ writeFileSync(configPath, withImport, 'utf8');
276
+ return { status: 'edited' };
277
+ }
278
+ function wireStorybook(configPath) {
279
+ const source = readFileSync(configPath, 'utf8');
280
+ if (source.includes(STORYBOOK_PRESET))
281
+ return { status: 'already' };
282
+ const updated = insertIntoArrayField(source, 'addons', `'${STORYBOOK_PRESET}'`);
283
+ if (updated == null) {
284
+ return { status: 'manual', manualSteps: storybookManualSteps(configPath) };
285
+ }
286
+ writeFileSync(configPath, updated, 'utf8');
287
+ return { status: 'edited' };
288
+ }
289
+ function wireAstro(configPath) {
290
+ const source = readFileSync(configPath, 'utf8');
291
+ if (hasRealImport(source, ASTRO_PKG) && hasCallExpression(source, ASTRO_NAME)) {
292
+ return { status: 'already' };
293
+ }
294
+ const withIntegration = insertIntoArrayField(source, 'integrations', `${ASTRO_NAME}()`);
295
+ if (withIntegration == null) {
296
+ return { status: 'manual', manualSteps: astroManualSteps(configPath) };
297
+ }
298
+ const importLine = `import ${ASTRO_NAME} from '${ASTRO_PKG}'`;
299
+ const withImport = hasRealImport(withIntegration, ASTRO_PKG)
300
+ ? withIntegration
301
+ : insertImportAfterLastImport(withIntegration, importLine);
302
+ writeFileSync(configPath, withImport, 'utf8');
303
+ return { status: 'edited' };
304
+ }
305
+ function wireNext(fw) {
306
+ // 1. Wrap-the-export in next.config.*
307
+ const configResult = wrapNextConfig(fw.configPath);
308
+ if (configResult.status === 'manual')
309
+ return configResult;
310
+ // 2. Turbopack only: insert <WebDevMcpInit /> in layout.tsx
311
+ if (fw.bundler === 'webpack')
312
+ return configResult;
313
+ if (!fw.layoutPath) {
314
+ // Turbopack but no root layout found — config edited but init not injected
315
+ return {
316
+ status: 'manual',
317
+ manualSteps: nextLayoutManualSteps('app/layout.tsx (or src/app/layout.tsx)'),
318
+ };
319
+ }
320
+ const layoutResult = injectInitIntoLayout(fw.layoutPath);
321
+ if (layoutResult.status === 'manual')
322
+ return layoutResult;
323
+ // Both succeeded (or were already wired)
324
+ return configResult.status === 'already' && layoutResult.status === 'already'
325
+ ? { status: 'already' }
326
+ : { status: 'edited' };
327
+ }
328
+ function wrapNextConfig(configPath) {
329
+ const source = readFileSync(configPath, 'utf8');
330
+ if (hasRealImport(source, NEXTJS_PKG) && hasCallExpression(source, NEXTJS_WRAP)) {
331
+ return { status: 'already' };
332
+ }
333
+ // Match `export default <expr>;?` (single line, expr is identifier or simple call).
334
+ // Use [ \t]* (not \s*) to avoid consuming the trailing newline.
335
+ const exportRe = /^export default ([^;\n]+?)(;?)[ \t]*$/m;
336
+ const cjsRe = /^module\.exports[ \t]*=[ \t]*([^;\n]+?)(;?)[ \t]*$/m;
337
+ let m = source.match(exportRe);
338
+ let isEsm = true;
339
+ if (!m) {
340
+ m = source.match(cjsRe);
341
+ isEsm = false;
342
+ }
343
+ if (!m) {
344
+ return { status: 'manual', manualSteps: nextConfigManualSteps(configPath) };
345
+ }
346
+ const expr = m[1].trim();
347
+ const semi = m[2];
348
+ const wrappedLine = isEsm
349
+ ? `export default ${NEXTJS_WRAP}(${expr})${semi}`
350
+ : `module.exports = ${NEXTJS_WRAP}(${expr})${semi}`;
351
+ let updated = source.replace(m[0], wrappedLine);
352
+ // Quote style: match what the file uses
353
+ const quote = source.includes(`from "`) ? '"' : `'`;
354
+ const importLine = `import { ${NEXTJS_WRAP} } from ${quote}${NEXTJS_PKG}${quote}`;
355
+ if (!hasRealImport(updated, NEXTJS_PKG)) {
356
+ updated = insertImportAfterLastImport(updated, importLine);
357
+ }
358
+ writeFileSync(configPath, updated, 'utf8');
359
+ return { status: 'edited' };
360
+ }
361
+ function injectInitIntoLayout(layoutPath) {
362
+ const source = readFileSync(layoutPath, 'utf8');
363
+ if (hasRealImport(source, NEXTJS_INIT_PATH) && /<WebDevMcpInit\b/.test(source)) {
364
+ return { status: 'already' };
365
+ }
366
+ // Find <body ...> opening tag (not </body>, not <body/>)
367
+ const bodyOpenRe = /<body\b[^>]*>/;
368
+ const m = source.match(bodyOpenRe);
369
+ if (!m) {
370
+ return { status: 'manual', manualSteps: nextLayoutManualSteps(layoutPath) };
371
+ }
372
+ const insertAt = m.index + m[0].length;
373
+ // Determine indent: peek at the next non-empty line after `<body>` to copy its indent
374
+ const after = source.slice(insertAt);
375
+ const nextLineMatch = after.match(/\n(\s*)\S/);
376
+ const childIndent = nextLineMatch?.[1] ?? ' ';
377
+ const insertion = `\n${childIndent}<${NEXTJS_INIT_COMPONENT} />`;
378
+ let updated = source.slice(0, insertAt) + insertion + source.slice(insertAt);
379
+ // Quote style match for the import
380
+ const quote = source.includes(`from "`) ? '"' : `'`;
381
+ const importLine = `import { ${NEXTJS_INIT_COMPONENT} } from ${quote}${NEXTJS_INIT_PATH}${quote}`;
382
+ if (!hasRealImport(updated, NEXTJS_INIT_PATH)) {
383
+ updated = insertImportAfterLastImport(updated, importLine);
384
+ }
385
+ writeFileSync(layoutPath, updated, 'utf8');
386
+ return { status: 'edited' };
387
+ }
388
+ /**
389
+ * Check if a framework is fully wired (both import + wiring expression present).
390
+ * Re-exported as the basis for `doctor` checks.
391
+ */
392
+ export function isWired(fw) {
393
+ if (!existsSync(fw.configPath))
394
+ return false;
395
+ const source = readFileSync(fw.configPath, 'utf8');
396
+ if (fw.name === 'vite') {
397
+ return hasRealImport(source, VITE_PLUGIN_PKG) && hasCallExpression(source, VITE_PLUGIN_NAME);
398
+ }
399
+ if (fw.name === 'astro') {
400
+ return hasRealImport(source, ASTRO_PKG) && hasCallExpression(source, ASTRO_NAME);
401
+ }
402
+ if (fw.name === 'storybook') {
403
+ return source.includes(STORYBOOK_PRESET);
404
+ }
405
+ if (fw.name === 'next') {
406
+ const cfgWired = hasRealImport(source, NEXTJS_PKG) && hasCallExpression(source, NEXTJS_WRAP);
407
+ if (fw.bundler === 'webpack')
408
+ return cfgWired;
409
+ if (!fw.layoutPath || !existsSync(fw.layoutPath))
410
+ return false;
411
+ const layoutSource = readFileSync(fw.layoutPath, 'utf8');
412
+ const layoutWired = hasRealImport(layoutSource, NEXTJS_INIT_PATH) && /<WebDevMcpInit\b/.test(layoutSource);
413
+ return cfgWired && layoutWired;
414
+ }
415
+ return false;
416
+ }
417
+ /**
418
+ * Check if a real `import ... from '<pkg>'` line is present (not a commented-out one).
419
+ * Anchors to start-of-line + optional whitespace + literal `import` keyword, so `// import ...`
420
+ * does not match.
421
+ */
422
+ function hasRealImport(source, pkg) {
423
+ const escaped = pkg.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
424
+ const re = new RegExp(`^\\s*import\\b[^\\n;]*['"\`]${escaped}['"\`]`, 'm');
425
+ return re.test(source);
426
+ }
427
+ /**
428
+ * Check if a function call expression is present in the source.
429
+ * Used as the second half of the "already wired" marker — paired with hasRealImport.
430
+ * Comment-commented-out cases (e.g. `// webDevMcp()`) will incorrectly match;
431
+ * acceptable since hasRealImport will fail in that case.
432
+ */
433
+ function hasCallExpression(source, name) {
434
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
435
+ return new RegExp(`\\b${escaped}\\s*\\(`).test(source);
436
+ }
437
+ /**
438
+ * Insert an entry into a `<field>: [...]` array. Entry can be any code (string
439
+ * literal with quotes, function call, etc.). Preserves user's indentation and
440
+ * inline-vs-multiline style. Returns null if the array can't be located safely.
441
+ */
442
+ function insertIntoArrayField(source, field, entry) {
443
+ const fieldRe = new RegExp(`(^|[^\\w$])${field}\\s*:\\s*\\[`);
444
+ const m = source.match(fieldRe);
445
+ if (!m)
446
+ return null;
447
+ const arrayOpenIdx = m.index + m[0].length - 1;
448
+ let depth = 1;
449
+ let i = arrayOpenIdx + 1;
450
+ let str = null;
451
+ while (i < source.length && depth > 0) {
452
+ const c = source[i];
453
+ if (str) {
454
+ if (c === '\\') {
455
+ i += 2;
456
+ continue;
457
+ }
458
+ if (c === str)
459
+ str = null;
460
+ }
461
+ else if (c === '"' || c === "'" || c === '`') {
462
+ str = c;
463
+ }
464
+ else if (c === '[')
465
+ depth++;
466
+ else if (c === ']')
467
+ depth--;
468
+ if (depth === 0)
469
+ break;
470
+ i++;
471
+ }
472
+ if (depth !== 0)
473
+ return null;
474
+ const closeIdx = i;
475
+ const arrayContents = source.slice(arrayOpenIdx + 1, closeIdx);
476
+ const isEmpty = arrayContents.trim() === '';
477
+ const isInline = !arrayContents.includes('\n');
478
+ // Inline empty: `plugins: []` → `plugins: [entry]`
479
+ if (isEmpty && isInline) {
480
+ return source.slice(0, arrayOpenIdx + 1) + entry + source.slice(closeIdx);
481
+ }
482
+ // Inline non-empty: `plugins: [a(), b()]` → `plugins: [a(), b(), entry]`
483
+ if (isInline) {
484
+ const beforeClose = arrayContents.replace(/\s+$/, '');
485
+ const lastChar = beforeClose[beforeClose.length - 1];
486
+ const sep = lastChar === ',' ? ' ' : ', ';
487
+ return source.slice(0, arrayOpenIdx + 1) + beforeClose + sep + entry + source.slice(closeIdx);
488
+ }
489
+ // Multi-line. Indent of `]`'s line.
490
+ const closeLineStart = source.lastIndexOf('\n', closeIdx - 1) + 1;
491
+ const closeLineIndent = source.slice(closeLineStart, closeIdx).match(/^\s*/)?.[0] ?? '';
492
+ if (isEmpty) {
493
+ return source.slice(0, arrayOpenIdx + 1) + `\n${closeLineIndent} ${entry},\n${closeLineIndent}` + source.slice(closeIdx);
494
+ }
495
+ // Find item indent from last non-empty line in the array
496
+ let itemIndent = closeLineIndent + ' ';
497
+ const lines = arrayContents.split('\n');
498
+ for (let li = lines.length - 1; li >= 0; li--) {
499
+ const line = lines[li];
500
+ if (line.trim() === '')
501
+ continue;
502
+ const indent = line.match(/^\s*/)?.[0] ?? '';
503
+ if (indent.length > 0)
504
+ itemIndent = indent;
505
+ break;
506
+ }
507
+ const beforeTrim = arrayContents.replace(/\s+$/, '');
508
+ const lastChar = beforeTrim[beforeTrim.length - 1];
509
+ const needsComma = lastChar !== ',' && lastChar !== '[';
510
+ const insertion = `${needsComma ? ',' : ''}\n${itemIndent}${entry},\n${closeLineIndent}`;
511
+ return source.slice(0, arrayOpenIdx + 1) + beforeTrim + insertion + source.slice(closeIdx);
512
+ }
513
+ /**
514
+ * Insert an import statement on its own line after the last existing import.
515
+ * Matches the file's existing semicolon style. Falls back to inserting at the
516
+ * top of the file if no imports are found.
517
+ */
518
+ function insertImportAfterLastImport(source, importLine) {
519
+ const importPattern = /^import\s+[^;]+?\s+from\s+['"][^'"]+['"];?\s*$/;
520
+ const lines = source.split('\n');
521
+ let lastIdx = -1;
522
+ let usesSemicolons = false;
523
+ for (let i = 0; i < lines.length; i++) {
524
+ if (importPattern.test(lines[i])) {
525
+ lastIdx = i;
526
+ if (lines[i].trim().endsWith(';'))
527
+ usesSemicolons = true;
528
+ }
529
+ }
530
+ const final = usesSemicolons && !importLine.trim().endsWith(';') ? importLine + ';' : importLine;
531
+ if (lastIdx === -1) {
532
+ // No existing imports — add ours plus a blank line before existing code,
533
+ // unless the source already starts with a blank line.
534
+ const sep = source.startsWith('\n') ? '\n' : '\n\n';
535
+ return final + sep + source;
536
+ }
537
+ lines.splice(lastIdx + 1, 0, final);
538
+ return lines.join('\n');
539
+ }
540
+ function viteManualSteps(configPath) {
541
+ return [
542
+ `In ${configPath}:`,
543
+ '',
544
+ ` import { ${VITE_PLUGIN_NAME} } from '${VITE_PLUGIN_PKG}'`,
545
+ '',
546
+ ' export default defineConfig({',
547
+ ' plugins: [',
548
+ ' // ... your other plugins',
549
+ ` ${VITE_PLUGIN_NAME}(),`,
550
+ ' ],',
551
+ ' })',
552
+ ].join('\n');
553
+ }
554
+ function storybookManualSteps(configPath) {
555
+ return [
556
+ `In ${configPath}:`,
557
+ '',
558
+ ' addons: [',
559
+ ' // ... your other addons',
560
+ ` '${STORYBOOK_PRESET}',`,
561
+ ' ],',
562
+ ].join('\n');
563
+ }
564
+ function astroManualSteps(configPath) {
565
+ return [
566
+ `In ${configPath}:`,
567
+ '',
568
+ ` import ${ASTRO_NAME} from '${ASTRO_PKG}'`,
569
+ '',
570
+ ' export default defineConfig({',
571
+ ' integrations: [',
572
+ ' // ... your other integrations',
573
+ ` ${ASTRO_NAME}(),`,
574
+ ' ],',
575
+ ' })',
576
+ ].join('\n');
577
+ }
578
+ function nextConfigManualSteps(configPath) {
579
+ return [
580
+ `In ${configPath}:`,
581
+ '',
582
+ ` import { ${NEXTJS_WRAP} } from '${NEXTJS_PKG}'`,
583
+ '',
584
+ ' // ... your config ...',
585
+ '',
586
+ ` export default ${NEXTJS_WRAP}(nextConfig)`,
587
+ ].join('\n');
588
+ }
589
+ function nextLayoutManualSteps(layoutPath) {
590
+ return [
591
+ `In ${layoutPath} (root layout, the one with <html><body>):`,
592
+ '',
593
+ ` import { ${NEXTJS_INIT_COMPONENT} } from '${NEXTJS_INIT_PATH}'`,
594
+ '',
595
+ ' // inside <body>:',
596
+ ` <${NEXTJS_INIT_COMPONENT} />`,
597
+ ' {children}',
598
+ ].join('\n');
599
+ }
600
+ async function installAdapters(cwd, frameworks) {
601
+ // Group adapter package sets by project dir so deps land in the right
602
+ // package.json (matters for monorepos where init at root wires sub-projects)
603
+ const byDir = new Map();
604
+ for (const fw of frameworks) {
605
+ const set = byDir.get(fw.projectDir) ?? new Set([GATEWAY_PKG]);
606
+ if (fw.name === 'vite' || fw.name === 'storybook')
607
+ set.add(VITE_PLUGIN_PKG);
608
+ else if (fw.name === 'astro')
609
+ set.add(ASTRO_PKG);
610
+ else if (fw.name === 'next')
611
+ set.add(NEXTJS_PKG);
612
+ byDir.set(fw.projectDir, set);
613
+ }
614
+ for (const [projectDir, pkgSet] of byDir) {
615
+ const pkgList = [...pkgSet];
616
+ const detected = await detect({ cwd: projectDir });
617
+ const agent = detected?.agent ?? 'npm';
618
+ const verb = agent === 'npm' ? 'install' : 'add';
619
+ const args = [verb, '-D', ...pkgList];
620
+ const rel = relPath(cwd, projectDir) || '.';
621
+ const s = spinner();
622
+ s.start(`Installing ${pkgList.join(' + ')} in ${rel} via ${agent}`);
623
+ try {
624
+ await execa(agent, args, { cwd: projectDir });
625
+ s.stop(`Installed in ${rel} via ${agent}`);
626
+ }
627
+ catch (err) {
628
+ s.stop(pc.red(`Install failed in ${rel}`));
629
+ log.error(err.message);
630
+ throw err;
631
+ }
632
+ }
633
+ }
634
+ export function relPath(cwd, p) {
635
+ return p.startsWith(cwd) ? p.slice(cwd.length + 1) : p;
636
+ }
637
+ //# sourceMappingURL=installer.js.map