webmcp-codegen 0.3.1 → 0.3.3

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.
@@ -0,0 +1,1255 @@
1
+ import {
2
+ CONFIG_FILE_NAMES,
3
+ loadConfig,
4
+ runGenerate
5
+ } from "./chunk-MJQ5B6HB.js";
6
+ import {
7
+ js
8
+ } from "./chunk-EAKYM4YS.js";
9
+ import {
10
+ openapi
11
+ } from "./chunk-3LTHWIAP.js";
12
+
13
+ // src/dev/server.ts
14
+ import { spawn } from "child_process";
15
+ import { createServer } from "http";
16
+
17
+ // src/data-file.ts
18
+ import { readFile, writeFile } from "fs/promises";
19
+ import { join } from "path";
20
+ var DATA_FILE_NAME = ".webmcp-codegen.json";
21
+ async function loadDataFile(cwd) {
22
+ try {
23
+ const parsed = JSON.parse(await readFile(join(cwd, DATA_FILE_NAME), "utf8"));
24
+ return parsed && typeof parsed === "object" ? parsed : {};
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+ async function saveDataFile(cwd, patch) {
30
+ const current = await loadDataFile(cwd);
31
+ const next = { ...current, ...patch };
32
+ if (JSON.stringify(next) === JSON.stringify(current)) return;
33
+ await writeFile(join(cwd, DATA_FILE_NAME), `${JSON.stringify(next, null, 2)}
34
+ `, "utf8");
35
+ }
36
+
37
+ // src/setup.ts
38
+ import { existsSync } from "fs";
39
+ import { basename, join as join4 } from "path";
40
+ import { createInterface } from "readline/promises";
41
+
42
+ // src/detect.ts
43
+ import { readdir } from "fs/promises";
44
+ import { join as join2, relative } from "path";
45
+ var SPEC_FILE_PATTERN = /^(openapi|swagger|api)\.(ya?ml|json)$/i;
46
+ var IGNORED_DIRS = /* @__PURE__ */ new Set([
47
+ "node_modules",
48
+ ".git",
49
+ ".turbo",
50
+ ".next",
51
+ "dist",
52
+ "build",
53
+ "coverage"
54
+ ]);
55
+ var MAX_DEPTH = 5;
56
+ async function findSpecs(cwd) {
57
+ const found = [];
58
+ async function walk(dir, depth) {
59
+ if (depth > MAX_DEPTH) return;
60
+ let entries;
61
+ try {
62
+ entries = await readdir(dir, { withFileTypes: true });
63
+ } catch {
64
+ return;
65
+ }
66
+ for (const entry of entries) {
67
+ if (entry.isDirectory()) {
68
+ if (!IGNORED_DIRS.has(entry.name)) await walk(join2(dir, entry.name), depth + 1);
69
+ } else if (SPEC_FILE_PATTERN.test(entry.name)) {
70
+ found.push({ path: join2(dir, entry.name), depth });
71
+ }
72
+ }
73
+ }
74
+ await walk(cwd, 0);
75
+ return found.sort((a, b) => a.depth - b.depth).map((entry) => relative(cwd, entry.path));
76
+ }
77
+
78
+ // src/detect-app.ts
79
+ import { readdir as readdir2, readFile as readFile2 } from "fs/promises";
80
+ import { join as join3 } from "path";
81
+ var FRAMEWORKS = [
82
+ { dep: "next", framework: "next" },
83
+ { dep: "nuxt", framework: "nuxt" },
84
+ { dep: "@sveltejs/kit", framework: "sveltekit" }
85
+ ];
86
+ async function findWebApps(cwd) {
87
+ const packageDirs = await findPackageDirs(cwd);
88
+ const apps = [];
89
+ for (const dir of packageDirs) {
90
+ const pkg = await readPackageJson(join3(cwd, dir));
91
+ if (!pkg) continue;
92
+ const deps = {
93
+ ...pkg.dependencies,
94
+ ...pkg.devDependencies
95
+ };
96
+ const known = FRAMEWORKS.find(({ dep }) => deps[dep]);
97
+ const framework = known?.framework ?? (deps.react && deps.vite ? "vite-react" : void 0);
98
+ if (framework) apps.push({ dir, framework });
99
+ }
100
+ return apps.sort((a, b) => score(b) - score(a));
101
+ function score(app) {
102
+ return (app.framework === "unknown" ? 0 : 10) + (/(^|\/)(web|app|frontend|client)$/.test(app.dir) ? 2 : 0);
103
+ }
104
+ }
105
+ async function findPackageDirs(cwd) {
106
+ const dirs = [];
107
+ const root = await readPackageJson(join3(cwd, ""));
108
+ if (root) {
109
+ dirs.push(".");
110
+ for (const pattern of await workspaceGlobs(cwd, root)) {
111
+ dirs.push(...await expandShallowGlob(cwd, pattern));
112
+ }
113
+ }
114
+ return [...new Set(dirs)];
115
+ }
116
+ async function workspaceGlobs(cwd, rootPkg) {
117
+ const workspaces = rootPkg.workspaces;
118
+ if (Array.isArray(workspaces)) return workspaces;
119
+ if (workspaces && typeof workspaces === "object" && Array.isArray(workspaces.packages)) {
120
+ return workspaces.packages;
121
+ }
122
+ return readPnpmWorkspaceGlobs(cwd);
123
+ }
124
+ async function readPnpmWorkspaceGlobs(cwd) {
125
+ try {
126
+ const text = await readFile2(join3(cwd, "pnpm-workspace.yaml"), "utf8");
127
+ const packagesBlock = /^packages:\s*\n((?:\s+-\s+.+\n?)+)/m.exec(text);
128
+ if (!packagesBlock) return [];
129
+ return [...packagesBlock[1].matchAll(/^\s+-\s+['"]?([^'"\n]+?)['"]?\s*$/gm)].map(
130
+ (match) => match[1]
131
+ );
132
+ } catch {
133
+ return [];
134
+ }
135
+ }
136
+ async function expandShallowGlob(cwd, pattern) {
137
+ const starAt = pattern.indexOf("*");
138
+ const base = starAt === -1 ? pattern : pattern.slice(0, starAt).replace(/\/$/, "");
139
+ if (starAt === -1) return [base];
140
+ try {
141
+ const entries = await readdir2(join3(cwd, base), { withFileTypes: true });
142
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => `${base}/${entry.name}`);
143
+ } catch {
144
+ return [];
145
+ }
146
+ }
147
+ async function readPackageJson(dir) {
148
+ try {
149
+ return JSON.parse(await readFile2(join3(dir, "package.json"), "utf8"));
150
+ } catch {
151
+ return void 0;
152
+ }
153
+ }
154
+
155
+ // src/setup.ts
156
+ async function resolveSetup(cwd, flags) {
157
+ const hasConfigFile = flags.configPath ? existsSync(join4(cwd, flags.configPath)) : CONFIG_FILE_NAMES.some((name) => existsSync(join4(cwd, name)));
158
+ if (hasConfigFile) {
159
+ const { config, path } = await loadConfig(cwd, flags.configPath);
160
+ if (flags.spec || flags.out) {
161
+ console.warn(`Note: --spec/--out are ignored; ${basename(path)} is in charge here.`);
162
+ }
163
+ const data2 = await loadDataFile(cwd);
164
+ const apps = await findWebApps(cwd);
165
+ const app2 = apps.find((candidate) => candidate.dir === data2.app) ?? apps[0];
166
+ return { config, label: basename(path), app: app2, fromConfigFile: true, remember: {} };
167
+ }
168
+ if (flags.configPath) {
169
+ throw new Error(`No config file at "${flags.configPath}".`);
170
+ }
171
+ const data = await loadDataFile(cwd);
172
+ const spec = flags.spec ?? data.spec ?? await detectSpec(cwd);
173
+ let app;
174
+ if (!flags.out) {
175
+ const apps = await findWebApps(cwd);
176
+ const remembered = apps.find((candidate) => candidate.dir === data.app);
177
+ if (remembered) {
178
+ app = remembered;
179
+ } else if (apps.length === 1) {
180
+ app = apps[0];
181
+ console.log(`Found your web app: ${app?.dir} (${app?.framework})`);
182
+ } else if (apps.length > 1) {
183
+ app = await askWhichApp(apps);
184
+ }
185
+ }
186
+ const outDir = flags.out ?? (app && app.dir !== "." ? `${app.dir}/src/webmcp` : "./src/webmcp");
187
+ return {
188
+ config: { sources: [openapi({ spec })], generate: [js({ outDir })] },
189
+ label: flags.spec ? `--spec ${spec}` : `detected ${spec}`,
190
+ app,
191
+ fromConfigFile: false,
192
+ remember: { spec, app: app?.dir }
193
+ };
194
+ }
195
+ async function askWhichApp(apps) {
196
+ if (!process.stdin.isTTY) {
197
+ const first = apps[0];
198
+ console.log(`Several packages look like web apps; using ${first.dir}. Override with --out.`);
199
+ return first;
200
+ }
201
+ console.log("Several packages look like the web app. Which one should the tools live in?");
202
+ apps.forEach((app, index) => {
203
+ console.log(` ${index + 1}. ${app.dir} (${app.framework})${index === 0 ? " [default]" : ""}`);
204
+ });
205
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
206
+ try {
207
+ const answer = await rl.question("Choice [1]: ");
208
+ const picked = Number.parseInt(answer.trim() || "1", 10);
209
+ return apps[picked - 1] ?? apps[0];
210
+ } finally {
211
+ rl.close();
212
+ }
213
+ }
214
+ async function detectSpec(cwd) {
215
+ const specs = await findSpecs(cwd);
216
+ if (specs.length === 0) {
217
+ throw new Error(
218
+ "No OpenAPI spec found in this project.\nPoint at one: npx webmcp-codegen generate --spec path/to/openapi.json"
219
+ );
220
+ }
221
+ if (specs.length > 1) {
222
+ const list = specs.map((spec) => ` - ${spec}`).join("\n");
223
+ throw new Error(
224
+ `Found ${specs.length} API specs:
225
+ ${list}
226
+
227
+ Pick one: npx webmcp-codegen generate --spec ${specs[0]}`
228
+ );
229
+ }
230
+ console.log(`Detected ${specs[0]} (override with --spec)`);
231
+ return specs[0];
232
+ }
233
+
234
+ // src/dev/ui.ts
235
+ function dashboardHtml() {
236
+ return `<!DOCTYPE html>
237
+ <html lang="en">
238
+ <head>
239
+ <meta charset="utf-8" />
240
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
241
+ <title>webmcp-codegen</title>
242
+ <style>
243
+ :root {
244
+ --baseline: #0a0b0f;
245
+ --surface: #10131a;
246
+ --surface-raised: #161a23;
247
+ --line: #1e2330;
248
+ --line-subtle: #161a23;
249
+ --ink: #e9ecf2;
250
+ --dim: #9aa3b2;
251
+ --faint: #5d6575;
252
+ --ghost: #3b4150;
253
+ --accent: #58a6ff;
254
+ --accent-dim: rgba(88, 166, 255, 0.15);
255
+ --signal: #e3b341;
256
+ --signal-dim: rgba(227, 179, 65, 0.15);
257
+ --fault: #f47067;
258
+ --fault-dim: rgba(244, 112, 103, 0.15);
259
+ --sans: ui-sans-serif, system-ui, -apple-system, sans-serif;
260
+ --mono: ui-monospace, SFMono-Regular, Menlo, monospace;
261
+ }
262
+ * { box-sizing: border-box; }
263
+ html, body { margin: 0; height: 100%; }
264
+ body {
265
+ background: var(--baseline);
266
+ color: var(--ink);
267
+ font-family: var(--sans);
268
+ font-size: 14px;
269
+ -webkit-font-smoothing: antialiased;
270
+ overflow: hidden;
271
+ }
272
+ ::selection { background: var(--accent); color: var(--baseline); }
273
+
274
+ /* Layout */
275
+ .app { display: flex; height: 100vh; }
276
+ .sidebar {
277
+ width: 320px;
278
+ min-width: 320px;
279
+ border-right: 1px solid var(--line);
280
+ display: flex;
281
+ flex-direction: column;
282
+ background: var(--surface);
283
+ }
284
+ .main {
285
+ flex: 1;
286
+ overflow-y: auto;
287
+ background: var(--baseline);
288
+ }
289
+
290
+ /* Sidebar header */
291
+ .sidebar-header {
292
+ padding: 20px 20px 16px;
293
+ border-bottom: 1px solid var(--line-subtle);
294
+ }
295
+ .brand {
296
+ display: flex;
297
+ align-items: center;
298
+ gap: 10px;
299
+ font-weight: 600;
300
+ font-size: 15px;
301
+ margin-bottom: 4px;
302
+ }
303
+ .brand-mark {
304
+ width: 24px;
305
+ height: 24px;
306
+ background: linear-gradient(135deg, var(--accent), #7c3aed);
307
+ border-radius: 6px;
308
+ display: flex;
309
+ align-items: center;
310
+ justify-content: center;
311
+ font-size: 12px;
312
+ font-weight: 700;
313
+ color: white;
314
+ }
315
+ .brand-sub {
316
+ color: var(--faint);
317
+ font-size: 12px;
318
+ }
319
+
320
+ /* Search */
321
+ .search-wrap {
322
+ padding: 12px 16px;
323
+ border-bottom: 1px solid var(--line-subtle);
324
+ }
325
+ .search {
326
+ width: 100%;
327
+ background: var(--surface-raised);
328
+ border: 1px solid var(--line);
329
+ border-radius: 6px;
330
+ padding: 8px 12px 8px 32px;
331
+ color: var(--ink);
332
+ font-size: 13px;
333
+ font-family: inherit;
334
+ position: relative;
335
+ }
336
+ .search:focus {
337
+ outline: none;
338
+ border-color: var(--accent);
339
+ }
340
+ .search-icon {
341
+ position: absolute;
342
+ left: 28px;
343
+ top: 50%;
344
+ transform: translateY(-50%);
345
+ color: var(--faint);
346
+ pointer-events: none;
347
+ }
348
+ .search-wrap { position: relative; }
349
+
350
+ /* Tool list */
351
+ .tool-list {
352
+ flex: 1;
353
+ overflow-y: auto;
354
+ padding: 8px 0;
355
+ }
356
+ .tool-group {
357
+ padding: 8px 16px 4px;
358
+ font-size: 11px;
359
+ font-weight: 600;
360
+ text-transform: uppercase;
361
+ letter-spacing: 0.05em;
362
+ color: var(--faint);
363
+ }
364
+ .tool {
365
+ display: flex;
366
+ align-items: center;
367
+ gap: 10px;
368
+ width: 100%;
369
+ padding: 8px 16px;
370
+ border: none;
371
+ background: none;
372
+ color: var(--ink);
373
+ font-size: 13px;
374
+ font-family: var(--mono);
375
+ text-align: left;
376
+ cursor: pointer;
377
+ transition: background 0.1s;
378
+ }
379
+ .tool:hover { background: var(--surface-raised); }
380
+ .tool[aria-selected="true"] {
381
+ background: var(--accent-dim);
382
+ border-right: 2px solid var(--accent);
383
+ }
384
+ .tool-indicator {
385
+ width: 6px;
386
+ height: 6px;
387
+ border-radius: 50%;
388
+ flex-shrink: 0;
389
+ }
390
+ .tool-indicator.read { background: var(--accent); }
391
+ .tool-indicator.write { background: var(--signal); }
392
+ .tool-indicator.destructive { background: var(--fault); }
393
+ .tool-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
394
+ .tool-badge {
395
+ font-size: 10px;
396
+ padding: 2px 6px;
397
+ border-radius: 4px;
398
+ background: var(--surface-raised);
399
+ color: var(--dim);
400
+ text-transform: uppercase;
401
+ letter-spacing: 0.02em;
402
+ }
403
+ .tool-badge.disabled { color: var(--signal); }
404
+
405
+ /* Main content */
406
+ .detail {
407
+ max-width: 640px;
408
+ margin: 0 auto;
409
+ padding: 32px 40px;
410
+ }
411
+ .placeholder {
412
+ display: flex;
413
+ flex-direction: column;
414
+ align-items: center;
415
+ justify-content: center;
416
+ height: 100%;
417
+ color: var(--faint);
418
+ text-align: center;
419
+ padding: 40px;
420
+ }
421
+ .placeholder-icon {
422
+ width: 48px;
423
+ height: 48px;
424
+ border-radius: 12px;
425
+ background: var(--surface-raised);
426
+ display: flex;
427
+ align-items: center;
428
+ justify-content: center;
429
+ margin-bottom: 16px;
430
+ color: var(--ghost);
431
+ }
432
+ .placeholder kbd {
433
+ background: var(--surface-raised);
434
+ padding: 2px 6px;
435
+ border-radius: 4px;
436
+ font-family: var(--mono);
437
+ font-size: 12px;
438
+ }
439
+
440
+ /* Detail header */
441
+ .detail-header {
442
+ margin-bottom: 24px;
443
+ padding-bottom: 20px;
444
+ border-bottom: 1px solid var(--line-subtle);
445
+ }
446
+ .detail-crumb {
447
+ font-size: 12px;
448
+ color: var(--faint);
449
+ margin-bottom: 8px;
450
+ font-family: var(--mono);
451
+ }
452
+ .detail-title {
453
+ font-size: 24px;
454
+ font-weight: 600;
455
+ margin: 0 0 8px;
456
+ font-family: var(--mono);
457
+ }
458
+ .detail-route {
459
+ font-family: var(--mono);
460
+ font-size: 13px;
461
+ color: var(--dim);
462
+ display: flex;
463
+ align-items: center;
464
+ gap: 8px;
465
+ }
466
+ .verb {
467
+ font-weight: 600;
468
+ padding: 2px 6px;
469
+ border-radius: 4px;
470
+ font-size: 11px;
471
+ }
472
+ .verb.read { color: var(--accent); background: var(--accent-dim); }
473
+ .verb.write { color: var(--signal); background: var(--signal-dim); }
474
+ .verb.destructive { color: var(--fault); background: var(--fault-dim); }
475
+
476
+ /* Badges */
477
+ .badges {
478
+ display: flex;
479
+ gap: 8px;
480
+ margin-top: 12px;
481
+ flex-wrap: wrap;
482
+ }
483
+ .badge {
484
+ font-size: 11px;
485
+ padding: 3px 8px;
486
+ border-radius: 4px;
487
+ font-weight: 500;
488
+ }
489
+ .badge.read { color: var(--accent); background: var(--accent-dim); }
490
+ .badge.write { color: var(--signal); background: var(--signal-dim); }
491
+ .badge.destructive { color: var(--fault); background: var(--fault-dim); }
492
+ .badge.disabled { color: var(--signal); background: var(--signal-dim); }
493
+ .badge.auth { color: var(--fault); background: var(--fault-dim); }
494
+
495
+ /* Sections */
496
+ .section {
497
+ margin-bottom: 28px;
498
+ }
499
+ .section-label {
500
+ font-size: 11px;
501
+ font-weight: 600;
502
+ text-transform: uppercase;
503
+ letter-spacing: 0.05em;
504
+ color: var(--faint);
505
+ margin-bottom: 10px;
506
+ }
507
+
508
+ /* Description edit */
509
+ .description-edit {
510
+ width: 100%;
511
+ background: var(--surface);
512
+ border: 1px solid var(--line);
513
+ border-radius: 6px;
514
+ padding: 12px;
515
+ color: var(--ink);
516
+ font-size: 14px;
517
+ font-family: inherit;
518
+ line-height: 1.5;
519
+ resize: vertical;
520
+ min-height: 80px;
521
+ }
522
+ .description-edit:focus {
523
+ outline: none;
524
+ border-color: var(--accent);
525
+ }
526
+ .edit-actions {
527
+ display: flex;
528
+ align-items: center;
529
+ gap: 12px;
530
+ margin-top: 10px;
531
+ }
532
+ .btn {
533
+ padding: 8px 16px;
534
+ border-radius: 6px;
535
+ font-size: 13px;
536
+ font-weight: 500;
537
+ cursor: pointer;
538
+ transition: all 0.15s;
539
+ border: 1px solid var(--line);
540
+ background: var(--surface);
541
+ color: var(--ink);
542
+ }
543
+ .btn:hover { background: var(--surface-raised); border-color: var(--ghost); }
544
+ .btn-primary {
545
+ background: var(--accent);
546
+ border-color: var(--accent);
547
+ color: var(--baseline);
548
+ }
549
+ .btn-primary:hover { background: #4a95ee; border-color: #4a95ee; }
550
+ .saved-indicator {
551
+ font-size: 12px;
552
+ color: var(--accent);
553
+ opacity: 0;
554
+ transition: opacity 0.2s;
555
+ }
556
+ .saved-indicator.show { opacity: 1; }
557
+ .edit-hint {
558
+ font-size: 12px;
559
+ color: var(--faint);
560
+ margin-top: 8px;
561
+ line-height: 1.5;
562
+ }
563
+
564
+ /* Toggle */
565
+ .toggle-row {
566
+ display: flex;
567
+ align-items: center;
568
+ gap: 12px;
569
+ padding: 14px;
570
+ background: var(--surface);
571
+ border: 1px solid var(--line);
572
+ border-radius: 8px;
573
+ }
574
+ .switch {
575
+ width: 40px;
576
+ height: 22px;
577
+ border-radius: 11px;
578
+ background: var(--surface-raised);
579
+ border: 1px solid var(--line);
580
+ position: relative;
581
+ cursor: pointer;
582
+ transition: all 0.2s;
583
+ flex-shrink: 0;
584
+ }
585
+ .switch::after {
586
+ content: "";
587
+ position: absolute;
588
+ width: 16px;
589
+ height: 16px;
590
+ border-radius: 50%;
591
+ background: var(--dim);
592
+ top: 2px;
593
+ left: 2px;
594
+ transition: all 0.2s;
595
+ }
596
+ .switch[aria-checked="true"] {
597
+ background: var(--accent);
598
+ border-color: var(--accent);
599
+ }
600
+ .switch[aria-checked="true"]::after {
601
+ left: 20px;
602
+ background: white;
603
+ }
604
+ .toggle-copy { font-size: 13px; line-height: 1.5; }
605
+ .toggle-copy strong { display: block; margin-bottom: 2px; }
606
+
607
+ /* Try it */
608
+ .try-section {
609
+ background: var(--surface);
610
+ border: 1px solid var(--line);
611
+ border-radius: 8px;
612
+ overflow: hidden;
613
+ }
614
+ .try-header {
615
+ padding: 14px 16px;
616
+ border-bottom: 1px solid var(--line-subtle);
617
+ display: flex;
618
+ align-items: center;
619
+ justify-content: space-between;
620
+ }
621
+ .try-header h3 {
622
+ margin: 0;
623
+ font-size: 13px;
624
+ font-weight: 600;
625
+ }
626
+ .try-note {
627
+ font-size: 11px;
628
+ color: var(--faint);
629
+ }
630
+ .try-body { padding: 16px; }
631
+ .auth-note {
632
+ background: var(--signal-dim);
633
+ border: 1px solid var(--signal);
634
+ color: var(--signal);
635
+ padding: 10px 12px;
636
+ border-radius: 6px;
637
+ font-size: 12px;
638
+ margin-bottom: 14px;
639
+ line-height: 1.5;
640
+ }
641
+ .base-url-input {
642
+ width: 100%;
643
+ background: var(--baseline);
644
+ border: 1px solid var(--line);
645
+ border-radius: 6px;
646
+ padding: 8px 12px;
647
+ color: var(--ink);
648
+ font-size: 13px;
649
+ font-family: var(--mono);
650
+ margin-bottom: 14px;
651
+ }
652
+ .base-url-input:focus {
653
+ outline: none;
654
+ border-color: var(--accent);
655
+ }
656
+ .param-list { margin-bottom: 14px; }
657
+ .param {
658
+ margin-bottom: 12px;
659
+ }
660
+ .param-label {
661
+ display: block;
662
+ font-size: 12px;
663
+ font-weight: 500;
664
+ margin-bottom: 4px;
665
+ color: var(--dim);
666
+ }
667
+ .param-label .req { color: var(--fault); }
668
+ .param-hint {
669
+ font-size: 11px;
670
+ color: var(--faint);
671
+ margin-top: 2px;
672
+ }
673
+ .param-input {
674
+ width: 100%;
675
+ background: var(--baseline);
676
+ border: 1px solid var(--line);
677
+ border-radius: 6px;
678
+ padding: 8px 12px;
679
+ color: var(--ink);
680
+ font-size: 13px;
681
+ font-family: var(--mono);
682
+ }
683
+ .param-input:focus {
684
+ outline: none;
685
+ border-color: var(--accent);
686
+ }
687
+ .run-btn {
688
+ width: 100%;
689
+ padding: 10px;
690
+ background: var(--accent);
691
+ border: none;
692
+ border-radius: 6px;
693
+ color: var(--baseline);
694
+ font-size: 13px;
695
+ font-weight: 600;
696
+ cursor: pointer;
697
+ transition: background 0.15s;
698
+ }
699
+ .run-btn:hover { background: #4a95ee; }
700
+ .run-btn:disabled { opacity: 0.5; cursor: not-allowed; }
701
+ .result {
702
+ margin-top: 14px;
703
+ padding: 12px;
704
+ background: var(--baseline);
705
+ border: 1px solid var(--line);
706
+ border-radius: 6px;
707
+ font-family: var(--mono);
708
+ font-size: 12px;
709
+ white-space: pre-wrap;
710
+ word-break: break-all;
711
+ max-height: 300px;
712
+ overflow-y: auto;
713
+ }
714
+ .result.ok { border-color: var(--accent); }
715
+ .result.err { border-color: var(--fault); }
716
+
717
+ /* Findings */
718
+ .findings {
719
+ margin-bottom: 20px;
720
+ }
721
+ .finding {
722
+ display: flex;
723
+ gap: 8px;
724
+ padding: 10px 12px;
725
+ background: var(--surface);
726
+ border: 1px solid var(--line);
727
+ border-radius: 6px;
728
+ margin-bottom: 8px;
729
+ font-size: 13px;
730
+ line-height: 1.5;
731
+ }
732
+ .finding.warning { border-left: 3px solid var(--signal); }
733
+ .finding.error { border-left: 3px solid var(--fault); }
734
+ .finding-icon { flex-shrink: 0; }
735
+
736
+ /* Scrollbar */
737
+ ::-webkit-scrollbar { width: 8px; height: 8px; }
738
+ ::-webkit-scrollbar-track { background: transparent; }
739
+ ::-webkit-scrollbar-thumb { background: var(--line); border-radius: 4px; }
740
+ ::-webkit-scrollbar-thumb:hover { background: var(--ghost); }
741
+ </style>
742
+ </head>
743
+ <body>
744
+ <div class="app">
745
+ <aside class="sidebar">
746
+ <div class="sidebar-header">
747
+ <div class="brand">
748
+ <div class="brand-mark">W</div>
749
+ <span>webmcp-codegen</span>
750
+ </div>
751
+ <div class="brand-sub" id="tool-count"></div>
752
+ </div>
753
+ <div class="search-wrap">
754
+ <svg class="search-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
755
+ <circle cx="11" cy="11" r="8"></circle>
756
+ <path d="m21 21-4.35-4.35"></path>
757
+ </svg>
758
+ <input type="text" class="search" id="search" placeholder="Search tools..." spellcheck="false" />
759
+ </div>
760
+ <div class="tool-list" id="tool-list"></div>
761
+ </aside>
762
+ <main class="main" id="main">
763
+ <div class="placeholder" id="placeholder">
764
+ <div class="placeholder-icon">
765
+ <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
766
+ <path d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"/>
767
+ </svg>
768
+ </div>
769
+ <p>Select a tool to view details</p>
770
+ <p style="font-size: 12px; margin-top: 8px;">
771
+ <kbd>\u2191</kbd> <kbd>\u2193</kbd> to navigate &nbsp;\xB7&nbsp; <kbd>\u2318K</kbd> to search
772
+ </p>
773
+ </div>
774
+ <div class="detail" id="detail" hidden></div>
775
+ </main>
776
+ </div>
777
+
778
+ <script>
779
+ (function () {
780
+ var state = null;
781
+ var selected = null;
782
+ var filter = "";
783
+
784
+ var listEl = document.getElementById("tool-list");
785
+ var detailEl = document.getElementById("detail");
786
+ var placeholderEl = document.getElementById("placeholder");
787
+ var searchEl = document.getElementById("search");
788
+ var countEl = document.getElementById("tool-count");
789
+
790
+ function esc(text) {
791
+ var div = document.createElement("div");
792
+ div.textContent = text == null ? "" : String(text);
793
+ return div.innerHTML;
794
+ }
795
+
796
+ function api(path, options) {
797
+ return fetch(path, options).then(function (res) {
798
+ if (!res.ok) throw new Error("Request failed: " + res.status);
799
+ return res.json();
800
+ });
801
+ }
802
+
803
+ function load() {
804
+ api("/api/state").then(function (data) {
805
+ state = data;
806
+ countEl.textContent = data.tools.length + " tools from " + data.label;
807
+ renderList();
808
+ renderDetail();
809
+ }).catch(function (error) {
810
+ console.error("Failed to load tools:", error);
811
+ countEl.textContent = "Failed to load";
812
+ listEl.innerHTML = '<div style="padding: 20px; text-align: center; color: var(--fault);">Error loading tools: ' + esc(error.message) + '</div>';
813
+ });
814
+ }
815
+
816
+ function visibleTools() {
817
+ if (!state) return [];
818
+ var f = filter.toLowerCase();
819
+ return state.tools.filter(function (tool) {
820
+ return tool.name.toLowerCase().indexOf(f) !== -1 ||
821
+ (tool.description && tool.description.toLowerCase().indexOf(f) !== -1);
822
+ });
823
+ }
824
+
825
+ function groupTools(tools) {
826
+ var groups = { read: [], write: [], destructive: [] };
827
+ tools.forEach(function (tool) {
828
+ var key = tool.sideEffect || "read";
829
+ if (!groups[key]) groups[key] = [];
830
+ groups[key].push(tool);
831
+ });
832
+ return groups;
833
+ }
834
+
835
+ function renderList() {
836
+ var tools = visibleTools();
837
+ var groups = groupTools(tools);
838
+ var html = "";
839
+
840
+ ["read", "write", "destructive"].forEach(function (risk) {
841
+ var group = groups[risk];
842
+ if (!group || group.length === 0) return;
843
+ html += '<div class="tool-group">' + risk + ' (' + group.length + ')</div>';
844
+ group.forEach(function (tool) {
845
+ var isSelected = tool.name === selected;
846
+ html += '<button class="tool" data-name="' + esc(tool.name) + '" aria-selected="' + isSelected + '">' +
847
+ '<span class="tool-indicator ' + risk + '"></span>' +
848
+ '<span class="tool-name">' + esc(tool.name) + "</span>" +
849
+ (!tool.enabled ? '<span class="tool-badge disabled">off</span>' : "") +
850
+ "</button>";
851
+ });
852
+ });
853
+
854
+ if (tools.length === 0) {
855
+ html = '<div style="padding: 20px; text-align: center; color: var(--faint);">No tools match your search</div>';
856
+ }
857
+
858
+ listEl.innerHTML = html;
859
+
860
+ Array.prototype.forEach.call(listEl.querySelectorAll(".tool"), function (btn) {
861
+ btn.addEventListener("click", function () {
862
+ selected = btn.getAttribute("data-name");
863
+ renderList();
864
+ renderDetail();
865
+ });
866
+ });
867
+ }
868
+
869
+ function currentTool() {
870
+ if (!state || !selected) return null;
871
+ return state.tools.find(function (tool) { return tool.name === selected; });
872
+ }
873
+
874
+ function renderDetail() {
875
+ var tool = currentTool();
876
+ if (!tool) {
877
+ detailEl.hidden = true;
878
+ placeholderEl.hidden = false;
879
+ return;
880
+ }
881
+
882
+ placeholderEl.hidden = true;
883
+ detailEl.hidden = false;
884
+
885
+ var badges = [
886
+ '<span class="badge ' + tool.sideEffect + '">' + tool.sideEffect + "</span>",
887
+ !tool.enabled ? '<span class="badge disabled">starts disabled</span>' : "",
888
+ tool.endpointRole !== "endpoint" ? '<span class="badge auth">' + tool.endpointRole + "</span>" : "",
889
+ tool.piiInOutput.length > 0 ? '<span class="badge write">pii: ' + esc(tool.piiInOutput.join(", ")) + "</span>" : "",
890
+ ].filter(Boolean).join("");
891
+
892
+ var findings = tool.findings.map(function (finding) {
893
+ var icon = finding.level === "error" ? "\u2716" : "\u26A0";
894
+ return '<div class="finding ' + finding.level + '"><span class="finding-icon">' + icon + "</span><span>" + esc(finding.message) + "</span></div>";
895
+ }).join("");
896
+
897
+ var schema = tool.inputSchema || {};
898
+ var properties = schema.properties || {};
899
+ var required = schema.required || [];
900
+ var fields = Object.keys(properties).map(function (key) {
901
+ var field = properties[key];
902
+ var type = field.type === "number" || field.type === "integer" ? "number" : "text";
903
+ var req = required.indexOf(key) !== -1 ? ' <span class="req">*</span>' : "";
904
+ var hint = field.description ? '<div class="param-hint">' + esc(field.description) + "</div>" : "";
905
+ return '<div class="param"><label class="param-label">' + esc(key) + req + '</label>' +
906
+ '<input class="param-input" data-field="' + esc(key) + '" data-type="' + esc(field.type || "string") + '" type="' + type + '" spellcheck="false" />' +
907
+ hint + "</div>";
908
+ }).join("");
909
+
910
+ var baseUrl = "";
911
+ try { baseUrl = localStorage.getItem("webmcp-codegen:baseUrl") || tool.serverUrl || ""; } catch (e) {}
912
+
913
+ detailEl.innerHTML =
914
+ '<div class="detail-header">' +
915
+ '<div class="detail-crumb">' + esc(state.label) + (state.outDir ? " \u2192 " + esc(state.outDir) : "") + "</div>" +
916
+ '<h1 class="detail-title">' + esc(tool.name) + "</h1>" +
917
+ '<div class="detail-route">' +
918
+ '<span class="verb ' + tool.sideEffect + '">' + esc(tool.verb || "GET") + "</span>" +
919
+ "<span>" + esc(tool.path || "") + "</span>" +
920
+ "</div>" +
921
+ '<div class="badges">' + badges + "</div>" +
922
+ "</div>" +
923
+
924
+ (findings ? '<div class="section"><div class="section-label">Audit findings</div>' + findings + "</div>" : "") +
925
+
926
+ '<div class="section">' +
927
+ '<div class="section-label">Description</div>' +
928
+ '<textarea class="description-edit" id="desc" spellcheck="false">' + esc(tool.description) + "</textarea>" +
929
+ '<div class="edit-actions">' +
930
+ '<button class="btn btn-primary" id="save-desc">Save</button>' +
931
+ '<span class="saved-indicator" id="saved">Saved</span>' +
932
+ "</div>" +
933
+ '<div class="edit-hint">Agents pick tools by this text. Saved to .webmcp-codegen.json, so it survives regeneration. \u2318S to save.</div>' +
934
+ "</div>" +
935
+
936
+ '<div class="section">' +
937
+ '<div class="section-label">Status</div>' +
938
+ '<div class="toggle-row">' +
939
+ '<button class="switch" id="toggle-enabled" role="switch" aria-checked="' + tool.enabled + '" aria-label="Enabled"></button>' +
940
+ '<div class="toggle-copy"><strong>' + (tool.enabled ? "Enabled" : "Disabled") + "</strong>" +
941
+ (tool.enabled
942
+ ? "This tool works as soon as the app registers it."
943
+ : "The generated code is there, commented out. Flipping this regenerates it enabled on the next run.") +
944
+ "</div></div>" +
945
+ "</div>" +
946
+
947
+ '<div class="section">' +
948
+ '<div class="section-label">Test</div>' +
949
+ '<div class="try-section">' +
950
+ '<div class="try-header"><h3>Run this tool</h3><span class="try-note">server-side, no browser session</span></div>' +
951
+ '<div class="try-body">' +
952
+ (tool.requiresAuth
953
+ ? '<div class="auth-note">\u26A0 This endpoint requires a browser session. The dashboard runs server-side, so you will get a 401. Test it in Chrome DevTools where you are signed in.</div>'
954
+ : "") +
955
+ '<input class="base-url-input" id="base-url" type="text" placeholder="Base URL (e.g. http://localhost:3000)" value="' + esc(baseUrl) + '" spellcheck="false" />' +
956
+ (fields || '<div style="color: var(--faint); font-size: 13px; margin-bottom: 14px;">This tool takes no inputs.</div>') +
957
+ '<button class="run-btn" id="run">Run tool</button>' +
958
+ '<pre class="result" id="result" hidden></pre>' +
959
+ "</div></div>" +
960
+ "</div>";
961
+
962
+ document.getElementById("save-desc").addEventListener("click", saveDescription);
963
+ document.getElementById("toggle-enabled").addEventListener("click", toggleEnabled);
964
+ document.getElementById("run").addEventListener("click", runTool);
965
+ document.getElementById("base-url").addEventListener("change", function (event) {
966
+ try { localStorage.setItem("webmcp-codegen:baseUrl", event.target.value); } catch (e) {}
967
+ });
968
+ }
969
+
970
+ function saveDescription() {
971
+ var tool = currentTool();
972
+ var desc = document.getElementById("desc").value.trim();
973
+ if (!tool || !desc) return;
974
+ api("/api/override", {
975
+ method: "POST",
976
+ headers: { "content-type": "application/json" },
977
+ body: JSON.stringify({ name: tool.name, description: desc }),
978
+ }).then(function () {
979
+ tool.description = desc;
980
+ var saved = document.getElementById("saved");
981
+ saved.classList.add("show");
982
+ setTimeout(function () { saved.classList.remove("show"); }, 2000);
983
+ }).catch(function (error) { alert(error.message); });
984
+ }
985
+
986
+ function toggleEnabled() {
987
+ var tool = currentTool();
988
+ if (!tool) return;
989
+ var next = !tool.enabled;
990
+ api("/api/override", {
991
+ method: "POST",
992
+ headers: { "content-type": "application/json" },
993
+ body: JSON.stringify({ name: tool.name, enabled: next }),
994
+ }).then(function () {
995
+ tool.enabled = next;
996
+ renderList();
997
+ renderDetail();
998
+ }).catch(function (error) { alert(error.message); });
999
+ }
1000
+
1001
+ function runTool() {
1002
+ var tool = currentTool();
1003
+ if (!tool) return;
1004
+ var input = {};
1005
+ Array.prototype.forEach.call(document.querySelectorAll("[data-field]"), function (field) {
1006
+ var value = field.value;
1007
+ if (value === "") return;
1008
+ var type = field.getAttribute("data-type");
1009
+ if (type === "number" || type === "integer") value = Number(value);
1010
+ if (type === "boolean") value = value === "true";
1011
+ if (type === "object" || type === "array") {
1012
+ try { value = JSON.parse(value); } catch (e) { /* keep as string */ }
1013
+ }
1014
+ input[field.getAttribute("data-field")] = value;
1015
+ });
1016
+ var baseUrl = document.getElementById("base-url").value.trim();
1017
+ var resultEl = document.getElementById("result");
1018
+ var runEl = document.getElementById("run");
1019
+ runEl.disabled = true;
1020
+ runEl.textContent = "Running...";
1021
+ resultEl.hidden = true;
1022
+ api("/api/run", {
1023
+ method: "POST",
1024
+ headers: { "content-type": "application/json" },
1025
+ body: JSON.stringify({ name: tool.name, input: input, baseUrl: baseUrl || undefined }),
1026
+ }).then(function (result) {
1027
+ resultEl.hidden = false;
1028
+ resultEl.className = "result " + (result.ok ? "ok" : "err");
1029
+ resultEl.textContent =
1030
+ (result.status ? "HTTP " + result.status + "
1031
+
1032
+ " : "") +
1033
+ (result.error ? result.error : JSON.stringify(result.body, null, 2));
1034
+ }).catch(function (error) {
1035
+ resultEl.hidden = false;
1036
+ resultEl.className = "result err";
1037
+ resultEl.textContent = error.message;
1038
+ }).finally(function () {
1039
+ runEl.disabled = false;
1040
+ runEl.textContent = "Run tool";
1041
+ });
1042
+ }
1043
+
1044
+ /* Keyboard navigation */
1045
+ document.addEventListener("keydown", function (event) {
1046
+ if ((event.metaKey || event.ctrlKey) && event.key === "k") {
1047
+ event.preventDefault();
1048
+ searchEl.focus();
1049
+ return;
1050
+ }
1051
+ if ((event.metaKey || event.ctrlKey) && event.key === "s") {
1052
+ event.preventDefault();
1053
+ saveDescription();
1054
+ return;
1055
+ }
1056
+ if (event.target === searchEl || event.target.tagName === "TEXTAREA" || event.target.tagName === "INPUT") {
1057
+ return;
1058
+ }
1059
+ if (event.key !== "ArrowDown" && event.key !== "ArrowUp") return;
1060
+ var tools = visibleTools();
1061
+ var index = tools.findIndex(function (tool) { return tool.name === selected; });
1062
+ var next = event.key === "ArrowDown" ? index + 1 : index - 1;
1063
+ if (next < 0 || next >= tools.length) return;
1064
+ event.preventDefault();
1065
+ selected = tools[next].name;
1066
+ renderList();
1067
+ renderDetail();
1068
+ var button = listEl.querySelector('[aria-selected="true"]');
1069
+ if (button) button.scrollIntoView({ block: "nearest" });
1070
+ });
1071
+
1072
+ searchEl.addEventListener("input", function (event) {
1073
+ filter = event.target.value;
1074
+ renderList();
1075
+ });
1076
+
1077
+ load();
1078
+ })();
1079
+ </script>
1080
+ </body>
1081
+ </html>`;
1082
+ }
1083
+
1084
+ // src/dev/server.ts
1085
+ async function startDevServer(options) {
1086
+ const setup = await resolveSetup(options.cwd, {
1087
+ dryRun: true,
1088
+ skipAudit: false,
1089
+ force: false,
1090
+ watch: false
1091
+ });
1092
+ async function currentState() {
1093
+ const data = await loadDataFile(options.cwd);
1094
+ const result = await runGenerate(setup.config, {
1095
+ cwd: options.cwd,
1096
+ dryRun: true,
1097
+ overrides: data.overrides
1098
+ });
1099
+ return {
1100
+ label: setup.label,
1101
+ outDir: setup.config.generate[0]?.outDir,
1102
+ tools: result.tools.map((tool) => toUiTool(tool, result.findings)),
1103
+ skipped: result.skipped,
1104
+ notes: result.notes
1105
+ };
1106
+ }
1107
+ const server = createServer(async (request, response) => {
1108
+ try {
1109
+ await route(request, response);
1110
+ } catch (error) {
1111
+ sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) });
1112
+ }
1113
+ });
1114
+ async function route(request, response) {
1115
+ const url = new URL(request.url ?? "/", "http://localhost");
1116
+ if (request.method === "GET" && url.pathname === "/") {
1117
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8" });
1118
+ response.end(dashboardHtml());
1119
+ return;
1120
+ }
1121
+ if (request.method === "GET" && url.pathname === "/api/state") {
1122
+ sendJson(response, 200, await currentState());
1123
+ return;
1124
+ }
1125
+ if (request.method === "POST" && url.pathname === "/api/override") {
1126
+ const body = await readJson(request);
1127
+ if (!body.name) {
1128
+ sendJson(response, 400, { error: "Missing tool name." });
1129
+ return;
1130
+ }
1131
+ const data = await loadDataFile(options.cwd);
1132
+ const overrides = { ...data.overrides ?? {} };
1133
+ const existing = overrides[body.name] ?? {};
1134
+ overrides[body.name] = {
1135
+ ...existing,
1136
+ ...body.description !== void 0 ? { description: body.description } : {},
1137
+ ...body.enabled !== void 0 ? { enabled: body.enabled } : {}
1138
+ };
1139
+ await saveDataFile(options.cwd, { overrides });
1140
+ sendJson(response, 200, { ok: true, saved: `.webmcp-codegen.json` });
1141
+ return;
1142
+ }
1143
+ if (request.method === "POST" && url.pathname === "/api/run") {
1144
+ const body = await readJson(request);
1145
+ const state = await currentState();
1146
+ const tool = state.tools.find((candidate) => candidate.name === body.name);
1147
+ if (!tool) {
1148
+ sendJson(response, 404, { error: `No tool named "${body.name}".` });
1149
+ return;
1150
+ }
1151
+ const result = await runEndpoint(tool, body.input ?? {}, body.baseUrl);
1152
+ sendJson(response, result.ok ? 200 : 502, result);
1153
+ return;
1154
+ }
1155
+ sendJson(response, 404, { error: "Not found" });
1156
+ }
1157
+ await new Promise(
1158
+ (resolveListen) => server.listen(options.port, "127.0.0.1", resolveListen)
1159
+ );
1160
+ if (options.open !== false) openBrowser(`http://localhost:${options.port}`);
1161
+ return server;
1162
+ }
1163
+ function toUiTool(tool, findings) {
1164
+ const [verb, ...rest] = tool.source.ref.split(" ");
1165
+ return {
1166
+ name: tool.name,
1167
+ verb,
1168
+ path: rest.join(" "),
1169
+ description: tool.description,
1170
+ sideEffect: tool.sideEffect,
1171
+ riskTier: tool.riskTier,
1172
+ enabled: tool.enabledByDefault,
1173
+ endpointRole: tool.endpointRole,
1174
+ piiInOutput: tool.piiInOutput,
1175
+ inputSchema: tool.inputSchema,
1176
+ ...tool.pathTemplate ? { pathTemplate: tool.pathTemplate } : {},
1177
+ ...tool.paramLocations ? { paramLocations: tool.paramLocations } : {},
1178
+ ...tool.serverUrl ? { serverUrl: tool.serverUrl } : {},
1179
+ requiresAuth: tool.requiresAuth,
1180
+ findings: findings.filter((finding) => finding.tool === tool.name).map((finding) => ({ level: finding.level, message: finding.message }))
1181
+ };
1182
+ }
1183
+ async function runEndpoint(tool, input, baseUrlOverride) {
1184
+ const base = baseUrlOverride ?? tool.serverUrl;
1185
+ if (!base) {
1186
+ return {
1187
+ ok: false,
1188
+ error: `No base URL: the spec lists no absolute server. Type your app's URL (e.g. http://localhost:3000) in the "base URL" field and run again.`
1189
+ };
1190
+ }
1191
+ if (!tool.pathTemplate || !tool.verb) {
1192
+ return { ok: false, error: "This tool has no route to call." };
1193
+ }
1194
+ let path = tool.pathTemplate;
1195
+ for (const param of tool.paramLocations?.path ?? []) {
1196
+ path = path.replace(`{${param}}`, encodeURIComponent(String(input[param] ?? "")));
1197
+ }
1198
+ const url = new URL(path, base);
1199
+ for (const param of tool.paramLocations?.query ?? []) {
1200
+ const value = input[param];
1201
+ if (value !== void 0 && value !== null) url.searchParams.set(param, String(value));
1202
+ }
1203
+ const bodyFields = tool.paramLocations?.body ?? [];
1204
+ const body = bodyFields.length === 1 && bodyFields[0] === "body" ? input.body : bodyFields.length > 0 ? Object.fromEntries(bodyFields.map((field) => [field, input[field]])) : void 0;
1205
+ try {
1206
+ const response = await fetch(url, {
1207
+ method: tool.verb,
1208
+ headers: body !== void 0 ? { "content-type": "application/json" } : void 0,
1209
+ body: body !== void 0 ? JSON.stringify(body) : void 0
1210
+ });
1211
+ const text = await response.text();
1212
+ let parsed = text;
1213
+ try {
1214
+ parsed = JSON.parse(text);
1215
+ } catch {
1216
+ }
1217
+ return { ok: response.ok, status: response.status, body: parsed };
1218
+ } catch (error) {
1219
+ return { ok: false, error: error instanceof Error ? error.message : String(error) };
1220
+ }
1221
+ }
1222
+ function sendJson(response, status, body) {
1223
+ response.writeHead(status, {
1224
+ "content-type": "application/json"
1225
+ });
1226
+ response.end(JSON.stringify(body));
1227
+ }
1228
+ function readJson(request) {
1229
+ return new Promise((resolveRead, reject) => {
1230
+ let text = "";
1231
+ request.on("data", (chunk) => {
1232
+ text += chunk.toString("utf8");
1233
+ });
1234
+ request.on("end", () => {
1235
+ try {
1236
+ resolveRead(text ? JSON.parse(text) : {});
1237
+ } catch {
1238
+ reject(new Error("Invalid JSON body"));
1239
+ }
1240
+ });
1241
+ request.on("error", reject);
1242
+ });
1243
+ }
1244
+ function openBrowser(url) {
1245
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
1246
+ spawn(command, [url], { stdio: "ignore", shell: process.platform === "win32" }).unref();
1247
+ }
1248
+
1249
+ export {
1250
+ saveDataFile,
1251
+ findSpecs,
1252
+ resolveSetup,
1253
+ startDevServer
1254
+ };
1255
+ //# sourceMappingURL=chunk-MUTXYBL6.js.map