@kubb/cli 5.11.4 → 5.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/{Telemetry-Be1nltUh.cjs → Telemetry-Be3hIy6_.cjs} +2 -42
  2. package/dist/Telemetry-Be3hIy6_.cjs.map +1 -0
  3. package/dist/{Telemetry-CwaVQsvD.js → Telemetry-CfJAUDnh.js} +3 -31
  4. package/dist/Telemetry-CfJAUDnh.js.map +1 -0
  5. package/dist/{background-CQPyCSn0.js → background-BBpH2jmk.js} +7 -5
  6. package/dist/background-BBpH2jmk.js.map +1 -0
  7. package/dist/{background-mKdZ5HRx.cjs → background-BW-e9FCX.cjs} +5 -4
  8. package/dist/background-BW-e9FCX.cjs.map +1 -0
  9. package/dist/{background-aTbwrgui.js → background-DS2zOde-.js} +1 -1
  10. package/dist/config-Bh00Hwpw.js +132 -0
  11. package/dist/config-Bh00Hwpw.js.map +1 -0
  12. package/dist/config-D63Gum4F.cjs +169 -0
  13. package/dist/config-D63Gum4F.cjs.map +1 -0
  14. package/dist/config-iVdOcpro.js +1 -0
  15. package/dist/credentials-CZK6zPLv.js.map +1 -1
  16. package/dist/credentials-DDaKqtJn.cjs.map +1 -1
  17. package/dist/errors-Cl69p_vI.cjs +43 -0
  18. package/dist/errors-Cl69p_vI.cjs.map +1 -0
  19. package/dist/errors-DmC83ALX.js +32 -0
  20. package/dist/errors-DmC83ALX.js.map +1 -0
  21. package/dist/index.cjs +872 -307
  22. package/dist/index.cjs.map +1 -1
  23. package/dist/index.js +863 -298
  24. package/dist/index.js.map +1 -1
  25. package/dist/registry-BlKLO4ft.cjs +272 -0
  26. package/dist/registry-BlKLO4ft.cjs.map +1 -0
  27. package/dist/registry-CgQYxUju.js +200 -0
  28. package/dist/registry-CgQYxUju.js.map +1 -0
  29. package/dist/reporters-Dl8lrJZG.js +817 -0
  30. package/dist/reporters-Dl8lrJZG.js.map +1 -0
  31. package/dist/reporters-TkCgd4dS.cjs +829 -0
  32. package/dist/reporters-TkCgd4dS.cjs.map +1 -0
  33. package/dist/{run-YHth5Afz.js → run-BJn0RSW2.js} +170 -76
  34. package/dist/run-BJn0RSW2.js.map +1 -0
  35. package/dist/{run-BWRmQxtE.cjs → run-BszyZwY3.cjs} +176 -82
  36. package/dist/run-BszyZwY3.cjs.map +1 -0
  37. package/dist/{run-Bczs5vbI.cjs → run-BtVKUjdK.cjs} +4 -3
  38. package/dist/{run-Bczs5vbI.cjs.map → run-BtVKUjdK.cjs.map} +1 -1
  39. package/dist/run-CFf0yxyY.cjs +94 -0
  40. package/dist/run-CFf0yxyY.cjs.map +1 -0
  41. package/dist/run-D_dD7xHp.js +93 -0
  42. package/dist/run-D_dD7xHp.js.map +1 -0
  43. package/dist/run-DsJJCzQD.js +1 -0
  44. package/dist/{run-B6VxJ_VM.js → run-KuzAqsjg.js} +3 -2
  45. package/dist/{run-B6VxJ_VM.js.map → run-KuzAqsjg.js.map} +1 -1
  46. package/dist/run-TrjCNL8e.cjs +227 -0
  47. package/dist/run-TrjCNL8e.cjs.map +1 -0
  48. package/dist/run-wkN1irUo.js +224 -0
  49. package/dist/run-wkN1irUo.js.map +1 -0
  50. package/dist/{snapshot-Cilp10vE.js → snapshot-CdGwHH15.js} +49 -62
  51. package/dist/snapshot-CdGwHH15.js.map +1 -0
  52. package/dist/{snapshot-D_rXnnMH.cjs → snapshot-u4L8mh4Y.cjs} +51 -64
  53. package/dist/snapshot-u4L8mh4Y.cjs.map +1 -0
  54. package/dist/status-DHPeR3bq.js +77 -0
  55. package/dist/status-DHPeR3bq.js.map +1 -0
  56. package/dist/status-dpSvxxtN.cjs +78 -0
  57. package/dist/status-dpSvxxtN.cjs.map +1 -0
  58. package/dist/studioWorker.js +2 -2
  59. package/dist/summary-6-rotl2L.cjs +46 -0
  60. package/dist/summary-6-rotl2L.cjs.map +1 -0
  61. package/dist/summary-DkSLti6f.js +41 -0
  62. package/dist/summary-DkSLti6f.js.map +1 -0
  63. package/dist/text-CMP4M19Z.js +239 -0
  64. package/dist/text-CMP4M19Z.js.map +1 -0
  65. package/dist/text-CknhKMvM.cjs +341 -0
  66. package/dist/text-CknhKMvM.cjs.map +1 -0
  67. package/dist/utils-CnjEJIdZ.js +612 -0
  68. package/dist/utils-CnjEJIdZ.js.map +1 -0
  69. package/dist/utils-DpKrXR2t.cjs +727 -0
  70. package/dist/utils-DpKrXR2t.cjs.map +1 -0
  71. package/package.json +7 -7
  72. package/dist/Telemetry-Be1nltUh.cjs.map +0 -1
  73. package/dist/Telemetry-CwaVQsvD.js.map +0 -1
  74. package/dist/background-CQPyCSn0.js.map +0 -1
  75. package/dist/background-mKdZ5HRx.cjs.map +0 -1
  76. package/dist/config-B56ZuVhi.js +0 -95
  77. package/dist/config-B56ZuVhi.js.map +0 -1
  78. package/dist/config-Cs4N79Fm.cjs +0 -113
  79. package/dist/config-Cs4N79Fm.cjs.map +0 -1
  80. package/dist/output-HgzJqkSF.cjs +0 -1076
  81. package/dist/output-HgzJqkSF.cjs.map +0 -1
  82. package/dist/output-PhkPtNP7.js +0 -937
  83. package/dist/output-PhkPtNP7.js.map +0 -1
  84. package/dist/registry-DYUhC7rH.js +0 -115
  85. package/dist/registry-DYUhC7rH.js.map +0 -1
  86. package/dist/registry-tk-v78fd.cjs +0 -157
  87. package/dist/registry-tk-v78fd.cjs.map +0 -1
  88. package/dist/run-AfjIBtiL.js +0 -39
  89. package/dist/run-AfjIBtiL.js.map +0 -1
  90. package/dist/run-BWRmQxtE.cjs.map +0 -1
  91. package/dist/run-B_Gr9TJ4.cjs +0 -345
  92. package/dist/run-B_Gr9TJ4.cjs.map +0 -1
  93. package/dist/run-CwrI0wRy.js +0 -1
  94. package/dist/run-DDoJGjWT.cjs +0 -40
  95. package/dist/run-DDoJGjWT.cjs.map +0 -1
  96. package/dist/run-SYKj8-Q5.js +0 -343
  97. package/dist/run-SYKj8-Q5.js.map +0 -1
  98. package/dist/run-YHth5Afz.js.map +0 -1
  99. package/dist/snapshot-Cilp10vE.js.map +0 -1
  100. package/dist/snapshot-D_rXnnMH.cjs.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,8 +1,12 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
2
  const require_rolldown_runtime = require("./rolldown-runtime-CPE0t6oh.cjs");
3
- const require_Telemetry = require("./Telemetry-Be1nltUh.cjs");
4
- const require_output = require("./output-HgzJqkSF.cjs");
5
- const require_registry = require("./registry-tk-v78fd.cjs");
3
+ const require_errors = require("./errors-Cl69p_vI.cjs");
4
+ const require_Telemetry = require("./Telemetry-Be3hIy6_.cjs");
5
+ const require_utils = require("./utils-DpKrXR2t.cjs");
6
+ const require_text = require("./text-CknhKMvM.cjs");
7
+ const require_registry = require("./registry-BlKLO4ft.cjs");
8
+ const require_config = require("./config-D63Gum4F.cjs");
9
+ const require_summary = require("./summary-6-rotl2L.cjs");
6
10
  let node_util = require("node:util");
7
11
  let _gunshi_plugin_dryrun = require("@gunshi/plugin-dryrun");
8
12
  _gunshi_plugin_dryrun = require_rolldown_runtime.__toESM(_gunshi_plugin_dryrun, 1);
@@ -14,9 +18,9 @@ let node_path = require("node:path");
14
18
  node_path = require_rolldown_runtime.__toESM(node_path, 1);
15
19
  let node_fs = require("node:fs");
16
20
  node_fs = require_rolldown_runtime.__toESM(node_fs, 1);
17
- let _kubb_core = require("@kubb/core");
18
21
  let _clack_prompts = require("@clack/prompts");
19
22
  _clack_prompts = require_rolldown_runtime.__toESM(_clack_prompts, 1);
23
+ let _kubb_core = require("@kubb/core");
20
24
  //#region src/commands/shared.ts
21
25
  const configArg = { config: {
22
26
  type: "string",
@@ -114,7 +118,8 @@ function resolveLogLevel({ verbose, silent, logLevel }) {
114
118
  }
115
119
  const command$13 = (0, gunshi.defineWithTypes)()({
116
120
  name: "generate",
117
- description: "Generate TypeScript types, API clients, React Query hooks, Zod schemas, and more from an OpenAPI specification. Reads kubb.config.ts by default. Pass an OpenAPI file path as the first argument to override the input without editing the config.",
121
+ rendering: { header: null },
122
+ description: "Generate types, clients, hooks, and schemas from an OpenAPI specification.",
118
123
  examples: [
119
124
  "kubb generate # generate from kubb.config.ts",
120
125
  "kubb generate ./openapi.yaml # generate from this spec instead of the config input",
@@ -162,7 +167,7 @@ const command$13 = (0, gunshi.defineWithTypes)()({
162
167
  async run(ctx) {
163
168
  const { values } = ctx;
164
169
  const logLevel = resolveLogLevel(values);
165
- const { run } = await Promise.resolve().then(() => require("./run-B_Gr9TJ4.cjs"));
170
+ const { run } = await Promise.resolve().then(() => require("./run-TrjCNL8e.cjs"));
166
171
  await run({
167
172
  input: values.input,
168
173
  configPath: values.config,
@@ -178,7 +183,7 @@ const command$13 = (0, gunshi.defineWithTypes)()({
178
183
  /** The runner is imported when the command runs, so listing `kubb --help` never loads `@kubb/mcp`. */
179
184
  const command$12 = (0, gunshi.define)({
180
185
  name: "mcp",
181
- description: "Start a Model Context Protocol (MCP) server that exposes Kubb code generation as tools for AI assistants. Once running, configure your AI client (Claude, Cursor, Windsurf, etc.) to connect to it — the assistant can then call kubb generate directly without leaving the chat.",
186
+ description: "Start an MCP server for code generation from an AI assistant.",
182
187
  examples: [
183
188
  "kubb mcp",
184
189
  "# Then add to your MCP client config:",
@@ -186,7 +191,7 @@ const command$12 = (0, gunshi.define)({
186
191
  ].join("\n"),
187
192
  rendering: { header: null },
188
193
  async run() {
189
- const { run } = await Promise.resolve().then(() => require("./run-Bczs5vbI.cjs"));
194
+ const { run } = await Promise.resolve().then(() => require("./run-BtVKUjdK.cjs"));
190
195
  await run();
191
196
  }
192
197
  });
@@ -201,7 +206,7 @@ function studioCommand({ action, block, ...definition }) {
201
206
  ...definition,
202
207
  toKebab: true,
203
208
  async run({ values }) {
204
- const { runStudio } = await Promise.resolve().then(() => require("./run-BWRmQxtE.cjs"));
209
+ const { runStudio } = await Promise.resolve().then(() => require("./run-BszyZwY3.cjs"));
205
210
  await runStudio(values, action, { block });
206
211
  }
207
212
  });
@@ -231,7 +236,7 @@ const command$11 = studioCommand({
231
236
  },
232
237
  block: true,
233
238
  async action(options) {
234
- const { connect } = await Promise.resolve().then(() => require("./run-BWRmQxtE.cjs"));
239
+ const { connect } = await Promise.resolve().then(() => require("./run-BszyZwY3.cjs"));
235
240
  await connect(options);
236
241
  }
237
242
  });
@@ -243,7 +248,7 @@ const command$10 = studioCommand({
243
248
  examples: ["kubb studio login", "kubb studio login --url http://localhost:3000"].join("\n"),
244
249
  args: command$11.args,
245
250
  async action(options) {
246
- const { login } = await Promise.resolve().then(() => require("./run-BWRmQxtE.cjs"));
251
+ const { login } = await Promise.resolve().then(() => require("./run-BszyZwY3.cjs"));
247
252
  await login(options);
248
253
  }
249
254
  });
@@ -255,7 +260,7 @@ const command$9 = studioCommand({
255
260
  examples: "kubb studio logout",
256
261
  args: {},
257
262
  async action() {
258
- const { stopWorker } = await Promise.resolve().then(() => require("./background-mKdZ5HRx.cjs"));
263
+ const { stopWorker } = await Promise.resolve().then(() => require("./background-BW-e9FCX.cjs"));
259
264
  const { clearCredentials } = await Promise.resolve().then(() => require("./credentials-DDaKqtJn.cjs")).then((n) => n.credentials_exports);
260
265
  await stopWorker();
261
266
  await clearCredentials();
@@ -298,7 +303,7 @@ const command$8 = (0, gunshi.define)({
298
303
  ...logLevelArg
299
304
  },
300
305
  async run(ctx) {
301
- const { runner } = await Promise.resolve().then(() => require("./snapshot-D_rXnnMH.cjs"));
306
+ const { runner } = await Promise.resolve().then(() => require("./snapshot-u4L8mh4Y.cjs"));
302
307
  await runner(ctx);
303
308
  }
304
309
  });
@@ -309,7 +314,7 @@ const command$7 = studioCommand({
309
314
  description: "Keep this project connected to Kubb Studio in the background.",
310
315
  args: command$11.args,
311
316
  async action(options) {
312
- const { startWorker } = await Promise.resolve().then(() => require("./background-mKdZ5HRx.cjs"));
317
+ const { startWorker } = await Promise.resolve().then(() => require("./background-BW-e9FCX.cjs"));
313
318
  await startWorker(options);
314
319
  }
315
320
  });
@@ -317,7 +322,7 @@ const stopCommand = (0, gunshi.define)({
317
322
  name: "stop",
318
323
  description: "Stop this project's background connection and keep its pairing.",
319
324
  async run() {
320
- const { stopWorker } = await Promise.resolve().then(() => require("./background-mKdZ5HRx.cjs"));
325
+ const { stopWorker } = await Promise.resolve().then(() => require("./background-BW-e9FCX.cjs"));
321
326
  await stopWorker();
322
327
  console.log("Background connection stopped.");
323
328
  }
@@ -330,32 +335,21 @@ const command$6 = studioCommand({
330
335
  examples: "kubb studio status",
331
336
  args: { url: studioConnectionArgs.url },
332
337
  async action(options) {
333
- const { getWorkerLogPath, getWorkerStatus } = await Promise.resolve().then(() => require("./background-mKdZ5HRx.cjs"));
338
+ const { getWorkerLogPath, getWorkerStatus } = await Promise.resolve().then(() => require("./background-BW-e9FCX.cjs"));
334
339
  const { readCredentials } = await Promise.resolve().then(() => require("./credentials-DDaKqtJn.cjs")).then((n) => n.credentials_exports);
335
- const { formatPermissionRows } = await Promise.resolve().then(() => require("./run-BWRmQxtE.cjs"));
336
- const worker = await getWorkerStatus();
337
- console.log(`Background connection: ${worker.state}${worker.running ? ` (PID ${worker.pid})` : ""}`);
338
- console.log(`Logs: ${getWorkerLogPath()}`);
339
- const credentials = await readCredentials();
340
- if (!credentials) {
341
- console.log("Not paired. Run `kubb studio login`.");
342
- return;
343
- }
344
- console.log(`Paired with ${credentials.studioUrl} as ${(0, node_util.styleText)("cyan", credentials.agentSlug || credentials.agentId)}`);
345
- if (credentials.studioUrl !== options.studioUrl) console.log((0, node_util.styleText)("yellow", `Connecting to ${options.studioUrl} needs pairing again.`));
346
- const remembered = credentials.projects?.[node_process.default.cwd()];
347
- if (!remembered) {
348
- console.log((0, node_util.styleText)("dim", "No saved permissions for this project. Run `kubb studio` to connect and choose."));
349
- return;
350
- }
351
- console.log((0, node_util.styleText)("dim", "Saved permissions"));
352
- for (const row of formatPermissionRows({
353
- allowRead: remembered.allowRead === true,
354
- allowReadAst: remembered.allowReadAst === true,
355
- allowWrite: remembered.allowWrite === true,
356
- allowConfigEdit: remembered.allowConfigEdit === true,
357
- allowExec: remembered.allowExec === true
358
- })) console.log(row);
340
+ const { formatStatus } = await Promise.resolve().then(() => require("./status-dpSvxxtN.cjs"));
341
+ const [worker, credentials] = await Promise.all([getWorkerStatus(), readCredentials()]);
342
+ console.log([
343
+ "",
344
+ ...formatStatus({
345
+ studioUrl: options.studioUrl,
346
+ credentials,
347
+ worker,
348
+ project: node_process.default.cwd(),
349
+ logPath: getWorkerLogPath()
350
+ }),
351
+ ""
352
+ ].join("\n"));
359
353
  }
360
354
  });
361
355
  //#endregion
@@ -400,16 +394,22 @@ function routeStudioFlags(args) {
400
394
  /** The runner is imported when the command runs, so listing `kubb --help` never loads `@kubb/adapter-oas`. */
401
395
  const command$4 = (0, gunshi.define)({
402
396
  name: "validate",
403
- description: "Parse and validate an OpenAPI/Swagger file for structural correctness. Reports schema errors, missing required fields, and malformed references. Use this before running generate to catch spec issues early.",
397
+ description: "Check a specification with the configured adapter before generating.",
404
398
  examples: ["kubb validate ./openapi.yaml", "kubb validate https://petstore3.swagger.io/api/v3/openapi.json"].join("\n"),
405
- args: { input: {
406
- type: "positional",
407
- description: "Path or URL to the OpenAPI/Swagger file to validate",
408
- required: true
409
- } },
399
+ args: {
400
+ ...configArg,
401
+ input: {
402
+ type: "positional",
403
+ description: "Path or URL to the specification to validate",
404
+ required: true
405
+ }
406
+ },
410
407
  async run({ values }) {
411
- const { run } = await Promise.resolve().then(() => require("./run-DDoJGjWT.cjs"));
412
- await run({ input: values.input });
408
+ const { run } = await Promise.resolve().then(() => require("./run-CFf0yxyY.cjs"));
409
+ await run({
410
+ input: values.input,
411
+ configPath: values.config
412
+ });
413
413
  }
414
414
  });
415
415
  //#endregion
@@ -426,6 +426,8 @@ const DEPRECATED_FLAGS = {
426
426
  dryRun: "dry-run",
427
427
  packageVersion: "package-version"
428
428
  };
429
+ /** Flags a single command renamed, rewritten only for that command, since the old name is a common word. */
430
+ const DEPRECATED_COMMAND_FLAGS = { update: { check: "dry-run" } };
429
431
  const FLAG_PATTERN = /^--([a-zA-Z][\w-]*)(=.*)?$/;
430
432
  /**
431
433
  * Rewrites a deprecated camelCase flag to its kebab-case replacement and warns on stderr, so a
@@ -433,10 +435,11 @@ const FLAG_PATTERN = /^--([a-zA-Z][\w-]*)(=.*)?$/;
433
435
  * of failing with an unknown-option error.
434
436
  */
435
437
  function resolveDeprecatedFlags(args) {
438
+ const commandFlags = DEPRECATED_COMMAND_FLAGS[args[0] ?? ""] ?? {};
436
439
  return args.map((arg) => {
437
440
  const match = FLAG_PATTERN.exec(arg);
438
441
  const name = match?.[1];
439
- const replacement = name ? DEPRECATED_FLAGS[name] : void 0;
442
+ const replacement = name ? DEPRECATED_FLAGS[name] ?? commandFlags[name] : void 0;
440
443
  if (!name || !replacement) return arg;
441
444
  console.error(`${(0, node_util.styleText)("yellow", "Warning:")} --${name} is deprecated, use --${replacement} instead. --${name} will be removed in a future major version.`);
442
445
  return `--${replacement}${match?.[2] ?? ""}`;
@@ -466,25 +469,29 @@ const packageManagers = {
466
469
  name: "pnpm",
467
470
  lockFiles: ["pnpm-lock.yaml"],
468
471
  initCommand: ["init"],
469
- installCommand: ["add", "-D"]
472
+ installCommand: ["add", "-D"],
473
+ runCommand: []
470
474
  },
471
475
  yarn: {
472
476
  name: "yarn",
473
477
  lockFiles: ["yarn.lock"],
474
478
  initCommand: ["init", "-y"],
475
- installCommand: ["add", "-D"]
479
+ installCommand: ["add", "-D"],
480
+ runCommand: []
476
481
  },
477
482
  bun: {
478
483
  name: "bun",
479
484
  lockFiles: ["bun.lock", "bun.lockb"],
480
485
  initCommand: ["init", "-y"],
481
- installCommand: ["add", "-d"]
486
+ installCommand: ["add", "-d"],
487
+ runCommand: ["run"]
482
488
  },
483
489
  npm: {
484
490
  name: "npm",
485
491
  lockFiles: ["package-lock.json"],
486
492
  initCommand: ["init", "-y"],
487
- installCommand: ["install", "--save-dev"]
493
+ installCommand: ["install", "--save-dev"],
494
+ runCommand: ["run"]
488
495
  }
489
496
  };
490
497
  /** Detects the package manager for `cwd`: the `packageManager` field in `package.json`, then a lock file, then `npm`. */
@@ -512,27 +519,191 @@ async function initPackageJson(cwd, packageManager) {
512
519
  throwOnError: true
513
520
  });
514
521
  }
522
+ const INSTALL_TIMEOUT_MS = 3e5;
515
523
  /**
516
- * Installs the given packages at `cwd` using the detected package manager.
524
+ * Installs the given packages at `cwd` using the detected package manager. Each output line goes
525
+ * to `onLine`, so the caller can show it while the install runs; a failed install also keeps the
526
+ * end of the output in the error.
517
527
  */
518
- async function installPackages(packages, packageManager, cwd = node_process.default.cwd()) {
519
- await (0, tinyexec.x)(packageManager.name, [...packageManager.installCommand, ...packages], {
528
+ async function installPackages(packages, packageManager, cwd = node_process.default.cwd(), onLine) {
529
+ const args = [...packageManager.installCommand, ...packages];
530
+ const result = (0, tinyexec.x)(packageManager.name, args, {
520
531
  nodeOptions: {
521
532
  cwd,
522
- stdio: "inherit"
533
+ stdio: [
534
+ "ignore",
535
+ "pipe",
536
+ "pipe"
537
+ ]
523
538
  },
524
- throwOnError: true
539
+ timeout: INSTALL_TIMEOUT_MS,
540
+ throwOnError: false
525
541
  });
542
+ const lines = [];
543
+ for await (const line of result) {
544
+ lines.push(line);
545
+ onLine?.(line);
546
+ }
547
+ if (result.exitCode === 0) return;
548
+ if (result.exitCode === void 0) throw new Error(`\`${packageManager.name} ${args.join(" ")}\` timed out after ${INSTALL_TIMEOUT_MS / 6e4} minutes`);
549
+ const output = lines.slice(-20).join("\n");
550
+ throw new Error(`\`${packageManager.name} ${args.join(" ")}\` failed with exit code ${result.exitCode}${output ? `:\n${output}` : ""}`);
526
551
  }
527
552
  /**
528
- * What the installed manifests of the `selected` plugins say is missing from the selection: a
529
- * `dependencies` entry, or a `client` plugin next to a `consumer`. Empty when nothing is missing.
553
+ * What the selected plugins' `manifests` say is missing from the selection: a `dependencies`
554
+ * entry, or a `client` plugin next to a `consumer`. Empty when nothing is missing.
530
555
  * `optionalDependencies` are skipped, since `kubb init` writes the default options.
531
556
  */
532
- function missingPluginDependencies({ selected, manifests }) {
557
+ function missingPluginDependencies(manifests) {
558
+ const selected = manifests.map((manifest) => manifest.name);
533
559
  const hasClient = manifests.some((manifest) => manifest.role === "client");
534
560
  return manifests.flatMap((manifest) => [...(manifest.dependencies ?? []).filter((name) => !selected.includes(name)).map((name) => `${manifest.name} needs ${name}.`), ...manifest.role === "consumer" && !hasClient ? [`${manifest.name} needs a client plugin, such as plugin-axios or plugin-fetch.`] : []]);
535
561
  }
562
+ async function readPackageJson(cwd) {
563
+ try {
564
+ return JSON.parse(await require_utils.read(node_path.default.join(cwd, "package.json")));
565
+ } catch {
566
+ return {};
567
+ }
568
+ }
569
+ const SPEC_FILE = /^(openapi|swagger)[\w.-]*\.(ya?ml|json)$/i;
570
+ const SKIPPED_DIRS = /* @__PURE__ */ new Set([
571
+ "node_modules",
572
+ "dist",
573
+ "build",
574
+ "coverage"
575
+ ]);
576
+ /**
577
+ * Finds OpenAPI spec files up to `depth` folders below `cwd`, shallowest first, as `./`-relative
578
+ * paths. Walks one level at a time and stops after `limit` files, so a large repo stays fast.
579
+ */
580
+ async function findSpecFiles(cwd, { depth = 3, limit = 20 } = {}) {
581
+ const found = [];
582
+ let level = [""];
583
+ for (let current = 0; current <= depth && level.length && found.length < limit; current++) {
584
+ const listings = await Promise.all(level.map(async (dir) => ({
585
+ dir,
586
+ entries: await node_fs.default.promises.readdir(node_path.default.join(cwd, dir), { withFileTypes: true }).catch(() => [])
587
+ })));
588
+ level = [];
589
+ for (const { dir, entries } of listings) for (const entry of entries.toSorted((a, b) => a.name.localeCompare(b.name))) {
590
+ if (entry.isFile() && SPEC_FILE.test(entry.name)) found.push(`./${node_path.default.posix.join(dir, entry.name)}`);
591
+ if (entry.isDirectory() && !entry.name.startsWith(".") && !SKIPPED_DIRS.has(entry.name)) level.push(node_path.default.posix.join(dir, entry.name));
592
+ }
593
+ }
594
+ return found.slice(0, limit);
595
+ }
596
+ const SPEC_HEAD_BYTES = 262144;
597
+ const METHOD = "(?:get|put|post|delete|options|head|patch|trace)";
598
+ /**
599
+ * Reads the OpenAPI or Swagger version and counts the operations from the first 256 KB of the
600
+ * file, so a large spec is never parsed. The count is left out when the file is longer.
601
+ */
602
+ async function readSpecInfo(file) {
603
+ const handle = await node_fs.default.promises.open(file);
604
+ try {
605
+ const buffer = Buffer.alloc(SPEC_HEAD_BYTES);
606
+ const { bytesRead } = await handle.read(buffer, 0, SPEC_HEAD_BYTES, 0);
607
+ const head = buffer.toString("utf-8", 0, bytesRead);
608
+ const complete = bytesRead < SPEC_HEAD_BYTES;
609
+ const version = head.match(/^\s*["']?(?:openapi|swagger)["']?\s*:\s*["']?(\d[\d.]*)/m)?.[1];
610
+ const methods = new RegExp(`^\\s*["']?${METHOD}["']?\\s*:\\s*(?:\\{|$)`, "gm");
611
+ return {
612
+ version,
613
+ operations: complete ? head.match(methods)?.length ?? 0 : void 0
614
+ };
615
+ } finally {
616
+ await handle.close();
617
+ }
618
+ }
619
+ /** Finds a plugin by registry id, slug, or short name, so `ts`, `plugin-ts`, and `kubb-labs/plugin-ts` all match. */
620
+ function findPlugin({ plugins, id }) {
621
+ const ids = id.includes("/") ? [id] : [`kubb-labs/${id}`, `kubb-labs/plugin-${id}`];
622
+ return plugins.find((entry) => ids.includes(entry.id));
623
+ }
624
+ /**
625
+ * Turns the comma-separated `--plugins` value into registry entries.
626
+ *
627
+ * @throws when an id is not in `plugins`.
628
+ */
629
+ function parsePluginsFlag({ plugins, value }) {
630
+ const ids = value.split(",").map((id) => id.trim()).filter(Boolean);
631
+ const unknown = ids.filter((id) => !findPlugin({
632
+ plugins,
633
+ id
634
+ }));
635
+ if (unknown.length) throw new _kubb_core.Diagnostics.Error({
636
+ code: _kubb_core.Diagnostics.code.pluginNotFound,
637
+ severity: "error",
638
+ message: `Not in the Kubb registry: ${unknown.join(", ")}. Browse the plugins at https://kubb.dev/extensions.`,
639
+ help: "Use plugin ids from https://kubb.dev/extensions, such as plugin-ts or ts."
640
+ });
641
+ return ids.map((id) => findPlugin({
642
+ plugins,
643
+ id
644
+ }));
645
+ }
646
+ /** Adds `"generate": "kubb generate"` to the `package.json` scripts at `cwd`. Returns `false`, and changes nothing, when a `generate` script exists. */
647
+ async function addGenerateScript(cwd) {
648
+ const file = node_path.default.join(cwd, "package.json");
649
+ const source = await require_utils.read(file);
650
+ const packageJson = JSON.parse(source);
651
+ if (packageJson.scripts?.generate !== void 0) return false;
652
+ const indent = source.match(/^[ \t]+/m)?.[0] ?? " ";
653
+ const updated = {
654
+ ...packageJson,
655
+ scripts: {
656
+ ...packageJson.scripts,
657
+ generate: "kubb generate"
658
+ }
659
+ };
660
+ await require_utils.write(file, JSON.stringify(updated, null, indent));
661
+ return true;
662
+ }
663
+ /** Reads the `generate` script from the `package.json` at `cwd`. */
664
+ async function generateScript(cwd) {
665
+ return (await readPackageJson(cwd)).scripts?.generate;
666
+ }
667
+ //#endregion
668
+ //#region src/runners/installWithSpinner.ts
669
+ /** How many package manager lines stay visible under the install title while it runs. */
670
+ const LOG_LINES = 6;
671
+ /**
672
+ * Installs `packages` and reports how long it took. A rich terminal shows the package manager's
673
+ * last lines under the title while the install runs and clears them when it succeeds; a plain one
674
+ * prints the start and end lines. A failed install keeps the output, reports the failure and rethrows.
675
+ */
676
+ async function installWithSpinner({ packages, names, packageManager, cwd, install = installPackages }) {
677
+ const start = node_process.default.hrtime();
678
+ const task = createTask(`Installing ${names} with ${packageManager.name}`);
679
+ try {
680
+ await install(packages, packageManager, cwd, task.message);
681
+ } catch (error) {
682
+ task.error("Install failed");
683
+ throw error;
684
+ }
685
+ task.success(`Installed ${names} with ${packageManager.name} in ${require_Telemetry.formatMs(require_Telemetry.getElapsedMs(start))}`);
686
+ }
687
+ function createTask(title) {
688
+ if (require_text.isRichOutput()) {
689
+ const log = _clack_prompts.taskLog({
690
+ title,
691
+ limit: LOG_LINES
692
+ });
693
+ return {
694
+ message: (line) => log.message(line),
695
+ error: (text) => log.error(text, { showLog: false }),
696
+ success: (text) => log.success(text)
697
+ };
698
+ }
699
+ const spinner = require_utils.createSpinner();
700
+ spinner.start(title);
701
+ return {
702
+ message: () => {},
703
+ error: (text) => spinner.error(text),
704
+ success: (text) => spinner.stop(text)
705
+ };
706
+ }
536
707
  //#endregion
537
708
  //#region src/runners/add/run.ts
538
709
  /** The import and the `defineConfig` property to merge into `kubb.config.ts` by hand. */
@@ -549,55 +720,108 @@ function toSnippet({ artifact, manifest }) {
549
720
  property[manifest.kind]
550
721
  ].join("\n");
551
722
  }
552
- /** Adds the plugin call and its import to the config file. Returns why it could not, or `undefined`. */
553
- async function addToConfig({ detail, configFile }) {
554
- const { artifact, manifest } = detail;
723
+ /** The `add-plugin` edits for `details`, and the manifests the edits read their export names from. */
724
+ function toConfigEdits(details) {
725
+ return {
726
+ edits: details.map(({ artifact }) => ({
727
+ operation: "add-plugin",
728
+ plugin: artifact.package
729
+ })),
730
+ extensions: new Map(details.map(({ artifact, manifest }) => [artifact.package, {
731
+ package: artifact.package,
732
+ version: artifact.version,
733
+ manifest
734
+ }]))
735
+ };
736
+ }
737
+ /** The lines adding `details` would put in the config `source`, trimmed, or why it could not add them. */
738
+ async function previewConfig({ details, source }) {
555
739
  try {
556
- const { writeConfigEdits } = await import("@kubb/studio");
557
- const { outcomes } = await writeConfigEdits({
558
- filePath: configFile,
559
- edits: [{
560
- operation: "add-plugin",
561
- plugin: artifact.package
562
- }],
563
- extensions: /* @__PURE__ */ new Map([[artifact.package, {
564
- package: artifact.package,
565
- version: artifact.version,
566
- manifest
567
- }]])
740
+ const { applyConfigEdits } = await import("@kubb/studio");
741
+ const result = applyConfigEdits({
742
+ source,
743
+ ...toConfigEdits(details)
568
744
  });
569
- return outcomes[0]?.applied ? void 0 : outcomes[0]?.reason ?? "the config file was not changed";
745
+ const failed = result.outcomes.find((outcome) => !outcome.applied);
746
+ if (failed) return { reason: failed.reason ?? "the config file was not changed" };
747
+ const remaining = source.split("\n");
748
+ return { lines: result.source.split("\n").filter((line) => {
749
+ const index = remaining.indexOf(line);
750
+ if (index !== -1) remaining.splice(index, 1);
751
+ return index === -1;
752
+ }).map((line) => line.trim()) };
570
753
  } catch (error) {
571
- return require_Telemetry.getErrorMessage(error);
754
+ return { reason: require_errors.getErrorMessage(error) };
572
755
  }
573
756
  }
574
- async function selectExtension(registry) {
575
- const options = (await require_registry.fetchRegistryIndex({ registry })).toSorted((a, b) => a.id.localeCompare(b.id)).map((entry) => ({
576
- value: entry.id,
577
- label: entry.manifest.displayName,
578
- hint: `${entry.id} · ${entry.kind} ${entry.artifact.version} · ${entry.manifest.description}`
579
- }));
580
- if (!options.length) throw new _kubb_core.Diagnostics.Error({
757
+ async function selectExtension({ entries, versions, registry }) {
758
+ if (!entries.length) throw new _kubb_core.Diagnostics.Error({
581
759
  code: _kubb_core.Diagnostics.code.unknown,
582
760
  severity: "error",
583
761
  message: `The registry at ${registry} lists no extensions.`,
584
762
  help: "Check that `KUBB_REGISTRY` points at a Kubb registry, or unset it to use https://kubb.dev/registry."
585
763
  });
764
+ const searchText = new Map(entries.map((entry) => [entry.id, `${entry.id} ${entry.manifest.displayName} ${entry.manifest.description}`.toLowerCase()]));
765
+ const options = entries.toSorted((a, b) => a.id.localeCompare(b.id)).map((entry) => {
766
+ const installed = versions.has(entry.artifact.package);
767
+ return {
768
+ value: entry.id,
769
+ label: require_registry.toShortId(entry.id),
770
+ hint: installed ? "installed" : summarize(entry.manifest.description),
771
+ disabled: installed
772
+ };
773
+ });
586
774
  const id = await _clack_prompts.autocomplete({
587
- message: "Select an extension to add:",
775
+ message: `Add an extension · ${entries.length} in the registry`,
776
+ placeholder: "Search",
588
777
  options,
589
- filter: (search, option) => `${option.label} ${option.hint}`.toLowerCase().includes(search.toLowerCase())
778
+ filter: (search, option) => searchText.get(option.value)?.includes(search.toLowerCase()) ?? false
590
779
  });
591
780
  return _clack_prompts.isCancel(id) ? void 0 : id;
592
781
  }
782
+ /** Asks whether to also add the required plugins the config lacks. `--yes` adds them. */
783
+ async function confirmDependencies({ entry, missing, yes }) {
784
+ if (!missing.length || yes) return true;
785
+ const names = missing.map((dependency) => dependency.manifest.name).join(", ");
786
+ const answer = await _clack_prompts.select({
787
+ message: `${entry.manifest.name} needs ${names}. Add ${names} as well?`,
788
+ options: [{
789
+ value: true,
790
+ label: `Yes, add ${missing.length === 1 ? "both" : "all"}`,
791
+ hint: "recommended"
792
+ }, {
793
+ value: false,
794
+ label: `Only ${entry.manifest.name}`,
795
+ hint: "generate fails until you add it"
796
+ }],
797
+ initialValue: true
798
+ });
799
+ return _clack_prompts.isCancel(answer) ? void 0 : answer;
800
+ }
801
+ /** The first sentence of a registry description, cut to one terminal line. */
802
+ function summarize(description, max = 64) {
803
+ const sentence = description.split(/\.\s/)[0].replace(/\.$/, "");
804
+ return sentence.length > max ? `${sentence.slice(0, max - 1).trimEnd()}…` : sentence;
805
+ }
593
806
  /**
594
807
  * Installs an extension from the Kubb registry at its reviewed version and, for a plugin, adds it to
595
- * `kubb.config.ts`. Without an id, asks which one. Refuses when the running Kubb or Node.js version
596
- * is outside its compatibility.
808
+ * `kubb.config.ts`, offering to add the plugins it needs as well. Without an id, asks which one.
809
+ * Previews the packages and config lines before asking. Refuses when the running Kubb or Node.js
810
+ * version is outside its compatibility.
597
811
  */
598
812
  async function run$4({ id: input, configPath, yes, install }) {
599
813
  const cwd = node_process.default.cwd();
600
- require_output.logIntro({ title: (0, node_util.styleText)("bgCyan", (0, node_util.styleText)("black", " Kubb Add ")) });
814
+ const hrStart = node_process.default.hrtime();
815
+ const summary = (title, status, lines) => require_summary.logCommandSummary({
816
+ title,
817
+ status,
818
+ hrStart,
819
+ block: true,
820
+ lines
821
+ });
822
+ const installedPackages = [];
823
+ const configured = [];
824
+ require_text.logIntro({ title: require_text.isInteractiveRun({ yes }) ? void 0 : (0, node_util.styleText)("bgCyan", (0, node_util.styleText)("black", " Kubb Add ")) });
601
825
  try {
602
826
  const registry = require_registry.registryUrl();
603
827
  if (!input && yes) throw new _kubb_core.Diagnostics.Error({
@@ -606,16 +830,38 @@ async function run$4({ id: input, configPath, yes, install }) {
606
830
  message: "Pass a registry id, such as plugin-zod, when using --yes.",
607
831
  help: "Run `kubb add plugin-zod --yes`, or drop `--yes` to pick from a list."
608
832
  });
609
- const id = input ? require_registry.toRegistryId(input) : await selectExtension(registry);
833
+ const inputId = input ? require_registry.toRegistryId(input) : void 0;
834
+ const foundConfig = configPath ? node_path.default.resolve(configPath) : require_config.findConfigFile(cwd);
835
+ const [entries, installed, configSource] = await require_utils.withSpinner({
836
+ message: "Reading the registry",
837
+ promise: Promise.all([
838
+ require_registry.fetchRegistryIndex({ registry }),
839
+ (0, _kubb_core.readInstalledPackages)({ root: cwd }),
840
+ foundConfig ? require_utils.read(foundConfig).catch(() => void 0) : void 0
841
+ ])
842
+ });
843
+ const versions = new Map(installed.map((item) => [item.package, item.version]));
844
+ const id = inputId ?? await selectExtension({
845
+ entries,
846
+ versions,
847
+ registry
848
+ });
610
849
  if (!id) {
611
- _clack_prompts.cancel("Operation canceled.");
850
+ summary("Add canceled", "canceled", [" No changes made"]);
612
851
  return;
613
852
  }
614
- const detail = await require_registry.fetchRegistryDetail({
615
- id,
616
- registry
617
- });
618
- const { artifact, manifest } = detail;
853
+ const entry = entries.find((candidate) => candidate.id === id);
854
+ if (!entry) {
855
+ const suggestions = require_registry.closestIds({
856
+ input: id,
857
+ ids: entries.map((entry) => entry.id)
858
+ }).map(require_registry.toShortId);
859
+ summary(`No extension named ${input ?? id}`, "failed", [...suggestions.map((suggestion) => ` kubb add ${suggestion}`), " Browse all kubb.dev/extensions"]);
860
+ node_process.default.exitCode = 1;
861
+ return;
862
+ }
863
+ const { artifact, manifest } = entry;
864
+ const kind = manifest.kind.charAt(0).toUpperCase() + manifest.kind.slice(1);
619
865
  const problem = require_registry.incompatibility(manifest);
620
866
  if (problem) throw new _kubb_core.Diagnostics.Error({
621
867
  code: _kubb_core.Diagnostics.code.incompatibleExtension,
@@ -623,53 +869,108 @@ async function run$4({ id: input, configPath, yes, install }) {
623
869
  message: problem,
624
870
  help: "Upgrade Kubb or Node.js to a version the extension supports."
625
871
  });
626
- const spec = require_registry.installSpec(artifact);
627
- const configFile = manifest.kind === "plugin" ? configPath ? node_path.default.resolve(configPath) : require_output.findConfigFile(cwd) : void 0;
872
+ const configFile = manifest.kind === "plugin" ? foundConfig : void 0;
628
873
  if (manifest.kind === "plugin" && !configFile) throw new _kubb_core.Diagnostics.Error({
629
874
  code: _kubb_core.Diagnostics.code.unknown,
630
875
  severity: "error",
631
876
  message: "No kubb.config.ts found. Run `kubb init` first, or pass --config.",
632
877
  help: "Run `kubb init` to create a config, or pass its path with `--config`."
633
878
  });
634
- require_output.logInfo(`${(0, node_util.styleText)("cyan", manifest.displayName)} (${manifest.kind}): ${manifest.description}`);
635
- require_output.logBlock([install ? `Install ${(0, node_util.styleText)("cyan", spec)} as a dev dependency` : `Skip installing ${spec}`, configFile ? `Add ${(0, node_util.styleText)("cyan", `${manifest.export}()`)} to ${node_path.default.relative(cwd, configFile) || configFile}` : `Print the ${manifest.kind} snippet to add`]);
879
+ const configName = configFile ? node_path.default.relative(cwd, configFile) || configFile : void 0;
880
+ const byName = new Map(entries.map((candidate) => [candidate.manifest.name, candidate]));
881
+ const inConfig = (name) => {
882
+ const dependency = byName.get(name);
883
+ return dependency ? !!configSource?.includes(`${dependency.manifest.export}(`) : installed.some((item) => item.manifest?.name === name);
884
+ };
885
+ const dependencies = manifest.dependencies ?? [];
886
+ const missing = configFile ? dependencies.filter((name) => !inConfig(name)).flatMap((name) => byName.get(name) ?? []) : [];
887
+ const withDependencies = await confirmDependencies({
888
+ entry,
889
+ missing,
890
+ yes
891
+ });
892
+ if (withDependencies === void 0) {
893
+ summary("Add canceled", "canceled", [` ${kind} ${manifest.name}`, " No changes made"]);
894
+ return;
895
+ }
896
+ const targets = [...withDependencies ? missing : [], entry];
897
+ const specs = targets.filter((target) => target === entry || !versions.has(target.artifact.package)).map((target) => require_registry.installSpec(target.artifact));
898
+ const dependencyStatus = (name) => {
899
+ if (inConfig(name)) return `${name} ${(0, node_util.styleText)("green", "✓ installed")}`;
900
+ return targets.some((target) => target.manifest.name === name) ? `${name} ${(0, node_util.styleText)("cyan", "+ adding")}` : `${name} ${(0, node_util.styleText)("yellow", "missing")}`;
901
+ };
902
+ const preview = configFile && configSource !== void 0 ? await previewConfig({
903
+ details: targets,
904
+ source: configSource
905
+ }) : void 0;
906
+ require_text.logStep(`${kind} ${(0, node_util.styleText)("cyan", manifest.name)} v${artifact.version} · ${summarize(manifest.description)}`);
907
+ require_text.logBlock([
908
+ ...dependencies.length ? [`Needs ${dependencies.map(dependencyStatus).join(" ")}`] : [],
909
+ install ? `Installs ${specs.join(" ")}` : "Installs nothing (--no-install)",
910
+ ...configName ? [`Config ${configName}`] : [`Config prints the ${manifest.kind} snippet to add`],
911
+ ...preview && "lines" in preview ? preview.lines.map((line) => (0, node_util.styleText)("green", ` + ${line}`)) : [],
912
+ ...preview && "reason" in preview ? [(0, node_util.styleText)("yellow", ` cannot edit it: ${preview.reason}`)] : []
913
+ ]);
636
914
  const confirmed = yes || await _clack_prompts.confirm({
637
- message: "Continue?",
915
+ message: "Add it?",
638
916
  initialValue: true
639
917
  });
640
918
  if (_clack_prompts.isCancel(confirmed) || !confirmed) {
641
- _clack_prompts.cancel("Operation canceled.");
919
+ summary("Add canceled", "canceled", [` ${kind} ${manifest.name}`, " No changes made"]);
642
920
  return;
643
921
  }
922
+ const packageManager = detectPackageManager(cwd);
923
+ const packages = () => install ? `${installedPackages.length} installed with ${packageManager.name}` : "installation skipped (--no-install)";
644
924
  if (install) {
645
- const packageManager = detectPackageManager(cwd);
646
- require_output.logStep(`Installing ${spec} with ${packageManager.name}`);
647
- await installPackages([spec], packageManager, cwd);
925
+ const names = targets.filter((target) => specs.includes(require_registry.installSpec(target.artifact))).map((target) => target.artifact.package);
926
+ await installWithSpinner({
927
+ packages: specs,
928
+ names: names.join(", "),
929
+ packageManager,
930
+ cwd
931
+ });
932
+ installedPackages.push(...names);
648
933
  }
649
- const reason = configFile ? await addToConfig({
650
- detail,
651
- configFile
652
- }) : `add the ${manifest.kind} to your config`;
653
- if (!reason) {
654
- require_output.logOutro((0, node_util.styleText)("green", install ? `Added ${spec}` : `Added ${manifest.export}() to the config. Install ${spec} to use it.`));
655
- return;
934
+ const unconfigured = configFile ? [] : [entry];
935
+ if (configFile) for (const target of targets) try {
936
+ const { writeConfigEdits } = await import("@kubb/studio");
937
+ const { outcomes, changed } = await writeConfigEdits({
938
+ filePath: configFile,
939
+ ...toConfigEdits([target])
940
+ });
941
+ if (!outcomes[0]?.applied) throw new Error(outcomes[0]?.reason ?? "the config file was not changed");
942
+ if (changed) configured.push(target);
943
+ require_text.logStep(`${target.manifest.export}() configured in ${configName}`);
944
+ } catch (error) {
945
+ require_text.logWarn(`Could not edit ${configName}: ${require_errors.getErrorMessage(error)}`);
946
+ unconfigured.push(target);
656
947
  }
657
- if (configFile) require_output.logWarn(`Could not edit ${configFile}: ${reason}`);
658
- require_output.logBlock([
948
+ const manual = unconfigured.length > 0;
949
+ if (manual) require_text.logBlock([
659
950
  "Add the import to your Kubb config and merge the property into its defineConfig call:",
660
951
  "",
661
- toSnippet(detail)
952
+ ...unconfigured.map((target) => toSnippet(target))
953
+ ]);
954
+ const dependenciesMissing = !withDependencies && missing.length > 0;
955
+ summary(manual ? `${kind} needs configuration` : dependenciesMissing ? "Plugin configured; dependencies missing" : configured.length ? install ? "Plugin ready" : "Plugin configured" : "Plugin already configured", manual || dependenciesMissing ? "warning" : "success", [
956
+ ` Packages ${packages()}`,
957
+ ...configured.map((target) => (0, node_util.styleText)("green", ` + ${target.manifest.export}()`)),
958
+ "",
959
+ ...!install ? [`Install ${specs.join(" ")} to use it.`] : [],
960
+ ...dependenciesMissing ? [`Missing plugins ${missing.map((target) => target.manifest.name).join(", ")}`] : [],
961
+ manual ? "Add the snippet above, then run kubb generate." : "Next kubb generate",
962
+ `Options kubb.dev/extensions/${entry.id}`
662
963
  ]);
663
- require_output.logOutro((0, node_util.styleText)("green", install ? `Installed ${spec}. Add it with the snippet above.` : `Install ${spec} and add the snippet above.`));
664
964
  } catch (error) {
665
- require_output.logError(require_Telemetry.getErrorMessage(error));
965
+ require_text.logError(require_errors.getErrorMessage(error));
966
+ summary("Add failed", "failed", installedPackages.length ? [` Packages ${installedPackages.join(", ")} installed`] : [" No changes made"]);
666
967
  node_process.default.exitCode = 1;
667
968
  }
668
969
  }
669
970
  require_rolldown_runtime.__name(run$4, "run");
670
971
  const command$3 = (0, gunshi.define)({
671
972
  name: "add",
672
- description: "Install a plugin, adapter, or parser from the Kubb registry at its reviewed version. Without an id, pick one from the registry. A plugin is added to kubb.config.ts, an adapter or parser prints the code to add. Reads KUBB_REGISTRY to use another registry.",
973
+ description: "Find and install plugins, adapters, and parsers from the Kubb registry.",
673
974
  examples: [
674
975
  "kubb add",
675
976
  "kubb add plugin-zod",
@@ -706,168 +1007,315 @@ function cancelAndExit(message = "Operation canceled.") {
706
1007
  _clack_prompts.cancel(message);
707
1008
  node_process.default.exit(0);
708
1009
  }
1010
+ function unwrap(value) {
1011
+ if (_clack_prompts.isCancel(value)) cancelAndExit();
1012
+ return value;
1013
+ }
1014
+ /** The full `kubb.config.ts` for `choices`, with each plugin added the way `kubb add` adds it. */
1015
+ async function buildConfig({ inputPath, outputPath, plugins }) {
1016
+ const { applyConfigEdits } = await import("@kubb/studio");
1017
+ const { source, outcomes } = applyConfigEdits({
1018
+ source: generateConfigFile({
1019
+ inputPath,
1020
+ outputPath
1021
+ }),
1022
+ edits: plugins.map((entry) => ({
1023
+ operation: "add-plugin",
1024
+ plugin: entry.artifact.package
1025
+ })),
1026
+ extensions: new Map(plugins.map(({ artifact, manifest }) => [artifact.package, {
1027
+ package: artifact.package,
1028
+ version: artifact.version,
1029
+ manifest
1030
+ }]))
1031
+ });
1032
+ const failed = outcomes.find((outcome) => !outcome.applied);
1033
+ if (failed && failed.edit.operation === "add-plugin") throw new _kubb_core.Diagnostics.Error({
1034
+ code: _kubb_core.Diagnostics.code.unknown,
1035
+ severity: "error",
1036
+ message: `Could not add ${failed.edit.plugin} to ${require_config.KUBB_CONFIG_FILENAME}: ${failed.reason}`,
1037
+ help: `Run kubb init without that plugin, then add it with kubb add.`
1038
+ });
1039
+ return source;
1040
+ }
1041
+ /** Generates from the config at `configPath` without reporters, and returns how many files it wrote. */
1042
+ async function firstRun(configPath) {
1043
+ const { configs } = await require_config.getConfigs({ configPath });
1044
+ let files = 0;
1045
+ for (const config of configs) {
1046
+ const result = await (0, _kubb_core.createKubb)(config).generate();
1047
+ if (!result.success) throw new Error(result.diagnostics.flatMap((diagnostic) => diagnostic.severity === "error" ? [diagnostic.message] : []).join("\n"));
1048
+ files += result.files.length;
1049
+ }
1050
+ return files;
1051
+ }
1052
+ /** The command that runs `kubb generate`: the `generate` script when it runs Kubb, otherwise `npx kubb generate`. */
1053
+ function generateCommand({ packageManager, script, watch }) {
1054
+ if (script !== "kubb generate") return `npx kubb generate${watch ? " --watch" : ""}`;
1055
+ const run = [
1056
+ packageManager.name,
1057
+ ...packageManager.runCommand,
1058
+ "generate"
1059
+ ].join(" ");
1060
+ if (!watch) return run;
1061
+ return packageManager.name === "npm" ? `${run} -- --watch` : `${run} --watch`;
1062
+ }
709
1063
  /**
710
- * Runs the interactive Kubb scaffolding wizard.
711
- * Detects the package manager, prompts for input/output paths and plugins, installs packages, and writes `kubb.config.ts`.
712
- * Pass `yes: true` to skip all prompts and use defaults.
1064
+ * Runs the Kubb setup wizard: detects the package manager and specs, asks for the spec, plugins, and
1065
+ * output folder, shows the config for review, then installs, writes `kubb.config.ts`, adds a
1066
+ * `generate` script, and runs the first generation.
1067
+ * Pass `yes: true` to skip all prompts and print one line per step.
713
1068
  */
714
1069
  async function run$3({ yes, input: inputFlag, output: outputFlag, plugins: pluginsFlag, dryRun }) {
715
1070
  const cwd = node_process.default.cwd();
716
- require_output.logIntro({ title: (0, node_util.styleText)("bgCyan", (0, node_util.styleText)("black", " Kubb Init ")) });
717
- /**
718
- * Returns `flag` when provided, the `defaultValue` when `yes` is set,
719
- * or calls `prompt()` for interactive input. Exits on cancellation.
720
- */
721
- async function resolveOrPrompt(flag, defaultValue, logLabel, prompt) {
722
- if (flag !== void 0) {
723
- require_output.logInfo(`${logLabel}: ${(0, node_util.styleText)("cyan", String(flag))}`);
724
- return flag;
725
- }
726
- if (yes) {
727
- require_output.logInfo(`${logLabel}: ${(0, node_util.styleText)("cyan", String(defaultValue))}`);
728
- return defaultValue;
1071
+ /** Runs `task` behind a spinner, or prints one line when it is done in `--yes` mode. */
1072
+ async function step({ start, task, done, failed }) {
1073
+ const spinner = require_utils.createSpinner();
1074
+ if (!yes) spinner.start(start);
1075
+ try {
1076
+ const result = await task();
1077
+ if (yes) console.log(`${(0, node_util.styleText)("green", "✓")} ${done(result)}`);
1078
+ else spinner.stop(done(result));
1079
+ return result;
1080
+ } catch (error) {
1081
+ if (!yes) spinner.error(failed);
1082
+ throw error;
729
1083
  }
730
- const result = await prompt();
731
- if (_clack_prompts.isCancel(result)) cancelAndExit();
732
- return result;
733
1084
  }
1085
+ if (!yes) require_text.logIntro({ title: require_text.isInteractiveRun({
1086
+ yes,
1087
+ dryRun: dryRun.enabled
1088
+ }) ? void 0 : "Set up Kubb" });
734
1089
  try {
735
- const plugins = (await require_registry.fetchRegistryIndex({ registry: require_registry.registryUrl() })).filter((entry) => entry.kind === "plugin" && !require_registry.incompatibility(entry.manifest));
736
- const byId = new Map(plugins.map((entry) => [entry.id, entry]));
737
- if (!node_fs.default.existsSync(node_path.default.join(cwd, "package.json"))) {
1090
+ const registry = require_registry.fetchRegistryIndex({ registry: require_registry.registryUrl() }).then((entries) => entries.filter((entry) => entry.kind === "plugin" && !require_registry.incompatibility(entry.manifest)));
1091
+ registry.catch(() => {});
1092
+ if (!yes) import("@kubb/studio").catch(() => {});
1093
+ if (!await require_utils.exists(node_path.default.join(cwd, "package.json"))) {
738
1094
  if (!yes) {
739
- const shouldInit = await _clack_prompts.confirm({
1095
+ if (!unwrap(await _clack_prompts.confirm({
740
1096
  message: "No package.json found. Would you like to create one?",
741
1097
  initialValue: true
742
- });
743
- if (_clack_prompts.isCancel(shouldInit) || !shouldInit) cancelAndExit();
1098
+ }))) cancelAndExit();
744
1099
  }
745
1100
  const packageManager = detectPackageManager(cwd);
746
- const spinner = require_output.createSpinner();
747
- spinner.start(`Initializing package.json with ${packageManager.name}`);
748
- await initPackageJson(cwd, packageManager);
749
- spinner.stop(`Created package.json with ${packageManager.name}`);
1101
+ await step({
1102
+ start: `Initializing package.json with ${packageManager.name}`,
1103
+ task: () => initPackageJson(cwd, packageManager),
1104
+ done: () => `Created package.json with ${packageManager.name}`,
1105
+ failed: "Could not create package.json"
1106
+ });
750
1107
  }
751
1108
  const packageManager = detectPackageManager(cwd);
752
- require_output.logInfo(`Detected package manager: ${(0, node_util.styleText)("cyan", packageManager.name)}`);
753
- const inputPath = await resolveOrPrompt(inputFlag, require_output.initDefaults.inputPath, "Using input path", () => _clack_prompts.text({
754
- message: "Where is your OpenAPI specification located?",
755
- placeholder: require_output.initDefaults.inputPath,
756
- defaultValue: require_output.initDefaults.inputPath,
757
- validate: (value) => {
758
- if (!value) return "Input path is required";
759
- }
760
- }));
761
- const outputPath = await resolveOrPrompt(outputFlag, require_output.initDefaults.outputPath, "Using output path", () => _clack_prompts.text({
762
- message: "Where should the generated files be output?",
763
- placeholder: require_output.initDefaults.outputPath,
764
- defaultValue: require_output.initDefaults.outputPath,
765
- validate: (value) => {
766
- if (!value) return "Output path is required";
767
- }
768
- }));
769
- const defaultPlugins = require_output.initDefaults.plugins.flatMap((slug) => byId.get(require_registry.toRegistryId(slug)) ?? []);
770
- const pluginLabel = (entries) => (0, node_util.styleText)("cyan", entries.map((entry) => entry.manifest.displayName).join(", "));
771
- const selectedPlugins = await (async () => {
772
- if (pluginsFlag) {
773
- const ids = pluginsFlag.split(",").map((id) => id.trim()).filter(Boolean);
774
- const unknown = ids.filter((id) => !byId.has(require_registry.toRegistryId(id)));
775
- if (unknown.length) throw new _kubb_core.Diagnostics.Error({
776
- code: _kubb_core.Diagnostics.code.pluginNotFound,
777
- severity: "error",
778
- message: `Not in the Kubb registry: ${unknown.join(", ")}. Browse the plugins at https://kubb.dev/extensions.`,
779
- help: "Use plugin ids from https://kubb.dev/extensions, such as plugin-ts."
780
- });
781
- const entries = ids.map((id) => byId.get(require_registry.toRegistryId(id)));
782
- require_output.logInfo(`Using plugins: ${pluginLabel(entries)}`);
783
- return entries;
784
- }
785
- if (yes) {
786
- require_output.logInfo(`Using plugins: ${pluginLabel(defaultPlugins)}`);
787
- return defaultPlugins;
1109
+ const specs = await findSpecFiles(cwd);
1110
+ if (!yes) {
1111
+ require_text.logStep(`Package manager ${packageManager.name}`);
1112
+ if (specs[0]) {
1113
+ const { version: specVersion, operations } = await readSpecInfo(node_path.default.join(cwd, specs[0]));
1114
+ const details = [
1115
+ specs[0].slice(2),
1116
+ specVersion ? `${specVersion.startsWith("2") ? "Swagger" : "OpenAPI"} ${specVersion}` : void 0,
1117
+ operations === void 0 ? void 0 : require_text.pluralize(operations, "operation"),
1118
+ specs.length > 1 ? `+${specs.length - 1} more` : void 0
1119
+ ];
1120
+ require_text.logStep(`Found ${details.filter(Boolean).join(" · ")}`);
788
1121
  }
789
- const values = await _clack_prompts.multiselect({
790
- message: "Select plugins to use:",
1122
+ }
1123
+ const plugins = pluginsFlag || yes ? await registry : void 0;
1124
+ const defaultPlugins = (entries) => require_config.initDefaults.plugins.flatMap((id) => findPlugin({
1125
+ plugins: entries,
1126
+ id
1127
+ }) ?? []);
1128
+ const flagPlugins = pluginsFlag && plugins ? parsePluginsFlag({
1129
+ plugins,
1130
+ value: pluginsFlag
1131
+ }) : void 0;
1132
+ let choices = {
1133
+ inputPath: inputFlag ?? specs[0] ?? require_config.initDefaults.inputPath,
1134
+ outputPath: outputFlag ?? require_config.initDefaults.outputPath,
1135
+ plugins: flagPlugins ?? (plugins ? defaultPlugins(plugins) : [])
1136
+ };
1137
+ if (yes && !choices.plugins.length) throw new Error("The registry has no default plugin. Pass --plugins to choose one.");
1138
+ async function askInput(current) {
1139
+ const choice = specs.length ? unwrap(await _clack_prompts.select({
1140
+ message: "Which spec should Kubb read?",
1141
+ options: [
1142
+ ...specs.map((spec) => ({
1143
+ value: spec,
1144
+ label: spec,
1145
+ hint: "found in this repo"
1146
+ })),
1147
+ {
1148
+ value: "url",
1149
+ label: "A URL"
1150
+ },
1151
+ {
1152
+ value: "file",
1153
+ label: "Another file"
1154
+ }
1155
+ ],
1156
+ initialValue: specs.includes(current) ? current : /^https?:\/\//.test(current) ? "url" : "file"
1157
+ })) : /^https?:\/\//.test(current) ? "url" : "file";
1158
+ if (specs.includes(choice)) return choice;
1159
+ const isUrl = choice === "url";
1160
+ const initial = isUrl === /^https?:\/\//.test(current) && !specs.includes(current) ? current : void 0;
1161
+ return unwrap(await _clack_prompts.text({
1162
+ message: isUrl ? "Spec URL" : "Spec path",
1163
+ placeholder: isUrl ? "https://example.com/openapi.yaml" : require_config.initDefaults.inputPath,
1164
+ initialValue: initial,
1165
+ defaultValue: isUrl ? void 0 : require_config.initDefaults.inputPath,
1166
+ validate: (value) => {
1167
+ if (isUrl && !/^https?:\/\//.test(value ?? "")) return "Enter an http(s) URL";
1168
+ }
1169
+ }));
1170
+ }
1171
+ async function askPlugins(previous) {
1172
+ const plugins = await registry;
1173
+ const current = previous.length ? previous : defaultPlugins(plugins);
1174
+ const width = Math.max(...plugins.map((entry) => entry.manifest.displayName.length));
1175
+ const nameWidth = Math.max(...plugins.map((entry) => entry.manifest.name.length));
1176
+ const values = unwrap(await _clack_prompts.multiselect({
1177
+ message: "What should Kubb generate?",
791
1178
  options: plugins.map(({ id, manifest }) => ({
792
1179
  value: id,
793
- label: manifest.displayName,
794
- hint: manifest.category
1180
+ label: `${manifest.displayName.padEnd(width)} ${(0, node_util.styleText)("dim", manifest.name.padEnd(nameWidth))}`.trimEnd()
795
1181
  })),
796
- initialValues: defaultPlugins.map((entry) => entry.id),
1182
+ initialValues: current.map((entry) => entry.id),
797
1183
  required: true
798
- });
799
- if (_clack_prompts.isCancel(values)) cancelAndExit();
1184
+ }));
800
1185
  return plugins.filter((entry) => values.includes(entry.id));
801
- })();
802
- const missing = missingPluginDependencies({
803
- selected: selectedPlugins.map((entry) => entry.manifest.name),
804
- manifests: selectedPlugins.map((entry) => entry.manifest)
805
- });
1186
+ }
1187
+ const answered = yes || Boolean(inputFlag && outputFlag && pluginsFlag);
1188
+ if (!answered) {
1189
+ let firstPass = true;
1190
+ while (true) {
1191
+ choices = {
1192
+ inputPath: firstPass && inputFlag ? inputFlag : await askInput(choices.inputPath),
1193
+ plugins: firstPass && flagPlugins ? flagPlugins : await askPlugins(choices.plugins),
1194
+ outputPath: firstPass && outputFlag ? outputFlag : unwrap(await _clack_prompts.text({
1195
+ message: "Output folder",
1196
+ placeholder: choices.outputPath,
1197
+ defaultValue: choices.outputPath
1198
+ }))
1199
+ };
1200
+ firstPass = false;
1201
+ const missing = missingPluginDependencies(choices.plugins.map((entry) => entry.manifest));
1202
+ if (missing.length) {
1203
+ require_text.logWarn(`${missing.join("\n")}\nAdd the missing plugins.`);
1204
+ continue;
1205
+ }
1206
+ const config = await buildConfig(choices);
1207
+ const packages = [require_config.KUBB_PACKAGE_NAME, ...choices.plugins.map((entry) => entry.artifact.package)];
1208
+ require_text.logStep(`Review ${require_config.KUBB_CONFIG_FILENAME}`);
1209
+ require_text.logBlock([
1210
+ ...config.trimEnd().split("\n"),
1211
+ "",
1212
+ `${(0, node_util.styleText)("dim", "Installs")} ${packages.join(" ")} with ${packageManager.name}`
1213
+ ]);
1214
+ const configExists = await require_utils.exists(node_path.default.join(cwd, require_config.KUBB_CONFIG_FILENAME));
1215
+ if (unwrap(await _clack_prompts.select({
1216
+ message: configExists ? `kubb.config.ts exists. Overwrite it?` : "Create it?",
1217
+ options: [{
1218
+ value: true,
1219
+ label: "Yes"
1220
+ }, {
1221
+ value: false,
1222
+ label: "Change a choice"
1223
+ }]
1224
+ }))) break;
1225
+ }
1226
+ }
1227
+ const missing = missingPluginDependencies(choices.plugins.map((entry) => entry.manifest));
806
1228
  if (missing.length) throw new _kubb_core.Diagnostics.Error({
807
1229
  code: _kubb_core.Diagnostics.code.missingDependency,
808
1230
  severity: "error",
809
1231
  message: `${missing.join("\n")}\nAdd the missing plugins and run kubb init again.`
810
1232
  });
811
- const packagesToInstall = [`${require_output.KUBB_PACKAGE_NAME}@${require_Telemetry.version}`, ...selectedPlugins.map((entry) => require_registry.installSpec(entry.artifact))];
812
- const spinner = require_output.createSpinner();
813
- spinner.start(`Installing ${packagesToInstall.length} packages with ${packageManager.name}`);
814
- try {
815
- await dryRun.run(() => installPackages(packagesToInstall, packageManager, cwd), { message: `install ${packagesToInstall.length} packages with ${packageManager.name}` });
816
- spinner.stop(`Installed ${packagesToInstall.length} packages`);
817
- } catch (error) {
818
- spinner.stop("Installation failed");
819
- throw error;
1233
+ const configPath = node_path.default.join(cwd, require_config.KUBB_CONFIG_FILENAME);
1234
+ if (answered && !yes && await require_utils.exists(configPath)) {
1235
+ if (!unwrap(await _clack_prompts.confirm({
1236
+ message: `kubb.config.ts exists. Overwrite it?`,
1237
+ initialValue: false
1238
+ }))) cancelAndExit("Keeping existing configuration.");
820
1239
  }
821
- const configSpinner = require_output.createSpinner();
822
- configSpinner.start(`Creating ${require_output.KUBB_CONFIG_FILENAME}`);
823
- const configContent = generateConfigFile({
824
- inputPath,
825
- outputPath
1240
+ const packagesToInstall = [`${require_config.KUBB_PACKAGE_NAME}@${require_Telemetry.version}`, ...choices.plugins.map((entry) => require_registry.installSpec(entry.artifact))];
1241
+ const packageNames = [require_config.KUBB_PACKAGE_NAME, ...choices.plugins.map((entry) => entry.artifact.package)];
1242
+ const dry = { message: `install ${packagesToInstall.length} packages with ${packageManager.name}` };
1243
+ if (yes) await step({
1244
+ start: "",
1245
+ task: async () => void await dryRun.run(() => installPackages(packagesToInstall, packageManager, cwd), dry),
1246
+ done: () => `Installed ${packageNames.join(" ")} with ${packageManager.name}`,
1247
+ failed: "Installation failed"
826
1248
  });
827
- const configPath = node_path.default.join(cwd, require_output.KUBB_CONFIG_FILENAME);
828
- if (node_fs.default.existsSync(configPath)) {
829
- configSpinner.stop(`${require_output.KUBB_CONFIG_FILENAME} already exists`);
830
- if (!yes) {
831
- const shouldOverwrite = await _clack_prompts.confirm({
832
- message: `${require_output.KUBB_CONFIG_FILENAME} already exists. Overwrite?`,
833
- initialValue: false
834
- });
835
- if (_clack_prompts.isCancel(shouldOverwrite) || !shouldOverwrite) cancelAndExit("Keeping existing configuration. Packages have been installed.");
836
- }
837
- configSpinner.start(`Overwriting ${require_output.KUBB_CONFIG_FILENAME}`);
838
- }
839
- await dryRun.run(() => node_fs.default.promises.writeFile(configPath, configContent, "utf-8"), { message: `write ${require_output.KUBB_CONFIG_FILENAME}` });
840
- for (const entry of selectedPlugins) {
841
- const reason = await dryRun.run(() => addToConfig({
842
- detail: entry,
843
- configFile: configPath
1249
+ else await installWithSpinner({
1250
+ packages: packagesToInstall,
1251
+ names: `${packagesToInstall.length} packages`,
1252
+ packageManager,
1253
+ cwd,
1254
+ install: async (packages, packageManager, cwd, onLine) => void await dryRun.run(() => installPackages(packages, packageManager, cwd, onLine), dry)
1255
+ });
1256
+ await step({
1257
+ start: `Writing ${require_config.KUBB_CONFIG_FILENAME}`,
1258
+ task: async () => dryRun.run(async () => void await require_utils.write(configPath, await buildConfig(choices)), { message: `write ${require_config.KUBB_CONFIG_FILENAME}` }),
1259
+ done: () => `Wrote ${require_config.KUBB_CONFIG_FILENAME}`,
1260
+ failed: `Could not write ${require_config.KUBB_CONFIG_FILENAME}`
1261
+ });
1262
+ const scriptAdded = await dryRun.run(() => addGenerateScript(cwd), {
1263
+ message: "add a generate script to package.json",
1264
+ resolve: async () => await generateScript(cwd) === void 0
1265
+ });
1266
+ if (scriptAdded && !yes) require_text.logStep("Added \"generate\": \"kubb generate\" to package.json");
1267
+ const script = scriptAdded ? "kubb generate" : await generateScript(cwd);
1268
+ const runStart = node_process.default.hrtime();
1269
+ try {
1270
+ await dryRun.run(() => step({
1271
+ start: "Running the first generation",
1272
+ task: () => firstRun(configPath),
1273
+ done: (files) => yes ? `Generated ${require_text.pluralize(files, "file")} in ${choices.outputPath}` : `First run ${require_text.pluralize(files, "file")} in ${choices.outputPath} · ${require_Telemetry.formatMs(require_Telemetry.getElapsedMs(runStart))}`,
1274
+ failed: "First run failed"
844
1275
  }), {
845
- message: `add ${entry.manifest.export}() to ${require_output.KUBB_CONFIG_FILENAME}`,
846
- resolve: () => void 0
847
- });
848
- if (reason) throw new _kubb_core.Diagnostics.Error({
849
- code: _kubb_core.Diagnostics.code.unknown,
850
- severity: "error",
851
- message: `Could not add ${entry.manifest.export}() to ${require_output.KUBB_CONFIG_FILENAME}: ${reason}`,
852
- help: `Add ${entry.manifest.export}() to the plugins array in ${require_output.KUBB_CONFIG_FILENAME} by hand.`
1276
+ message: "run the first generation",
1277
+ resolve: () => 0
853
1278
  });
1279
+ } catch (error) {
1280
+ require_text.logError(error instanceof Error ? error.message : String(error));
1281
+ const message = `Kubb is set up, but the first run failed. Fix the error, then run ${generateCommand({
1282
+ packageManager,
1283
+ script,
1284
+ watch: false
1285
+ })}.`;
1286
+ if (yes) console.log(message);
1287
+ else require_text.logOutro((0, node_util.styleText)("yellow", message));
1288
+ node_process.default.exitCode = 1;
1289
+ return;
854
1290
  }
855
- configSpinner.stop(`Created ${require_output.KUBB_CONFIG_FILENAME}`);
856
- require_output.logOutro((0, node_util.styleText)("green", "✓ All set!") + "\n\n" + (0, node_util.styleText)("dim", "Next steps:") + "\n" + (0, node_util.styleText)("cyan", ` 1. Make sure your OpenAPI spec is at: ${inputPath}`) + "\n" + (0, node_util.styleText)("cyan", " 2. Generate code with: npx kubb generate") + "\n" + (0, node_util.styleText)("cyan", ` 3. Find generated files in: ${outputPath}`) + "\n\n" + (0, node_util.styleText)("dim", `Using ${packageManager.name} • Kubb v${require_Telemetry.version}`));
1291
+ if (yes) {
1292
+ console.log(`Kubb is set up. Run ${generateCommand({
1293
+ packageManager,
1294
+ script,
1295
+ watch: false
1296
+ })} after changing the spec.`);
1297
+ return;
1298
+ }
1299
+ require_text.logOutro(`${(0, node_util.styleText)("green", "✓ Kubb is set up")}\n ${(0, node_util.styleText)("dim", "Next")} ${generateCommand({
1300
+ packageManager,
1301
+ script,
1302
+ watch: true
1303
+ })}`);
857
1304
  } catch (error) {
858
- require_output.logError((0, node_util.styleText)("red", "An error occurred during initialization"));
859
- if (error instanceof Error) require_output.logError(error.message);
1305
+ require_text.logError((0, node_util.styleText)("red", "An error occurred during initialization"));
1306
+ if (error instanceof Error) require_text.logError(error.message);
860
1307
  node_process.default.exit(1);
861
1308
  }
862
1309
  }
863
1310
  require_rolldown_runtime.__name(run$3, "run");
864
1311
  const command$2 = (0, gunshi.defineWithTypes)()({
865
1312
  name: "init",
866
- description: "Scaffold a kubb.config.ts and install plugins for code generation from an OpenAPI spec. Run without flags for interactive setup, or pass --input, --output, and --plugins to skip the prompts.",
1313
+ description: "Set up a config, install plugins, and run the first generation.",
867
1314
  examples: [
868
1315
  "kubb init",
869
1316
  "kubb init --yes",
870
1317
  "kubb init --input ./openapi.yaml --output ./src/gen --plugins plugin-ts,plugin-zod",
1318
+ "kubb init --yes --input ./openapi.yaml --plugins ts,zod --output src/api",
871
1319
  "kubb init --plugins plugin-ts,plugin-axios,plugin-react-query",
872
1320
  "kubb init --yes --dry-run"
873
1321
  ].join("\n"),
@@ -875,7 +1323,7 @@ const command$2 = (0, gunshi.defineWithTypes)()({
875
1323
  args: {
876
1324
  yes: {
877
1325
  type: "boolean",
878
- description: "Skip prompts and use default options",
1326
+ description: "Skip prompts and use the detected spec, the default plugin, and the default output folder",
879
1327
  short: "y",
880
1328
  default: false
881
1329
  },
@@ -893,7 +1341,7 @@ const command$2 = (0, gunshi.defineWithTypes)()({
893
1341
  },
894
1342
  plugins: {
895
1343
  type: "string",
896
- description: "Comma-separated Kubb registry ids of the plugins to add, the same ids `kubb add` takes (plugin-ts, plugin-zod, acme/plugin-x)",
1344
+ description: "Comma-separated Kubb registry ids of the plugins to add, the same ids `kubb add` takes (plugin-ts, plugin-zod, acme/plugin-x), or short names (ts, zod)",
897
1345
  metavar: "plugin-ts,plugin-zod,..."
898
1346
  }
899
1347
  },
@@ -906,6 +1354,13 @@ const command$2 = (0, gunshi.defineWithTypes)()({
906
1354
  })
907
1355
  });
908
1356
  //#endregion
1357
+ //#region src/ui/table.ts
1358
+ /** Pads each column to its widest cell, one line per row. */
1359
+ function formatTable(rows) {
1360
+ const widths = Array.from({ length: Math.max(0, ...rows.map((row) => row.length)) }, (_, index) => Math.max(...rows.map((row) => row[index]?.length ?? 0)));
1361
+ return rows.map((row) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ").trimEnd());
1362
+ }
1363
+ //#endregion
909
1364
  //#region src/runners/list/run.ts
910
1365
  /**
911
1366
  * The installed packages that ship a `kubb-extension.json` or have a registry entry, with the
@@ -913,7 +1368,7 @@ const command$2 = (0, gunshi.defineWithTypes)()({
913
1368
  */
914
1369
  async function listExtensions({ cwd, registry }) {
915
1370
  const [installed, entries] = await Promise.all([(0, _kubb_core.readInstalledPackages)({ root: cwd }), require_registry.fetchRegistryIndex({ registry }).catch((error) => {
916
- require_output.logWarn(`Showing installed extensions only: ${require_Telemetry.getErrorMessage(error)}`);
1371
+ require_text.logWarn(`Showing installed extensions only: ${require_errors.getErrorMessage(error)}`);
917
1372
  return [];
918
1373
  })]);
919
1374
  const byPackage = new Map(entries.map((entry) => [entry.artifact.package, entry]));
@@ -941,110 +1396,218 @@ async function run$2() {
941
1396
  registry: require_registry.registryUrl()
942
1397
  });
943
1398
  if (!rows.length) {
944
- require_output.logOutro("No installed extensions found.");
1399
+ require_text.logOutro("No installed extensions found.");
945
1400
  return;
946
1401
  }
947
- const header = [
1402
+ const [head, ...body] = formatTable([[
948
1403
  "ID",
949
1404
  "PACKAGE",
950
1405
  "INSTALLED",
951
1406
  "REGISTRY",
952
1407
  "UPDATE"
953
- ];
954
- const cells = rows.map((row) => [
1408
+ ], ...rows.map((row) => [
955
1409
  row.id ?? "-",
956
1410
  row.package,
957
1411
  row.installed ?? "-",
958
1412
  row.registry ?? "-",
959
1413
  row.update ? "yes" : "no"
960
- ]);
961
- const widths = header.map((title, index) => Math.max(title.length, ...cells.map((cell) => cell[index].length)));
962
- const line = (cell) => cell.map((value, index) => value.padEnd(widths[index])).join(" ").trimEnd();
963
- require_output.logBlock([(0, node_util.styleText)("dim", line(header)), ...cells.map(line)]);
1414
+ ])]);
1415
+ require_text.logBlock([(0, node_util.styleText)("dim", head), ...body]);
964
1416
  } catch (error) {
965
- require_output.logError(require_Telemetry.getErrorMessage(error));
1417
+ require_text.logError(require_errors.getErrorMessage(error));
966
1418
  node_process.default.exitCode = 1;
967
1419
  }
968
1420
  }
969
1421
  require_rolldown_runtime.__name(run$2, "run");
970
1422
  const command$1 = (0, gunshi.define)({
971
1423
  name: "list",
972
- description: "List the installed Kubb extensions: their registry id, package, installed version, the version the Kubb registry pins, and whether an update exists.",
1424
+ description: "List installed extensions, their versions, and available updates.",
973
1425
  examples: "kubb list",
974
1426
  run: () => run$2()
975
1427
  });
976
1428
  //#endregion
977
1429
  //#region src/runners/update/run.ts
978
1430
  /**
979
- * Moves installed extensions to the version the registry pins. Lists the proposals, asks, then
980
- * installs them. With `check`, only lists them.
1431
+ * Generates every config in this process, the way `kubb generate` does, and returns the output files
1432
+ * it changed. Runs after the install, so the config loads the new versions. `undefined` when the
1433
+ * config fails to load or a build fails.
1434
+ */
1435
+ async function regenerate({ cwd, configPath }) {
1436
+ const spinner = require_utils.createSpinner();
1437
+ spinner.start("Generating with the new versions");
1438
+ try {
1439
+ const [{ getConfigs }, { generate }] = await Promise.all([Promise.resolve().then(() => require("./config-D63Gum4F.cjs")).then((n) => n.config_exports), Promise.resolve().then(() => require("./run-TrjCNL8e.cjs"))]);
1440
+ const { configs } = await getConfigs({ configPath });
1441
+ const dirs = configs.map((config) => node_path.default.resolve(config.root ?? cwd, config.output.path));
1442
+ const before = new Map((await Promise.all(dirs.map(require_utils.readOutputFiles))).flatMap((files) => [...files]));
1443
+ const hooks = new _kubb_core.Hookable();
1444
+ for (const config of configs) if (!await generate({
1445
+ config,
1446
+ hooks
1447
+ })) throw new Error("Generation failed");
1448
+ const after = new Map((await Promise.all(dirs.map(require_utils.readOutputFiles))).flatMap((files) => [...files]));
1449
+ const diff = require_utils.diffFiles(before, after);
1450
+ spinner.stop(diff.files.length ? `Generated: ${require_text.pluralize(diff.files.length, "file")} changed` : "Generated: no files changed");
1451
+ return {
1452
+ ...diff,
1453
+ output: dirs.map((dir) => node_path.default.relative(cwd, dir) || ".").join(", ")
1454
+ };
1455
+ } catch (error) {
1456
+ spinner.error(`Generation failed: ${require_errors.getErrorMessage(error)}`);
1457
+ return;
1458
+ }
1459
+ }
1460
+ /**
1461
+ * Moves installed extensions to the version the registry pins. Shows each version change with its
1462
+ * bump, asks which to install with majors left out, installs them, and regenerates to show which
1463
+ * files changed. With `dryRun`, only lists the updates and installs nothing.
981
1464
  */
982
- async function run$1({ ids, check, yes }) {
1465
+ async function run$1({ ids, dryRun, yes }) {
983
1466
  const cwd = node_process.default.cwd();
984
- require_output.logIntro({ title: (0, node_util.styleText)("bgCyan", (0, node_util.styleText)("black", " Kubb Update ")) });
1467
+ const hrStart = node_process.default.hrtime();
1468
+ const installedPackages = [];
1469
+ const summary = (title, status, lines) => require_summary.logCommandSummary({
1470
+ title,
1471
+ status,
1472
+ hrStart,
1473
+ block: true,
1474
+ lines
1475
+ });
1476
+ require_text.logIntro({ title: require_text.isInteractiveRun({
1477
+ yes,
1478
+ dryRun
1479
+ }) ? void 0 : (0, node_util.styleText)("bgCyan", (0, node_util.styleText)("black", " Kubb Update ")) });
985
1480
  try {
986
1481
  const wanted = new Set(ids.map(require_registry.toRegistryId));
987
- const [entries, installed] = await Promise.all([require_registry.fetchRegistryIndex({ registry: require_registry.registryUrl() }), (0, _kubb_core.readInstalledPackages)({ root: cwd })]);
1482
+ const [entries, installed] = await require_utils.withSpinner({
1483
+ message: "Checking the registry",
1484
+ promise: Promise.all([require_registry.fetchRegistryIndex({ registry: require_registry.registryUrl() }), (0, _kubb_core.readInstalledPackages)({ root: cwd })])
1485
+ });
988
1486
  const versions = new Map(installed.map((item) => [item.package, item.version]));
989
1487
  const tracked = entries.filter((entry) => versions.has(entry.artifact.package) && (!wanted.size || wanted.has(entry.id)));
990
- for (const id of wanted) if (!tracked.some((entry) => entry.id === id)) require_output.logWarn(`${id} is not installed, or not in the registry.`);
991
- const proposals = tracked.filter((entry) => {
992
- if (!require_registry.isOutdated({
993
- installed: versions.get(entry.artifact.package),
994
- latest: entry.artifact.version
995
- })) return false;
1488
+ for (const id of wanted) if (!tracked.some((entry) => entry.id === id)) require_text.logWarn(`${id} is not installed, or not in the registry.`);
1489
+ const current = ({ artifact }) => versions.get(artifact.package);
1490
+ const isMajor = (entry) => require_registry.bumpOf({
1491
+ installed: current(entry),
1492
+ latest: entry.artifact.version
1493
+ }) === "major";
1494
+ const outdated = tracked.filter((entry) => require_registry.isOutdated({
1495
+ installed: current(entry),
1496
+ latest: entry.artifact.version
1497
+ }));
1498
+ const proposals = outdated.filter((entry) => {
996
1499
  const problem = require_registry.incompatibility(entry.manifest);
997
- if (problem) require_output.logWarn(`Skipped ${entry.id}: ${problem}`);
1500
+ if (problem) require_text.logWarn(`Skipped ${entry.id}: ${problem}`);
998
1501
  return !problem;
999
1502
  });
1000
1503
  if (!proposals.length) {
1001
- require_output.logOutro(ids.length ? `No updates for ${ids.join(", ")}.` : "Every installed extension is up to date.");
1504
+ const title = outdated.length ? "No compatible updates available" : "Already up to date";
1505
+ summary(tracked.length ? title : "No installed extensions to update", outdated.length ? "warning" : "success", [` ${tracked.length - outdated.length} of ${tracked.length} up to date`, ...ids.length ? ["", `No updates for ${ids.join(", ")}.`] : []]);
1002
1506
  return;
1003
1507
  }
1004
- const change = ({ kind, artifact }) => `${kind} ${versions.get(artifact.package) ?? "not installed"} -> ${(0, node_util.styleText)("green", artifact.version)}`;
1005
- require_output.logBlock(proposals.map((entry) => `${entry.artifact.package} ${change(entry)}`));
1006
- if (check) {
1007
- const plural = proposals.length === 1 ? "" : "s";
1008
- require_output.logOutro(`${proposals.length} update${plural} available. Run \`kubb update\` to install.`);
1508
+ if (tracked.length) {
1509
+ const lines = formatTable([[
1510
+ "Extension",
1511
+ "Current",
1512
+ "Latest",
1513
+ ""
1514
+ ], ...tracked.map((entry) => {
1515
+ if (!outdated.includes(entry)) return [
1516
+ entry.artifact.package,
1517
+ current(entry) ?? "-",
1518
+ "up to date",
1519
+ ""
1520
+ ];
1521
+ const bump = require_registry.bumpOf({
1522
+ installed: current(entry),
1523
+ latest: entry.artifact.version
1524
+ }) ?? "";
1525
+ return [
1526
+ entry.artifact.package,
1527
+ current(entry) ?? "not installed",
1528
+ entry.artifact.version,
1529
+ bump === "major" ? "major · breaking" : bump
1530
+ ];
1531
+ })]);
1532
+ require_text.logStep(`Checked ${require_text.pluralize(tracked.length, "extension")} against the registry`);
1533
+ require_text.logBlock([(0, node_util.styleText)("dim", lines[0]), ...lines.slice(1).map((line, index) => {
1534
+ if (!outdated.includes(tracked[index])) return (0, node_util.styleText)("dim", line);
1535
+ return line.endsWith("major · breaking") ? (0, node_util.styleText)("yellow", line) : line;
1536
+ })]);
1537
+ }
1538
+ if (dryRun) {
1539
+ summary("Updates available", "planned", [`Dry run: nothing was installed. Run kubb update to install ${proposals.length === 1 ? "it" : "them"}.`]);
1009
1540
  return;
1010
1541
  }
1011
- let selected = proposals;
1012
- if (!yes) {
1013
- const chosen = await _clack_prompts.multiselect({
1014
- message: "Select the updates to install:",
1015
- options: proposals.map((entry) => ({
1016
- value: entry.id,
1017
- label: entry.artifact.package,
1018
- hint: change(entry)
1019
- })),
1020
- initialValues: proposals.map((entry) => entry.id),
1021
- required: true
1022
- });
1023
- if (_clack_prompts.isCancel(chosen)) {
1024
- _clack_prompts.cancel("Operation canceled.");
1025
- return;
1026
- }
1027
- selected = proposals.filter((entry) => chosen.includes(entry.id));
1542
+ const defaults = proposals.filter((entry) => !isMajor(entry));
1543
+ const chosen = yes ? defaults.map((entry) => entry.id) : await _clack_prompts.multiselect({
1544
+ message: "Select the updates to install",
1545
+ options: proposals.map((entry) => ({
1546
+ value: entry.id,
1547
+ label: entry.artifact.package,
1548
+ hint: `${current(entry) ?? "not installed"} → ${entry.artifact.version}`
1549
+ })),
1550
+ initialValues: defaults.map((entry) => entry.id),
1551
+ required: false
1552
+ });
1553
+ if (_clack_prompts.isCancel(chosen)) {
1554
+ summary("Update canceled", "canceled", [" No changes made"]);
1555
+ return;
1556
+ }
1557
+ const selected = proposals.filter((entry) => chosen.includes(entry.id));
1558
+ const skipped = proposals.filter((entry) => !selected.includes(entry)).map((entry) => {
1559
+ const kind = entry.manifest.kind.charAt(0).toUpperCase() + entry.manifest.kind.slice(1);
1560
+ return ` ${(0, node_util.styleText)("dim", "Skipped")} ${kind} ${entry.artifact.package} ${entry.artifact.version} ${(0, node_util.styleText)("dim", `run kubb update ${require_registry.toShortId(entry.id)} later`)}`;
1561
+ });
1562
+ if (!selected.length) {
1563
+ summary("No updates installed", "canceled", [" No updates selected.", ...skipped]);
1564
+ return;
1028
1565
  }
1029
- const plural = selected.length === 1 ? "" : "s";
1030
1566
  const packageManager = detectPackageManager(cwd);
1031
- const specs = selected.map(({ artifact }) => require_registry.installSpec(artifact));
1032
- require_output.logStep(`Installing ${specs.join(", ")} with ${packageManager.name}`);
1033
- await installPackages(specs, packageManager, cwd);
1034
- require_output.logInfo(`Updated ${specs.length} extension${plural}`);
1035
- require_output.logOutro((0, node_util.styleText)("green", "Done"));
1567
+ const names = selected.map(({ artifact }) => artifact.package).join(", ");
1568
+ await installWithSpinner({
1569
+ packages: selected.map(({ artifact }) => require_registry.installSpec(artifact)),
1570
+ names,
1571
+ packageManager,
1572
+ cwd
1573
+ });
1574
+ installedPackages.push(...selected.map((entry) => entry.artifact.package));
1575
+ const foundConfig = require_config.findConfigFile(cwd);
1576
+ const configPath = foundConfig && !yes && await _clack_prompts.confirm({
1577
+ message: "Regenerate now? This rewrites the generated files.",
1578
+ initialValue: true
1579
+ }) === true ? foundConfig : void 0;
1580
+ const changed = configPath ? await regenerate({
1581
+ cwd,
1582
+ configPath
1583
+ }) : void 0;
1584
+ const generationFailed = Boolean(configPath && !changed);
1585
+ const fileCounts = {
1586
+ A: 0,
1587
+ M: 0,
1588
+ D: 0
1589
+ };
1590
+ for (const file of changed?.files ?? []) fileCounts[file.status]++;
1591
+ summary(generationFailed ? "Extensions updated; generation failed" : "Update complete", generationFailed ? "warning" : "success", [
1592
+ ` Packages ${selected.length} installed with ${packageManager.name}${skipped.length ? ` · ${skipped.length} skipped` : ""}`,
1593
+ ...skipped,
1594
+ ...changed ? [` Files ${fileCounts.A} added · ${fileCounts.M} modified · ${fileCounts.D} deleted`] : [],
1595
+ ...!configPath ? [" Regeneration not run"] : [],
1596
+ ...generationFailed ? ["", "Fix the generation error, then run kubb generate."] : []
1597
+ ]);
1036
1598
  } catch (error) {
1037
- require_output.logError(require_Telemetry.getErrorMessage(error));
1599
+ require_text.logError(require_errors.getErrorMessage(error));
1600
+ summary("Update failed", "failed", installedPackages.length ? [` Packages ${installedPackages.join(", ")} installed`] : [" No changes made"]);
1038
1601
  node_process.default.exitCode = 1;
1039
1602
  }
1040
1603
  }
1041
1604
  require_rolldown_runtime.__name(run$1, "run");
1042
- const command = (0, gunshi.define)({
1605
+ const command = (0, gunshi.defineWithTypes)()({
1043
1606
  name: "update",
1044
- description: "Update installed extensions to the version the Kubb registry pins. Pass ids to update only those, or --check to list the updates without installing.",
1607
+ description: "Update installed extensions, regenerate, and summarize file changes.",
1045
1608
  examples: [
1046
1609
  "kubb update",
1047
- "kubb update --check",
1610
+ "kubb update --dry-run",
1048
1611
  "kubb update plugin-zod plugin-ts --yes"
1049
1612
  ].join("\n"),
1050
1613
  args: {
@@ -1053,16 +1616,11 @@ const command = (0, gunshi.define)({
1053
1616
  description: "Registry ids to update, all installed extensions when omitted",
1054
1617
  multiple: true
1055
1618
  },
1056
- check: {
1057
- type: "boolean",
1058
- description: "List the available updates without installing them",
1059
- default: false
1060
- },
1061
1619
  ...yesArg
1062
1620
  },
1063
- run: ({ values }) => run$1({
1621
+ run: ({ values, extensions }) => run$1({
1064
1622
  ids: values.ids ?? [],
1065
- check: values.check,
1623
+ dryRun: extensions[_gunshi_plugin_dryrun.pluginId].enabled,
1066
1624
  yes: values.yes
1067
1625
  })
1068
1626
  });
@@ -1083,7 +1641,7 @@ const QUIET_FLAGS = /* @__PURE__ */ new Set([
1083
1641
  async function run(argv = process.argv.slice(2)) {
1084
1642
  const args = routeStudioFlags(resolveDeprecatedFlags(argv));
1085
1643
  const isQuietFlag = args.some((arg) => QUIET_FLAGS.has(arg));
1086
- const isJsonReporter = args.some((arg, index) => /^--reporter=.*\bjson\b/.test(arg) || arg === "--reporter" && /\bjson\b/.test(args[index + 1] ?? ""));
1644
+ const isJsonOutput = args.includes("--json") || args.some((arg, index) => /^--reporter=.*\bjson\b/.test(arg) || arg === "--reporter" && /\bjson\b/.test(args[index + 1] ?? ""));
1087
1645
  if (!require_Telemetry.isDisabled() && !isQuietFlag) console.error(`${(0, node_util.styleText)("yellow", "Notice:")} Kubb collects anonymous telemetry data to help improve the tool. No personal data or file contents are collected. \nTo disable, set ${(0, node_util.styleText)("cyan", "KUBB_DISABLE_TELEMETRY=1")}.\n`);
1088
1646
  await (0, gunshi.cli)(args, command$13, {
1089
1647
  name: "kubb",
@@ -1100,7 +1658,14 @@ async function run(argv = process.argv.slice(2)) {
1100
1658
  list: command$1
1101
1659
  },
1102
1660
  fallbackToEntry: true,
1103
- ...isJsonReporter ? { renderHeader: null } : {},
1661
+ renderHeader: isJsonOutput ? null : async (ctx) => {
1662
+ if (!require_registry.wantsMascot({
1663
+ commandPath: ctx.commandPath,
1664
+ values: ctx.values
1665
+ })) return "";
1666
+ ctx.log(require_registry.getBanner(require_Telemetry.version, ctx.commandPath.join(" ") || "generate"));
1667
+ return "";
1668
+ },
1104
1669
  strict: true,
1105
1670
  plugins: [(0, _gunshi_plugin_dryrun.default)({ name: "dry-run" })],
1106
1671
  onErrorCommand: async (_ctx, error) => {