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