webmcp-codegen 0.2.1 → 0.3.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/dist/cli.js CHANGED
@@ -1,58 +1,153 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- CONFIG_FILE_NAMES,
4
- loadConfig,
5
- runGenerate
6
- } from "./chunk-LYHLGSAI.js";
7
- import {
8
- js
9
- } from "./chunk-OILNQ2HE.js";
3
+ findSpecs,
4
+ loadDataFile,
5
+ resolveSetup,
6
+ saveDataFile
7
+ } from "./chunk-JVBVTHZ7.js";
10
8
  import {
11
- openapi
12
- } from "./chunk-N245GNCB.js";
13
- import "./chunk-BIKKPCRT.js";
9
+ runGenerate
10
+ } from "./chunk-MJQ5B6HB.js";
11
+ import "./chunk-TGOJ3HUE.js";
12
+ import "./chunk-3LTHWIAP.js";
13
+ import "./chunk-FWSATV7C.js";
14
14
  import "./chunk-KSQMJERY.js";
15
15
 
16
16
  // src/cli.ts
17
17
  import { existsSync, watch } from "fs";
18
- import { writeFile } from "fs/promises";
19
- import { basename, join as join2, relative as relative2 } from "path";
18
+ import { writeFile as writeFile2 } from "fs/promises";
19
+ import { join as join2, relative as relative2 } from "path";
20
+ import "readline/promises";
20
21
  import { parseArgs } from "util";
21
22
 
22
- // src/detect.ts
23
- import { readdir } from "fs/promises";
24
- import { join, relative } from "path";
25
- var SPEC_FILE_PATTERN = /^(openapi|swagger|api)\.(ya?ml|json)$/i;
26
- var IGNORED_DIRS = /* @__PURE__ */ new Set([
27
- "node_modules",
28
- ".git",
29
- ".turbo",
30
- ".next",
31
- "dist",
32
- "build",
33
- "coverage"
34
- ]);
35
- var MAX_DEPTH = 5;
36
- async function findSpecs(cwd) {
37
- const found = [];
38
- async function walk(dir, depth) {
39
- if (depth > MAX_DEPTH) return;
40
- let entries;
41
- try {
42
- entries = await readdir(dir, { withFileTypes: true });
43
- } catch {
44
- return;
23
+ // src/wire.ts
24
+ import { readFile, writeFile } from "fs/promises";
25
+ import { dirname, join, relative } from "path";
26
+ async function planWiring(cwd, app, outDir) {
27
+ switch (app.framework) {
28
+ case "next":
29
+ return planNextWiring(cwd, app, outDir);
30
+ case "vite-react":
31
+ return planViteWiring(cwd, app, outDir);
32
+ default:
33
+ return null;
34
+ }
35
+ }
36
+ async function applyWiring(plan) {
37
+ for (const edit of plan.edits) {
38
+ await writeFile(edit.path, edit.contents, "utf8");
39
+ }
40
+ }
41
+ async function planNextWiring(cwd, app, outDir) {
42
+ const layoutCandidates = [
43
+ join(cwd, app.dir, "src/app/layout.tsx"),
44
+ join(cwd, app.dir, "src/app/layout.jsx"),
45
+ join(cwd, app.dir, "app/layout.tsx"),
46
+ join(cwd, app.dir, "app/layout.jsx")
47
+ ];
48
+ const layoutPath = await firstExisting(layoutCandidates);
49
+ if (!layoutPath) return null;
50
+ const registerPath = join(cwd, outDir, "register.tsx");
51
+ const layout = await readFile(layoutPath, "utf8");
52
+ if (layout.includes("WebMCPRegister")) return { edits: [], alreadyWired: true };
53
+ const importPath = withoutExtension(relative(dirname(layoutPath), registerPath));
54
+ const edits = [
55
+ {
56
+ path: registerPath,
57
+ action: "create",
58
+ contents: nextRegisterComponent(),
59
+ summary: `created ${relative(cwd, registerPath)} (a client component that registers your tools on page load)`
45
60
  }
46
- for (const entry of entries) {
47
- if (entry.isDirectory()) {
48
- if (!IGNORED_DIRS.has(entry.name)) await walk(join(dir, entry.name), depth + 1);
49
- } else if (SPEC_FILE_PATTERN.test(entry.name)) {
50
- found.push({ path: join(dir, entry.name), depth });
61
+ ];
62
+ const withImport = insertAfterLastImport(
63
+ layout,
64
+ `import { WebMCPRegister } from "${importPath}";`
65
+ );
66
+ if (!withImport) return null;
67
+ const withComponent = withImport.replace(
68
+ /<body([^>]*)>\s*/,
69
+ "<body$1>\n <WebMCPRegister />\n "
70
+ );
71
+ if (withComponent === withImport) return null;
72
+ edits.push({
73
+ path: layoutPath,
74
+ action: "modify",
75
+ contents: withComponent,
76
+ summary: `added 2 lines to ${relative(cwd, layoutPath)} (an import and <WebMCPRegister /> inside <body>)`
77
+ });
78
+ return { edits };
79
+ }
80
+ function nextRegisterComponent() {
81
+ return `"use client";
82
+
83
+ import { useEffect } from "react";
84
+ import { registerAllTools } from "./index";
85
+
86
+ /**
87
+ * Registers the generated WebMCP tools once, on page load.
88
+ * Generated by webmcp-codegen. Safe to move; keep it mounted near the root.
89
+ */
90
+ export function WebMCPRegister() {
91
+ useEffect(() => {
92
+ void registerAllTools();
93
+ }, []);
94
+ return null;
95
+ }
96
+ `;
97
+ }
98
+ async function planViteWiring(cwd, app, outDir) {
99
+ const entryCandidates = [
100
+ join(cwd, app.dir, "src/main.tsx"),
101
+ join(cwd, app.dir, "src/main.jsx"),
102
+ join(cwd, app.dir, "src/index.tsx"),
103
+ join(cwd, app.dir, "src/index.jsx")
104
+ ];
105
+ const entryPath = await firstExisting(entryCandidates);
106
+ if (!entryPath) return null;
107
+ const entry = await readFile(entryPath, "utf8");
108
+ if (entry.includes("registerAllTools")) return { edits: [], alreadyWired: true };
109
+ const importPath = withoutExtension(relative(dirname(entryPath), join(cwd, outDir, "index")));
110
+ const withWiring = insertAfterLastImport(
111
+ entry,
112
+ `import { registerAllTools } from "${importPath}";
113
+
114
+ void registerAllTools();`
115
+ );
116
+ if (!withWiring) return null;
117
+ return {
118
+ edits: [
119
+ {
120
+ path: entryPath,
121
+ action: "modify",
122
+ contents: withWiring,
123
+ summary: `added 2 lines to ${relative(cwd, entryPath)} (an import and a registerAllTools() call)`
51
124
  }
125
+ ]
126
+ };
127
+ }
128
+ function insertAfterLastImport(source, line) {
129
+ const lines = source.split("\n");
130
+ let lastImport = -1;
131
+ for (let index = 0; index < lines.length; index += 1) {
132
+ if (/^import\s/.test(lines[index])) lastImport = index;
133
+ }
134
+ if (lastImport === -1) return null;
135
+ lines.splice(lastImport + 1, 0, line);
136
+ return lines.join("\n");
137
+ }
138
+ function withoutExtension(path) {
139
+ const bare = path.replace(/\.(tsx?|jsx?)$/, "").replace(/\/index$/, "");
140
+ return bare.startsWith(".") ? bare : `./${bare}`;
141
+ }
142
+ async function firstExisting(paths) {
143
+ for (const path of paths) {
144
+ try {
145
+ await readFile(path, "utf8");
146
+ return path;
147
+ } catch {
52
148
  }
53
149
  }
54
- await walk(cwd, 0);
55
- return found.sort((a, b) => a.depth - b.depth).map((entry) => relative(cwd, entry.path));
150
+ return void 0;
56
151
  }
57
152
 
58
153
  // src/cli.ts
@@ -60,22 +155,27 @@ var HELP = `webmcp-codegen: generate WebMCP tools from the API contracts you alr
60
155
 
61
156
  Fastest start (no install, no config):
62
157
  npx webmcp-codegen generate --dry-run Detect your spec, preview the tools
63
- npx webmcp-codegen generate Write the tool files
158
+ npx webmcp-codegen generate Write the tool files, wire them up
64
159
 
65
160
  Commands:
66
161
  init Write a codegen.config.mjs for full control
67
162
  generate Generate (or update) your WebMCP tools
68
163
  generate --watch Re-generate when files change
164
+ dev Open the tools dashboard (list, edit, try tools)
69
165
 
70
166
  Flags for generate:
71
167
  --spec PATH Which OpenAPI spec to use (auto-detected when omitted)
72
- --out DIR Where the tool files go (default: ./src/webmcp)
168
+ --out DIR Where the tool files go (default: your web app's src/webmcp)
73
169
  --dry-run Preview what would be written, write nothing
74
170
  --skip-audit Skip the safety report
75
171
  --force Write files even when the audit reports errors
76
172
  --config PATH Use a config file at PATH
173
+
174
+ Flags for dev:
175
+ --port N Dashboard port (default: 4700)
77
176
  `;
78
177
  var CONFIG_FILE = "codegen.config.mjs";
178
+ var MAX_LISTED_TOOLS = 15;
79
179
  async function main() {
80
180
  const { positionals, values } = parseArgs({
81
181
  allowPositionals: true,
@@ -87,6 +187,7 @@ async function main() {
87
187
  config: { type: "string" },
88
188
  spec: { type: "string" },
89
189
  out: { type: "string" },
190
+ port: { type: "string" },
90
191
  help: { type: "boolean", default: false }
91
192
  }
92
193
  });
@@ -98,6 +199,8 @@ async function main() {
98
199
  switch (command) {
99
200
  case "init":
100
201
  return init();
202
+ case "dev":
203
+ return dev(Number.parseInt(values.port ?? "4700", 10));
101
204
  case "generate":
102
205
  return generate({
103
206
  dryRun: values["dry-run"],
@@ -124,7 +227,7 @@ async function init() {
124
227
  }
125
228
  const specs = await findSpecs(cwd);
126
229
  const specPath = specs.length > 0 ? `./${specs[0]}` : "./openapi.yaml";
127
- await writeFile(
230
+ await writeFile2(
128
231
  configPath,
129
232
  `import { defineConfig } from "webmcp-codegen";
130
233
  import { openapi } from "webmcp-codegen/sources";
@@ -163,63 +266,58 @@ async function generate(flags) {
163
266
  const result = await runOnce(cwd, flags);
164
267
  return result.blocked ? 1 : 0;
165
268
  }
166
- async function resolveConfig(cwd, flags) {
167
- const hasConfigFile = flags.configPath ? existsSync(join2(cwd, flags.configPath)) : CONFIG_FILE_NAMES.some((name) => existsSync(join2(cwd, name)));
168
- if (hasConfigFile) {
169
- const { config, path } = await loadConfig(cwd, flags.configPath);
170
- if (flags.spec || flags.out) {
171
- console.warn(`Note: --spec/--out are ignored; ${basename(path)} is in charge here.`);
172
- }
173
- return { config, label: basename(path) };
174
- }
175
- if (flags.configPath) {
176
- throw new Error(`No config file at "${flags.configPath}".`);
177
- }
178
- const spec = flags.spec ?? await detectSpec(cwd);
179
- const outDir = flags.out ?? "./src/webmcp";
180
- return {
181
- config: { sources: [openapi({ spec })], generate: [js({ outDir })] },
182
- label: flags.spec ? `--spec ${spec}` : `detected ${spec}`
183
- };
184
- }
185
- async function detectSpec(cwd) {
186
- const specs = await findSpecs(cwd);
187
- if (specs.length === 0) {
188
- throw new Error(
189
- "No OpenAPI spec found in this project.\nPoint at one: npx webmcp-codegen generate --spec path/to/openapi.json"
190
- );
191
- }
192
- if (specs.length > 1) {
193
- const list = specs.map((spec) => ` - ${spec}`).join("\n");
194
- throw new Error(
195
- `Found ${specs.length} API specs:
196
- ${list}
197
-
198
- Pick one: npx webmcp-codegen generate --spec ${specs[0]}`
199
- );
200
- }
201
- console.log(`Detected ${specs[0]} (override with --spec)
202
- `);
203
- return specs[0];
204
- }
205
269
  async function runOnce(cwd, flags) {
206
- const { config, label } = await resolveConfig(cwd, flags);
207
- const result = await runGenerate(config, {
270
+ const setup = await resolveSetup(cwd, flags);
271
+ const data = await loadDataFile(cwd);
272
+ const result = await runGenerate(setup.config, {
208
273
  cwd,
209
274
  dryRun: flags.dryRun,
210
275
  skipAudit: flags.skipAudit,
211
- force: flags.force
276
+ force: flags.force,
277
+ overrides: data.overrides
212
278
  });
213
- printReport(result, flags, label, cwd);
279
+ let wiring = null;
280
+ if (!result.blocked && setup.app) {
281
+ const outDir = findOutDir(setup.config);
282
+ if (outDir) {
283
+ wiring = await planWiring(cwd, setup.app, outDir);
284
+ if (wiring && wiring.edits.length > 0 && !flags.dryRun && result.wrote) {
285
+ await applyWiring(wiring);
286
+ }
287
+ }
288
+ }
289
+ if (!setup.fromConfigFile && !flags.dryRun && result.wrote) {
290
+ await saveDataFile(cwd, setup.remember);
291
+ }
292
+ printReport(result, flags, setup, wiring, cwd);
214
293
  return result;
215
294
  }
216
- function printReport(result, flags, configName, cwd) {
217
- const { tools, findings, files, blocked } = result;
295
+ function findOutDir(config) {
296
+ return config.generate[0]?.outDir;
297
+ }
298
+ function printReport(result, flags, setup, wiring, cwd) {
299
+ const { tools, skipped, findings, files, notes, blocked } = result;
218
300
  console.log(`
219
- webmcp-codegen (${configName}): ${tools.length} tool(s)
220
- `);
221
- for (const tool of tools) {
222
- console.log(` ${tool.name} [${tool.sideEffect}] \u2190 ${tool.source.ref}`);
301
+ webmcp-codegen (${setup.label}): ${tools.length} tool(s)`);
302
+ for (const note of notes) {
303
+ console.log(`
304
+ note: ${note}`);
305
+ }
306
+ if (skipped.length > 0) {
307
+ console.log(`
308
+ ${skipped.length} endpoint(s) skipped:`);
309
+ for (const entry of skipped) {
310
+ console.log(` ${entry.ref}: ${entry.reason}`);
311
+ }
312
+ }
313
+ console.log("");
314
+ const listed = tools.slice(0, MAX_LISTED_TOOLS);
315
+ for (const tool of listed) {
316
+ const state = tool.enabledByDefault ? "" : " starts disabled";
317
+ console.log(` ${tool.name} [${tool.sideEffect}]${state} \u2190 ${tool.source.ref}`);
318
+ }
319
+ if (tools.length > listed.length) {
320
+ console.log(` \u2026and ${tools.length - listed.length} more`);
223
321
  }
224
322
  if (findings.length > 0) {
225
323
  console.log("");
@@ -237,23 +335,86 @@ webmcp-codegen (${configName}): ${tools.length} tool(s)
237
335
  console.log(` ${shown}: ${relative2(cwd, file.path)}`);
238
336
  }
239
337
  }
338
+ if (wiring) {
339
+ if (wiring.alreadyWired) {
340
+ console.log("\n registration: already wired into your app");
341
+ } else if (wiring.edits.length > 0) {
342
+ console.log(flags.dryRun ? "\n registration (would do):" : "\n registration:");
343
+ for (const edit of wiring.edits) {
344
+ console.log(` ${edit.summary}`);
345
+ }
346
+ if (!flags.dryRun) {
347
+ console.log(" undo: delete the added lines (nothing else was touched)");
348
+ }
349
+ }
350
+ } else if (!blocked && setup.app) {
351
+ console.log("\n registration: could not find your app's entry file, so add this by hand:");
352
+ console.log(' import { registerAllTools } from "<path-to>/src/webmcp";');
353
+ console.log(" void registerAllTools();");
354
+ }
240
355
  if (blocked) {
241
356
  console.log(
242
357
  "\nGeneration blocked by audit errors. Fix them, or re-run with --force to write anyway."
243
358
  );
244
- } else if (flags.dryRun) {
359
+ return;
360
+ }
361
+ if (flags.dryRun) {
245
362
  console.log("\nDry run: nothing written. Re-run without --dry-run to write these files.");
363
+ return;
364
+ }
365
+ printNextSteps(result);
366
+ }
367
+ function printNextSteps(result) {
368
+ const enabled = result.tools.filter((tool) => tool.enabledByDefault);
369
+ const disabled = result.tools.filter((tool) => !tool.enabledByDefault);
370
+ const parts = [];
371
+ if (enabled.length > 0) parts.push(`${enabled.length} read tool(s) work out of the box`);
372
+ if (disabled.length > 0) {
373
+ parts.push(`${disabled.length} tool(s) start disabled (open the file and uncomment to enable)`);
374
+ }
375
+ console.log(`
376
+ Done. ${parts.join("; ")}.`);
377
+ const suggestion = pickSuggestedTool(result.tools);
378
+ console.log("\nTry it:");
379
+ console.log(" 1. Start your app and open it in Chrome.");
380
+ console.log(" 2. Turn on chrome://flags/#enable-webmcp-testing and reload the page.");
381
+ if (suggestion) {
382
+ console.log(` 3. Ask the agent: "${suggestion}"`);
246
383
  } else {
247
- console.log("\nDone. Fill in each execute() below the marker, then registerAllTools().");
384
+ console.log(" 3. Ask the agent to use one of your tools.");
248
385
  }
249
386
  }
387
+ function pickSuggestedTool(tools) {
388
+ const reads = tools.filter((tool2) => tool2.enabledByDefault && tool2.endpointRole === "endpoint");
389
+ if (reads.length === 0) return void 0;
390
+ const tool = reads.find((candidate) => /^(list|get|search|find|fetch|recent)-/.test(candidate.name)) ?? reads[0];
391
+ if (!tool) return void 0;
392
+ const description = tool.description.trim().replace(/\.$/, "");
393
+ const looksTemplated = /^(GET|POST|PUT|PATCH|DELETE)\s/.test(description);
394
+ const phrase = looksTemplated ? tool.name.replace(/-/g, " ") : description.charAt(0).toLowerCase() + description.slice(1);
395
+ return phrase;
396
+ }
397
+ async function dev(port) {
398
+ const { startDevServer } = await import("./server-OR4IMRQ5.js");
399
+ const server = await startDevServer({ cwd: process.cwd(), port });
400
+ console.log(`
401
+ webmcp-codegen dashboard: http://localhost:${port}`);
402
+ console.log("List, describe, enable, and try your tools. Ctrl+C to stop.\n");
403
+ await new Promise((resolveExit) => {
404
+ process.on("SIGINT", () => {
405
+ server.close();
406
+ resolveExit();
407
+ });
408
+ });
409
+ return 0;
410
+ }
250
411
  async function watchLoop(cwd, flags) {
251
412
  await runOnce(cwd, { ...flags, dryRun: false });
252
413
  console.log("\nWatching for changes\u2026 (Ctrl+C to stop)");
253
414
  let timer;
254
415
  watch(cwd, { recursive: true }, (_event, filename) => {
255
416
  if (!filename) return;
256
- if (/node_modules|\.git|\/dist|\/src\/webmcp/.test(filename)) return;
417
+ if (/node_modules|\.git|\/dist|\/src\/webmcp|\.webmcp-codegen\.json/.test(filename)) return;
257
418
  if (!/\.(ya?ml|json|ts|tsx|mts|mjs)$/.test(filename)) return;
258
419
  clearTimeout(timer);
259
420
  timer = setTimeout(() => {
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/cli.ts","../src/detect.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * The webmcp-codegen CLI.\n *\n * The path we optimize for is the zero-everything first run:\n *\n * npx webmcp-codegen generate\n *\n * No install, no config file, no flags. The CLI detects your API spec and\n * generates into ./src/webmcp. When you outgrow the defaults:\n *\n * --spec/--out quick overrides without a config file\n * init writes codegen.config.mjs for full control (needs the\n * package installed, since the config imports from it)\n *\n * Plus the flags you'd expect on a codegen tool: --dry-run to preview,\n * --watch to re-run on change, --skip-audit to bypass the safety report,\n * --force to write through audit errors, --config to point at a config\n * file somewhere else.\n */\n\nimport { existsSync, watch } from \"node:fs\";\nimport { writeFile } from \"node:fs/promises\";\nimport { basename, join, relative } from \"node:path\";\nimport { parseArgs } from \"node:util\";\nimport { CONFIG_FILE_NAMES, loadConfig } from \"./config.js\";\nimport { findSpecs } from \"./detect.js\";\nimport { js } from \"./generators/js.js\";\nimport { type GenerateResult, runGenerate } from \"./pipeline.js\";\nimport { openapi } from \"./sources/openapi.js\";\nimport type { CodegenConfig } from \"./types.js\";\n\nconst HELP = `webmcp-codegen: generate WebMCP tools from the API contracts you already have\n\nFastest start (no install, no config):\n npx webmcp-codegen generate --dry-run Detect your spec, preview the tools\n npx webmcp-codegen generate Write the tool files\n\nCommands:\n init Write a codegen.config.mjs for full control\n generate Generate (or update) your WebMCP tools\n generate --watch Re-generate when files change\n\nFlags for generate:\n --spec PATH Which OpenAPI spec to use (auto-detected when omitted)\n --out DIR Where the tool files go (default: ./src/webmcp)\n --dry-run Preview what would be written, write nothing\n --skip-audit Skip the safety report\n --force Write files even when the audit reports errors\n --config PATH Use a config file at PATH\n`;\n\nconst CONFIG_FILE = \"codegen.config.mjs\";\n\nasync function main(): Promise<number> {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n options: {\n \"dry-run\": { type: \"boolean\", default: false },\n \"skip-audit\": { type: \"boolean\", default: false },\n force: { type: \"boolean\", default: false },\n watch: { type: \"boolean\", default: false },\n config: { type: \"string\" },\n spec: { type: \"string\" },\n out: { type: \"string\" },\n help: { type: \"boolean\", default: false },\n },\n });\n\n const command = positionals[0];\n if (values.help || !command) {\n console.log(HELP);\n return 0;\n }\n\n switch (command) {\n case \"init\":\n return init();\n case \"generate\":\n return generate({\n dryRun: values[\"dry-run\"],\n skipAudit: values[\"skip-audit\"],\n force: values.force,\n watch: values.watch,\n configPath: values.config,\n spec: values.spec,\n out: values.out,\n });\n default:\n console.error(`Unknown command \"${command}\".\\n`);\n console.log(HELP);\n return 1;\n }\n}\n\n/** Detect the project's API spec and write a starter config. */\nasync function init(): Promise<number> {\n const cwd = process.cwd();\n const configPath = join(cwd, CONFIG_FILE);\n\n if (existsSync(configPath)) {\n console.error(`${CONFIG_FILE} already exists. Nothing to do.`);\n return 1;\n }\n\n const specs = await findSpecs(cwd);\n const specPath = specs.length > 0 ? `./${specs[0]}` : \"./openapi.yaml\";\n\n await writeFile(\n configPath,\n `import { defineConfig } from \"webmcp-codegen\";\nimport { openapi } from \"webmcp-codegen/sources\";\nimport { js } from \"webmcp-codegen/generators\";\n\nexport default defineConfig({\n sources: [openapi({ spec: \"${specPath}\" })],\n generate: [js({ outDir: \"./src/webmcp\" })],\n safety: {\n // Extra field names to treat as PII, on top of the built-in list:\n // piiFields: [\"internalId\"],\n // Tools to skip entirely (matched against name and route):\n // exclude: [\"internal\"],\n },\n});\n`,\n );\n\n // The config imports from the package, so keeping it means installing it.\n console.log(\"Installed the package? A config file needs it:\");\n console.log(\" npm install -D webmcp-codegen\\n\");\n if (specs.length > 0) {\n console.log(`Found ${specs[0]}. Wrote ${CONFIG_FILE}.`);\n console.log(\"\\nNext: npx webmcp-codegen generate --dry-run\");\n } else {\n console.log(`No OpenAPI spec found, so ${CONFIG_FILE} points at ./openapi.yaml.`);\n console.log(\"Edit the `spec` path to point at your spec, then run:\");\n console.log(\"\\n npx webmcp-codegen generate --dry-run\");\n }\n return 0;\n}\n\ninterface GenerateFlags {\n dryRun: boolean;\n skipAudit: boolean;\n force: boolean;\n watch: boolean;\n configPath?: string;\n spec?: string;\n out?: string;\n}\n\nasync function generate(flags: GenerateFlags): Promise<number> {\n const cwd = process.cwd();\n\n if (flags.watch) {\n // Watch mode never exits; it re-runs generate on every relevant change.\n await watchLoop(cwd, flags);\n return 0;\n }\n\n const result = await runOnce(cwd, flags);\n return result.blocked ? 1 : 0;\n}\n\n/**\n * Where the tools come from, in priority order:\n *\n * 1. a config file (codegen.config.mjs or --config) for full control\n * 2. --spec/--out flags as quick overrides, no config needed\n * 3. auto-detection, on the zero-argument npx run\n *\n * Branches 2 and 3 build the config right here inside the CLI, which is\n * what makes `npx webmcp-codegen generate` work without installing the\n * package: the user's project never has to resolve a webmcp-codegen import.\n */\nasync function resolveConfig(\n cwd: string,\n flags: GenerateFlags,\n): Promise<{ config: CodegenConfig; label: string }> {\n const hasConfigFile = flags.configPath\n ? existsSync(join(cwd, flags.configPath))\n : CONFIG_FILE_NAMES.some((name) => existsSync(join(cwd, name)));\n\n if (hasConfigFile) {\n const { config, path } = await loadConfig(cwd, flags.configPath);\n if (flags.spec || flags.out) {\n console.warn(`Note: --spec/--out are ignored; ${basename(path)} is in charge here.`);\n }\n return { config, label: basename(path) };\n }\n if (flags.configPath) {\n throw new Error(`No config file at \"${flags.configPath}\".`);\n }\n\n const spec = flags.spec ?? (await detectSpec(cwd));\n const outDir = flags.out ?? \"./src/webmcp\";\n return {\n config: { sources: [openapi({ spec })], generate: [js({ outDir })] },\n label: flags.spec ? `--spec ${spec}` : `detected ${spec}`,\n };\n}\n\n/**\n * Find the project's API spec. One candidate: use it and say so. Several:\n * list them and make the human pick. None: say exactly what to do next.\n */\nasync function detectSpec(cwd: string): Promise<string> {\n const specs = await findSpecs(cwd);\n\n if (specs.length === 0) {\n throw new Error(\n \"No OpenAPI spec found in this project.\\n\" +\n \"Point at one: npx webmcp-codegen generate --spec path/to/openapi.json\",\n );\n }\n if (specs.length > 1) {\n const list = specs.map((spec) => ` - ${spec}`).join(\"\\n\");\n throw new Error(\n `Found ${specs.length} API specs:\\n${list}\\n\\n` +\n `Pick one: npx webmcp-codegen generate --spec ${specs[0]}`,\n );\n }\n\n console.log(`Detected ${specs[0]} (override with --spec)\\n`);\n return specs[0] as string;\n}\n\n/** One generate pass: resolve the config, run the pipeline, print the report. */\nasync function runOnce(cwd: string, flags: GenerateFlags): Promise<GenerateResult> {\n const { config, label } = await resolveConfig(cwd, flags);\n const result = await runGenerate(config, {\n cwd,\n dryRun: flags.dryRun,\n skipAudit: flags.skipAudit,\n force: flags.force,\n });\n printReport(result, flags, label, cwd);\n return result;\n}\n\n/**\n * The report is the product's voice: plain language, one line per file,\n * findings grouped by severity, and a summary that says what to do next.\n */\nfunction printReport(\n result: GenerateResult,\n flags: GenerateFlags,\n configName: string,\n cwd: string,\n): void {\n const { tools, findings, files, blocked } = result;\n\n console.log(`\\nwebmcp-codegen (${configName}): ${tools.length} tool(s)\\n`);\n\n for (const tool of tools) {\n console.log(` ${tool.name} [${tool.sideEffect}] ← ${tool.source.ref}`);\n }\n\n if (findings.length > 0) {\n console.log(\"\");\n for (const finding of findings) {\n const icon = finding.level === \"error\" ? \"✖\" : \"⚠\";\n const where = finding.tool ? ` (${finding.tool})` : \"\";\n console.log(` ${icon} ${finding.message}${where}`);\n }\n }\n\n if (files.length > 0) {\n console.log(\"\");\n for (const file of files) {\n if (file.action === \"unchanged\" && !file.conflict) continue;\n const shown = file.conflict\n ? `conflict → wrote ${relative(cwd, file.conflict)}`\n : file.action;\n console.log(` ${shown}: ${relative(cwd, file.path)}`);\n }\n }\n\n if (blocked) {\n console.log(\n \"\\nGeneration blocked by audit errors. Fix them, or re-run with --force to write anyway.\",\n );\n } else if (flags.dryRun) {\n console.log(\"\\nDry run: nothing written. Re-run without --dry-run to write these files.\");\n } else {\n console.log(\"\\nDone. Fill in each execute() below the marker, then registerAllTools().\");\n }\n}\n\n/**\n * Re-run generate when anything relevant changes. Node's recursive watcher\n * covers Linux/macOS/Windows on Node 20+, which is our engine floor anyway.\n */\nasync function watchLoop(cwd: string, flags: GenerateFlags): Promise<void> {\n await runOnce(cwd, { ...flags, dryRun: false });\n console.log(\"\\nWatching for changes… (Ctrl+C to stop)\");\n\n let timer: NodeJS.Timeout | undefined;\n watch(cwd, { recursive: true }, (_event, filename) => {\n if (!filename) return;\n // Only source-ish changes are worth regenerating for.\n if (/node_modules|\\.git|\\/dist|\\/src\\/webmcp/.test(filename)) return;\n if (!/\\.(ya?ml|json|ts|tsx|mts|mjs)$/.test(filename)) return;\n clearTimeout(timer);\n timer = setTimeout(() => {\n runOnce(cwd, { ...flags, dryRun: false }).catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : error);\n });\n }, 300);\n });\n}\n\nmain()\n .then((code) => process.exit(code))\n .catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : error);\n process.exit(1);\n });\n","/**\n * Spec auto-detection: the reason `npx webmcp-codegen generate` works with\n * zero arguments, zero config, and zero install.\n *\n * The rule is deliberately boring: walk the project (skipping the obvious\n * noise), recognize the usual spec filenames, and return what we find\n * shallowest-first. When exactly one spec exists we just use it; the CLI\n * layer decides what to do about zero or several.\n */\n\nimport type { Dirent } from \"node:fs\";\nimport { readdir } from \"node:fs/promises\";\nimport { join, relative } from \"node:path\";\n\n/** Filenames we recognize as API specs. */\nexport const SPEC_FILE_PATTERN = /^(openapi|swagger|api)\\.(ya?ml|json)$/i;\n\n/** Directories never worth descending into. */\nconst IGNORED_DIRS = new Set([\n \"node_modules\",\n \".git\",\n \".turbo\",\n \".next\",\n \"dist\",\n \"build\",\n \"coverage\",\n]);\n\n/**\n * How deep we look. Enough for monorepo layouts like\n * apps/server/openapi/openapi.json (depth 3) without wandering forever.\n */\nconst MAX_DEPTH = 5;\n\n/**\n * Find API spec files under `cwd`, returned as paths relative to `cwd`,\n * shallowest first. A root-level spec is a likelier intent than one\n * buried six folders deep.\n */\nexport async function findSpecs(cwd: string): Promise<string[]> {\n const found: { path: string; depth: number }[] = [];\n\n async function walk(dir: string, depth: number): Promise<void> {\n if (depth > MAX_DEPTH) return;\n let entries: Dirent[];\n try {\n entries = await readdir(dir, { withFileTypes: true });\n } catch {\n return; // Unreadable directory. Skip it, never die on detection.\n }\n for (const entry of entries) {\n if (entry.isDirectory()) {\n if (!IGNORED_DIRS.has(entry.name)) await walk(join(dir, entry.name), depth + 1);\n } else if (SPEC_FILE_PATTERN.test(entry.name)) {\n found.push({ path: join(dir, entry.name), depth });\n }\n }\n }\n\n await walk(cwd, 0);\n return found.sort((a, b) => a.depth - b.depth).map((entry) => relative(cwd, entry.path));\n}\n"],"mappings":"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","names":["join","relative","join","relative"]}
1
+ {"version":3,"sources":["../src/cli.ts","../src/wire.ts"],"sourcesContent":["#!/usr/bin/env node\n\n/**\n * The webmcp-codegen CLI.\n *\n * The path we optimize for is the zero-everything first run:\n *\n * npx webmcp-codegen generate\n *\n * No install, no config file, no flags. The CLI finds your API spec, finds\n * the package that is your web app, writes working tools into it, wires the\n * registration into your app's entry file, and tells you how to see it all\n * working. Choices we had to ask for are remembered in .webmcp-codegen.json\n * so we never ask twice.\n *\n * When you outgrow the defaults:\n *\n * --spec/--out quick overrides without a config file\n * init writes codegen.config.mjs for full control (needs the\n * package installed, since the config imports from it)\n *\n * Plus the flags you'd expect on a codegen tool: --dry-run to preview,\n * --watch to re-run on change, --skip-audit to bypass the safety report,\n * --force to write through audit errors, --config to point at a config\n * file somewhere else.\n */\n\nimport { existsSync, watch } from \"node:fs\";\nimport { writeFile } from \"node:fs/promises\";\nimport { basename, join, relative } from \"node:path\";\nimport { createInterface } from \"node:readline/promises\";\nimport { parseArgs } from \"node:util\";\nimport { CONFIG_FILE_NAMES, loadConfig } from \"./config.js\";\nimport { loadDataFile, saveDataFile } from \"./data-file.js\";\nimport { findSpecs } from \"./detect.js\";\nimport { js } from \"./generators/js.js\";\nimport { type GenerateResult, runGenerate } from \"./pipeline.js\";\nimport { type GenerateFlags, resolveSetup, type Setup } from \"./setup.js\";\nimport { openapi } from \"./sources/openapi.js\";\nimport type { CodegenConfig, ReviewedTool } from \"./types.js\";\nimport { applyWiring, planWiring, type WirePlan } from \"./wire.js\";\n\nconst HELP = `webmcp-codegen: generate WebMCP tools from the API contracts you already have\n\nFastest start (no install, no config):\n npx webmcp-codegen generate --dry-run Detect your spec, preview the tools\n npx webmcp-codegen generate Write the tool files, wire them up\n\nCommands:\n init Write a codegen.config.mjs for full control\n generate Generate (or update) your WebMCP tools\n generate --watch Re-generate when files change\n dev Open the tools dashboard (list, edit, try tools)\n\nFlags for generate:\n --spec PATH Which OpenAPI spec to use (auto-detected when omitted)\n --out DIR Where the tool files go (default: your web app's src/webmcp)\n --dry-run Preview what would be written, write nothing\n --skip-audit Skip the safety report\n --force Write files even when the audit reports errors\n --config PATH Use a config file at PATH\n\nFlags for dev:\n --port N Dashboard port (default: 4700)\n`;\n\nconst CONFIG_FILE = \"codegen.config.mjs\";\n\n/** How many tools to list before folding the rest into a count. */\nconst MAX_LISTED_TOOLS = 15;\n\nasync function main(): Promise<number> {\n const { positionals, values } = parseArgs({\n allowPositionals: true,\n options: {\n \"dry-run\": { type: \"boolean\", default: false },\n \"skip-audit\": { type: \"boolean\", default: false },\n force: { type: \"boolean\", default: false },\n watch: { type: \"boolean\", default: false },\n config: { type: \"string\" },\n spec: { type: \"string\" },\n out: { type: \"string\" },\n port: { type: \"string\" },\n help: { type: \"boolean\", default: false },\n },\n });\n\n const command = positionals[0];\n if (values.help || !command) {\n console.log(HELP);\n return 0;\n }\n\n switch (command) {\n case \"init\":\n return init();\n case \"dev\":\n return dev(Number.parseInt(values.port ?? \"4700\", 10));\n case \"generate\":\n return generate({\n dryRun: values[\"dry-run\"],\n skipAudit: values[\"skip-audit\"],\n force: values.force,\n watch: values.watch,\n configPath: values.config,\n spec: values.spec,\n out: values.out,\n });\n default:\n console.error(`Unknown command \"${command}\".\\n`);\n console.log(HELP);\n return 1;\n }\n}\n\n/** Detect the project's API spec and write a starter config. */\nasync function init(): Promise<number> {\n const cwd = process.cwd();\n const configPath = join(cwd, CONFIG_FILE);\n\n if (existsSync(configPath)) {\n console.error(`${CONFIG_FILE} already exists. Nothing to do.`);\n return 1;\n }\n\n const specs = await findSpecs(cwd);\n const specPath = specs.length > 0 ? `./${specs[0]}` : \"./openapi.yaml\";\n\n await writeFile(\n configPath,\n `import { defineConfig } from \"webmcp-codegen\";\nimport { openapi } from \"webmcp-codegen/sources\";\nimport { js } from \"webmcp-codegen/generators\";\n\nexport default defineConfig({\n sources: [openapi({ spec: \"${specPath}\" })],\n generate: [js({ outDir: \"./src/webmcp\" })],\n safety: {\n // Extra field names to treat as PII, on top of the built-in list:\n // piiFields: [\"internalId\"],\n // Tools to skip entirely (matched against name and route):\n // exclude: [\"internal\"],\n },\n});\n`,\n );\n\n // The config imports from the package, so keeping it means installing it.\n console.log(\"Installed the package? A config file needs it:\");\n console.log(\" npm install -D webmcp-codegen\\n\");\n if (specs.length > 0) {\n console.log(`Found ${specs[0]}. Wrote ${CONFIG_FILE}.`);\n console.log(\"\\nNext: npx webmcp-codegen generate --dry-run\");\n } else {\n console.log(`No OpenAPI spec found, so ${CONFIG_FILE} points at ./openapi.yaml.`);\n console.log(\"Edit the `spec` path to point at your spec, then run:\");\n console.log(\"\\n npx webmcp-codegen generate --dry-run\");\n }\n return 0;\n}\n\nasync function generate(flags: GenerateFlags): Promise<number> {\n const cwd = process.cwd();\n\n if (flags.watch) {\n // Watch mode never exits; it re-runs generate on every relevant change.\n // Wiring is idempotent, so it is part of every pass and quietly no-ops\n // after the first.\n await watchLoop(cwd, flags);\n return 0;\n }\n\n const result = await runOnce(cwd, flags);\n return result.blocked ? 1 : 0;\n}\n\n/** One generate pass: resolve setup, run the pipeline, wire, report. */\nasync function runOnce(cwd: string, flags: GenerateFlags): Promise<GenerateResult> {\n const setup = await resolveSetup(cwd, flags);\n const data = await loadDataFile(cwd);\n const result = await runGenerate(setup.config, {\n cwd,\n dryRun: flags.dryRun,\n skipAudit: flags.skipAudit,\n force: flags.force,\n overrides: data.overrides,\n });\n\n // Registration wiring: additive, idempotent, and only for real runs.\n let wiring: WirePlan | null = null;\n if (!result.blocked && setup.app) {\n const outDir = findOutDir(setup.config);\n if (outDir) {\n wiring = await planWiring(cwd, setup.app, outDir);\n if (wiring && wiring.edits.length > 0 && !flags.dryRun && result.wrote) {\n await applyWiring(wiring);\n }\n }\n }\n\n // Remember the choices detection made, so the next run never re-asks.\n if (!setup.fromConfigFile && !flags.dryRun && result.wrote) {\n await saveDataFile(cwd, setup.remember);\n }\n\n printReport(result, flags, setup, wiring, cwd);\n return result;\n}\n\n/** Pull the outDir back out of the resolved config (there is one generator). */\nfunction findOutDir(config: CodegenConfig): string | undefined {\n return config.generate[0]?.outDir;\n}\n\n/**\n * The report is the product's voice: plain language, no jargon, one line per\n * file, findings grouped by severity, and a summary that says what happens\n * next — including the one command's worth of \"try it\" at the end.\n */\nfunction printReport(\n result: GenerateResult,\n flags: GenerateFlags,\n setup: Setup,\n wiring: WirePlan | null,\n cwd: string,\n): void {\n const { tools, skipped, findings, files, notes, blocked } = result;\n\n console.log(`\\nwebmcp-codegen (${setup.label}): ${tools.length} tool(s)`);\n\n for (const note of notes) {\n console.log(`\\n note: ${note}`);\n }\n\n if (skipped.length > 0) {\n console.log(`\\n ${skipped.length} endpoint(s) skipped:`);\n for (const entry of skipped) {\n console.log(` ${entry.ref}: ${entry.reason}`);\n }\n }\n\n console.log(\"\");\n const listed = tools.slice(0, MAX_LISTED_TOOLS);\n for (const tool of listed) {\n const state = tool.enabledByDefault ? \"\" : \" starts disabled\";\n console.log(` ${tool.name} [${tool.sideEffect}]${state} ← ${tool.source.ref}`);\n }\n if (tools.length > listed.length) {\n console.log(` …and ${tools.length - listed.length} more`);\n }\n\n if (findings.length > 0) {\n console.log(\"\");\n for (const finding of findings) {\n const icon = finding.level === \"error\" ? \"✖\" : \"⚠\";\n const where = finding.tool ? ` (${finding.tool})` : \"\";\n console.log(` ${icon} ${finding.message}${where}`);\n }\n }\n\n if (files.length > 0) {\n console.log(\"\");\n for (const file of files) {\n if (file.action === \"unchanged\" && !file.conflict) continue;\n const shown = file.conflict\n ? `conflict → wrote ${relative(cwd, file.conflict)}`\n : file.action;\n console.log(` ${shown}: ${relative(cwd, file.path)}`);\n }\n }\n\n if (wiring) {\n if (wiring.alreadyWired) {\n console.log(\"\\n registration: already wired into your app\");\n } else if (wiring.edits.length > 0) {\n console.log(flags.dryRun ? \"\\n registration (would do):\" : \"\\n registration:\");\n for (const edit of wiring.edits) {\n console.log(` ${edit.summary}`);\n }\n if (!flags.dryRun) {\n console.log(\" undo: delete the added lines (nothing else was touched)\");\n }\n }\n } else if (!blocked && setup.app) {\n console.log(\"\\n registration: could not find your app's entry file, so add this by hand:\");\n console.log(' import { registerAllTools } from \"<path-to>/src/webmcp\";');\n console.log(\" void registerAllTools();\");\n }\n\n if (blocked) {\n console.log(\n \"\\nGeneration blocked by audit errors. Fix them, or re-run with --force to write anyway.\",\n );\n return;\n }\n if (flags.dryRun) {\n console.log(\"\\nDry run: nothing written. Re-run without --dry-run to write these files.\");\n return;\n }\n\n printNextSteps(result);\n}\n\n/**\n * The parting message. The read tools already work; the mutations are one\n * uncomment away; and we end with the single most convincing thing a new\n * user can do: watch an agent call their app.\n */\nfunction printNextSteps(result: GenerateResult): void {\n const enabled = result.tools.filter((tool) => tool.enabledByDefault);\n const disabled = result.tools.filter((tool) => !tool.enabledByDefault);\n\n const parts: string[] = [];\n if (enabled.length > 0) parts.push(`${enabled.length} read tool(s) work out of the box`);\n if (disabled.length > 0) {\n parts.push(`${disabled.length} tool(s) start disabled (open the file and uncomment to enable)`);\n }\n console.log(`\\nDone. ${parts.join(\"; \")}.`);\n\n const suggestion = pickSuggestedTool(result.tools);\n console.log(\"\\nTry it:\");\n console.log(\" 1. Start your app and open it in Chrome.\");\n console.log(\" 2. Turn on chrome://flags/#enable-webmcp-testing and reload the page.\");\n if (suggestion) {\n console.log(` 3. Ask the agent: \"${suggestion}\"`);\n } else {\n console.log(\" 3. Ask the agent to use one of your tools.\");\n }\n}\n\n/**\n * The example request in the \"try it\" line. Pick an enabled read tool —\n * preferably one whose name sounds like listing or looking something up —\n * and phrase it the way a user would say it, from the spec's own description.\n */\nfunction pickSuggestedTool(tools: ReviewedTool[]): string | undefined {\n const reads = tools.filter((tool) => tool.enabledByDefault && tool.endpointRole === \"endpoint\");\n if (reads.length === 0) return undefined;\n const tool =\n reads.find((candidate) => /^(list|get|search|find|fetch|recent)-/.test(candidate.name)) ??\n reads[0];\n if (!tool) return undefined;\n\n const description = tool.description.trim().replace(/\\.$/, \"\");\n // A template description (\"GET /v1/trips\") would read as jargon; fall back\n // to the tool name in words (\"list trips\").\n const looksTemplated = /^(GET|POST|PUT|PATCH|DELETE)\\s/.test(description);\n const phrase = looksTemplated\n ? tool.name.replace(/-/g, \" \")\n : description.charAt(0).toLowerCase() + description.slice(1);\n return phrase;\n}\n\n/**\n * The tools dashboard: a local control panel for what was generated.\n * Runs until Ctrl+C; nothing is written to the app, ever.\n */\nasync function dev(port: number): Promise<number> {\n const { startDevServer } = await import(\"./dev/server.js\");\n const server = await startDevServer({ cwd: process.cwd(), port });\n console.log(`\\nwebmcp-codegen dashboard: http://localhost:${port}`);\n console.log(\"List, describe, enable, and try your tools. Ctrl+C to stop.\\n\");\n\n await new Promise<void>((resolveExit) => {\n process.on(\"SIGINT\", () => {\n server.close();\n resolveExit();\n });\n });\n return 0;\n}\n\n/**\n * Re-run generate when anything relevant changes. Node's recursive watcher\n * covers Linux/macOS/Windows on Node 20+, which is our engine floor anyway.\n */\nasync function watchLoop(cwd: string, flags: GenerateFlags): Promise<void> {\n await runOnce(cwd, { ...flags, dryRun: false });\n console.log(\"\\nWatching for changes… (Ctrl+C to stop)\");\n\n let timer: NodeJS.Timeout | undefined;\n watch(cwd, { recursive: true }, (_event, filename) => {\n if (!filename) return;\n // Only source-ish changes are worth regenerating for. Never react to our\n // own outputs (generated files, the data file) or watch mode loops.\n if (/node_modules|\\.git|\\/dist|\\/src\\/webmcp|\\.webmcp-codegen\\.json/.test(filename)) return;\n if (!/\\.(ya?ml|json|ts|tsx|mts|mjs)$/.test(filename)) return;\n clearTimeout(timer);\n timer = setTimeout(() => {\n runOnce(cwd, { ...flags, dryRun: false }).catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : error);\n });\n }, 300);\n });\n}\n\nmain()\n .then((code) => process.exit(code))\n .catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : error);\n process.exit(1);\n });\n","/**\n * Registration wiring: making the app actually register its tools.\n *\n * Generated files do nothing until something calls registerAllTools() once at\n * startup. Rather than telling the developer to go do that, we do it for\n * them — under strict rules, because this is the one place we edit *their*\n * files instead of ours:\n *\n * 1. Edits are additive only. We insert lines; we never change or remove\n * existing ones.\n * 2. Idempotent. If the wiring is already there, we do nothing.\n * 3. Honest. Every edit is reported with exact paths and how to undo it.\n * 4. When we cannot find the entry point with confidence, we do not guess:\n * we print the two lines and where they go, and leave it to the human.\n */\n\nimport { readFile, writeFile } from \"node:fs/promises\";\nimport { dirname, join, relative } from \"node:path\";\nimport type { WebApp } from \"./detect-app.js\";\n\nexport interface WireEdit {\n /** Absolute path of the file to create or modify. */\n path: string;\n action: \"create\" | \"modify\";\n /** The full new contents (for modify: the existing contents plus our lines). */\n contents: string;\n /** One human sentence per edit, for the report. */\n summary: string;\n}\n\nexport interface WirePlan {\n edits: WireEdit[];\n /** Set when we already found the wiring in place. */\n alreadyWired?: boolean;\n}\n\n/**\n * Compute the wiring edits for the detected app, or null when we cannot\n * locate the entry point with confidence (the CLI then prints manual\n * instructions instead).\n */\nexport async function planWiring(\n cwd: string,\n app: WebApp,\n outDir: string,\n): Promise<WirePlan | null> {\n switch (app.framework) {\n case \"next\":\n return planNextWiring(cwd, app, outDir);\n case \"vite-react\":\n return planViteWiring(cwd, app, outDir);\n default:\n // Nuxt/SvelteKit/unknown: we know where the tools go but not where the\n // app boots. Print instructions rather than guess-edit an entry file.\n return null;\n }\n}\n\n/** Apply a plan. Kept trivial on purpose: the plan already did the thinking. */\nexport async function applyWiring(plan: WirePlan): Promise<void> {\n for (const edit of plan.edits) {\n await writeFile(edit.path, edit.contents, \"utf8\");\n }\n}\n\n/* ── Next.js (app router) ──────────────────────────────────────────────── */\n\n/**\n * Next needs the registration to run on the client, so we generate a tiny\n * \"use client\" component next to the tools and mount it in the root layout:\n *\n * import { WebMCPRegister } from \"../webmcp/register\"; ← added\n * ...\n * <body>\n * <WebMCPRegister /> ← added\n * {children}\n */\nasync function planNextWiring(cwd: string, app: WebApp, outDir: string): Promise<WirePlan | null> {\n const layoutCandidates = [\n join(cwd, app.dir, \"src/app/layout.tsx\"),\n join(cwd, app.dir, \"src/app/layout.jsx\"),\n join(cwd, app.dir, \"app/layout.tsx\"),\n join(cwd, app.dir, \"app/layout.jsx\"),\n ];\n const layoutPath = await firstExisting(layoutCandidates);\n if (!layoutPath) return null;\n\n const registerPath = join(cwd, outDir, \"register.tsx\");\n const layout = await readFile(layoutPath, \"utf8\");\n if (layout.includes(\"WebMCPRegister\")) return { edits: [], alreadyWired: true };\n\n // Import path from the layout's directory to the register component.\n const importPath = withoutExtension(relative(dirname(layoutPath), registerPath));\n\n const edits: WireEdit[] = [\n {\n path: registerPath,\n action: \"create\",\n contents: nextRegisterComponent(),\n summary: `created ${relative(cwd, registerPath)} (a client component that registers your tools on page load)`,\n },\n ];\n\n const withImport = insertAfterLastImport(\n layout,\n `import { WebMCPRegister } from \"${importPath}\";`,\n );\n if (!withImport) return null;\n // Mount right after <body ...>, each element on its own line.\n const withComponent = withImport.replace(\n /<body([^>]*)>\\s*/,\n \"<body$1>\\n <WebMCPRegister />\\n \",\n );\n if (withComponent === withImport) return null; // No <body> tag found; do not guess.\n\n edits.push({\n path: layoutPath,\n action: \"modify\",\n contents: withComponent,\n summary: `added 2 lines to ${relative(cwd, layoutPath)} (an import and <WebMCPRegister /> inside <body>)`,\n });\n return { edits };\n}\n\nfunction nextRegisterComponent(): string {\n return `\"use client\";\n\nimport { useEffect } from \"react\";\nimport { registerAllTools } from \"./index\";\n\n/**\n * Registers the generated WebMCP tools once, on page load.\n * Generated by webmcp-codegen. Safe to move; keep it mounted near the root.\n */\nexport function WebMCPRegister() {\n useEffect(() => {\n void registerAllTools();\n }, []);\n return null;\n}\n`;\n}\n\n/* ── Vite + React (SPAs) ───────────────────────────────────────────────── */\n\n/**\n * A Vite app boots in main.tsx, so wiring is two added lines there:\n *\n * import { registerAllTools } from \"./webmcp\"; ← added\n * void registerAllTools(); ← added\n */\nasync function planViteWiring(cwd: string, app: WebApp, outDir: string): Promise<WirePlan | null> {\n const entryCandidates = [\n join(cwd, app.dir, \"src/main.tsx\"),\n join(cwd, app.dir, \"src/main.jsx\"),\n join(cwd, app.dir, \"src/index.tsx\"),\n join(cwd, app.dir, \"src/index.jsx\"),\n ];\n const entryPath = await firstExisting(entryCandidates);\n if (!entryPath) return null;\n\n const entry = await readFile(entryPath, \"utf8\");\n if (entry.includes(\"registerAllTools\")) return { edits: [], alreadyWired: true };\n\n const importPath = withoutExtension(relative(dirname(entryPath), join(cwd, outDir, \"index\")));\n const withWiring = insertAfterLastImport(\n entry,\n `import { registerAllTools } from \"${importPath}\";\\n\\nvoid registerAllTools();`,\n );\n if (!withWiring) return null;\n\n return {\n edits: [\n {\n path: entryPath,\n action: \"modify\",\n contents: withWiring,\n summary: `added 2 lines to ${relative(cwd, entryPath)} (an import and a registerAllTools() call)`,\n },\n ],\n };\n}\n\n/* ── Shared helpers ────────────────────────────────────────────────────── */\n\n/** Insert a line after the file's last top-level import statement. */\nfunction insertAfterLastImport(source: string, line: string): string | null {\n const lines = source.split(\"\\n\");\n let lastImport = -1;\n for (let index = 0; index < lines.length; index += 1) {\n if (/^import\\s/.test(lines[index] as string)) lastImport = index;\n }\n if (lastImport === -1) return null;\n lines.splice(lastImport + 1, 0, line);\n return lines.join(\"\\n\");\n}\n\n/**\n * Turn a filesystem path into a JS import specifier: no extension, and an\n * explicit \"./\" when the target is in the same directory or deeper —\n * `relative()` alone yields \"webmcp/index\", which JS would read as a\n * package name, not a file.\n */\nfunction withoutExtension(path: string): string {\n const bare = path.replace(/\\.(tsx?|jsx?)$/, \"\").replace(/\\/index$/, \"\");\n return bare.startsWith(\".\") ? bare : `./${bare}`;\n}\n\nasync function firstExisting(paths: string[]): Promise<string | undefined> {\n for (const path of paths) {\n try {\n await readFile(path, \"utf8\");\n return path;\n } catch {\n // Try the next candidate.\n }\n }\n return undefined;\n}\n"],"mappings":"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","names":["writeFile","join","relative","join","writeFile","relative","tool"]}
@@ -1,4 +1,4 @@
1
- import { T as ToolGenerator } from '../types-DRCQtX0w.js';
1
+ import { T as ToolGenerator } from '../types-DWUum51l.js';
2
2
 
3
3
  /**
4
4
  * The `js` generator, named after what lands in your repo: plain JavaScript/
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  js
3
- } from "../chunk-OILNQ2HE.js";
3
+ } from "../chunk-TGOJ3HUE.js";
4
4
  import "../chunk-KSQMJERY.js";
5
5
  export {
6
6
  js
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { C as CodegenConfig, R as ReviewedTool, A as AuditFinding, G as GeneratedFile } from './types-DRCQtX0w.js';
2
- export { a as CandidateTool, J as JsonSchema, b as RiskTier, c as SafetyOptions, d as SideEffect, S as Source, e as SourceKind, T as ToolGenerator, f as ToolHints } from './types-DRCQtX0w.js';
1
+ import { C as CodegenConfig, a as ToolOverrides, R as ReviewedTool, b as SkippedEndpoint, A as AuditFinding, G as GeneratedFile } from './types-DWUum51l.js';
2
+ export { c as CandidateTool, J as JsonSchema, d as RiskTier, e as SafetyOptions, f as SideEffect, S as Source, g as SourceKind, T as ToolGenerator, h as ToolHints } from './types-DWUum51l.js';
3
3
 
4
4
  /**
5
5
  * Config: `defineConfig` for authoring, `loadConfig` for the CLI.
@@ -34,11 +34,21 @@ interface GenerateOptions {
34
34
  skipAudit?: boolean;
35
35
  /** Write even when the audit found errors. The report still shows them. */
36
36
  force?: boolean;
37
+ /**
38
+ * Hand-authored tweaks from .webmcp-codegen.json (usually written by the
39
+ * dev dashboard). Applied after the safety review so they survive
40
+ * regeneration.
41
+ */
42
+ overrides?: ToolOverrides;
37
43
  }
38
44
  interface GenerateResult {
39
45
  tools: ReviewedTool[];
46
+ /** Endpoints deliberately not generated (webhooks, config exclusions). */
47
+ skipped: SkippedEndpoint[];
40
48
  findings: AuditFinding[];
41
49
  files: GeneratedFile[];
50
+ /** Human-facing pipeline notes, e.g. "stripped the shared v1 prefix". */
51
+ notes: string[];
42
52
  /** True when audit errors stopped any file from being written. */
43
53
  blocked: boolean;
44
54
  /** True when this run actually wrote files (false for dry runs and blocks). */
package/dist/index.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import {
2
2
  defineConfig,
3
3
  runGenerate
4
- } from "./chunk-LYHLGSAI.js";
5
- import "./chunk-BIKKPCRT.js";
4
+ } from "./chunk-MJQ5B6HB.js";
5
+ import "./chunk-FWSATV7C.js";
6
6
  import "./chunk-KSQMJERY.js";
7
7
  export {
8
8
  defineConfig,