@kubb/studio 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.
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { E as InstalledExtensions, N as PublishedSnapshot, U as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-a_DcTFba.cjs";
1
+ import { F as PublishedSnapshot, G as SnapshotUploadMetadata, O as InstalledExtensions, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-dCzUP4dH.cjs";
2
2
  import { Config, Hookable, KubbHooks } from "@kubb/core";
3
3
  //#region src/connect.d.ts
4
4
  /**
@@ -47,6 +47,14 @@ declare global {
47
47
  'studio:command': [ctx: {
48
48
  command: 'generate' | 'saveConfig' | 'readFiles' | 'readInput' | 'readDiff';
49
49
  info: string;
50
+ /**
51
+ * The Studio user who asked for the command, when Studio names one.
52
+ */
53
+ requestedBy?: string;
54
+ /**
55
+ * Set when a generation failed. Its diagnostics went out through `kubb:generation:end`.
56
+ */
57
+ failed?: boolean;
50
58
  }];
51
59
  /**
52
60
  * A refused or ignored request, with the missing permission so the host can add a remedy.
@@ -72,9 +80,10 @@ type ConnectAgentOptions = {
72
80
  */
73
81
  configPath: string;
74
82
  /**
75
- * Called for every handshake and every generation, so edits on disk show up.
83
+ * Called for every handshake and every generation, so edits on disk show up. One entry per
84
+ * `defineConfig(...)` entry, and a generation names the one it runs.
76
85
  */
77
- loadConfig: () => Promise<Config>;
86
+ loadConfigs: () => Promise<Array<Config>>;
78
87
  /**
79
88
  * The host's own version, reported to Studio next to the runtime's.
80
89
  */
@@ -114,7 +123,7 @@ type ConnectAgentOptions = {
114
123
  * const hooks = new Hookable<KubbHooks>()
115
124
  * hooks.hook('studio:connected', ({ agentSlug }) => console.log(`Connected as ${agentSlug}`))
116
125
  *
117
- * await connectAgent({ token, configPath: 'kubb.config.ts', loadConfig, version: '1.0.0', hooks, signal })
126
+ * await connectAgent({ token, configPath: 'kubb.config.ts', loadConfigs, version: '1.0.0', hooks, signal })
118
127
  * ```
119
128
  */
120
129
  export declare function connectAgent(options: ConnectAgentOptions): Promise<void>;
@@ -242,6 +251,29 @@ export declare function createSnapshot({ studioUrl, apiKey, config, hooks, signa
242
251
  export declare const defaultStudioUrl = "https://kubb.studio";
243
252
  //#endregion
244
253
  //#region src/configFile.d.ts
254
+ /**
255
+ * Applies edits to a `kubb.config.ts` in place. Each edit rewrites only the text it targets, so
256
+ * comments, formatting, and hand-written code around the config survive.
257
+ *
258
+ * Edits are independent: one that cannot be applied is reported in `outcomes` and the rest still run.
259
+ *
260
+ * @example
261
+ * ```ts
262
+ * const { source, outcomes } = applyConfigEdits({
263
+ * source: current,
264
+ * edits: [{ operation: 'set', plugin: '@kubb/plugin-ts', path: ['enum', 'type'], value: 'enum' }],
265
+ * })
266
+ * ```
267
+ */
268
+ export declare function applyConfigEdits({ source, edits, extensions }: {
269
+ source: string;
270
+ edits: Array<ConfigEdit>;
271
+ extensions?: InstalledExtensions;
272
+ }): {
273
+ source: string;
274
+ outcomes: Array<ConfigEditOutcome>;
275
+ changed: boolean;
276
+ };
245
277
  /**
246
278
  * Applies `edits` to the config file at `filePath`, writing it only when something changed.
247
279
  * `add-plugin` accepts `@kubb/plugin-*` packages and the plugins in `extensions`.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { E as InstalledExtensions, N as PublishedSnapshot, U as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-a_DcTFba.js";
1
+ import { F as PublishedSnapshot, G as SnapshotUploadMetadata, O as InstalledExtensions, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-dCzUP4dH.js";
2
2
  import { Config, Hookable, KubbHooks } from "@kubb/core";
3
3
  //#region src/connect.d.ts
4
4
  /**
@@ -47,6 +47,14 @@ declare global {
47
47
  'studio:command': [ctx: {
48
48
  command: 'generate' | 'saveConfig' | 'readFiles' | 'readInput' | 'readDiff';
49
49
  info: string;
50
+ /**
51
+ * The Studio user who asked for the command, when Studio names one.
52
+ */
53
+ requestedBy?: string;
54
+ /**
55
+ * Set when a generation failed. Its diagnostics went out through `kubb:generation:end`.
56
+ */
57
+ failed?: boolean;
50
58
  }];
51
59
  /**
52
60
  * A refused or ignored request, with the missing permission so the host can add a remedy.
@@ -72,9 +80,10 @@ type ConnectAgentOptions = {
72
80
  */
73
81
  configPath: string;
74
82
  /**
75
- * Called for every handshake and every generation, so edits on disk show up.
83
+ * Called for every handshake and every generation, so edits on disk show up. One entry per
84
+ * `defineConfig(...)` entry, and a generation names the one it runs.
76
85
  */
77
- loadConfig: () => Promise<Config>;
86
+ loadConfigs: () => Promise<Array<Config>>;
78
87
  /**
79
88
  * The host's own version, reported to Studio next to the runtime's.
80
89
  */
@@ -114,7 +123,7 @@ type ConnectAgentOptions = {
114
123
  * const hooks = new Hookable<KubbHooks>()
115
124
  * hooks.hook('studio:connected', ({ agentSlug }) => console.log(`Connected as ${agentSlug}`))
116
125
  *
117
- * await connectAgent({ token, configPath: 'kubb.config.ts', loadConfig, version: '1.0.0', hooks, signal })
126
+ * await connectAgent({ token, configPath: 'kubb.config.ts', loadConfigs, version: '1.0.0', hooks, signal })
118
127
  * ```
119
128
  */
120
129
  export declare function connectAgent(options: ConnectAgentOptions): Promise<void>;
@@ -242,6 +251,29 @@ export declare function createSnapshot({ studioUrl, apiKey, config, hooks, signa
242
251
  export declare const defaultStudioUrl = "https://kubb.studio";
243
252
  //#endregion
244
253
  //#region src/configFile.d.ts
254
+ /**
255
+ * Applies edits to a `kubb.config.ts` in place. Each edit rewrites only the text it targets, so
256
+ * comments, formatting, and hand-written code around the config survive.
257
+ *
258
+ * Edits are independent: one that cannot be applied is reported in `outcomes` and the rest still run.
259
+ *
260
+ * @example
261
+ * ```ts
262
+ * const { source, outcomes } = applyConfigEdits({
263
+ * source: current,
264
+ * edits: [{ operation: 'set', plugin: '@kubb/plugin-ts', path: ['enum', 'type'], value: 'enum' }],
265
+ * })
266
+ * ```
267
+ */
268
+ export declare function applyConfigEdits({ source, edits, extensions }: {
269
+ source: string;
270
+ edits: Array<ConfigEdit>;
271
+ extensions?: InstalledExtensions;
272
+ }): {
273
+ source: string;
274
+ outcomes: Array<ConfigEditOutcome>;
275
+ changed: boolean;
276
+ };
245
277
  /**
246
278
  * Applies `edits` to the config file at `filePath`, writing it only when something changed.
247
279
  * `add-plugin` accepts `@kubb/plugin-*` packages and the plugins in `extensions`.
package/dist/index.js CHANGED
@@ -196,7 +196,7 @@ function getElapsedMs(hrStart) {
196
196
  }
197
197
  //#endregion
198
198
  //#region package.json
199
- var version = "5.11.4";
199
+ var version = "5.11.6";
200
200
  //#endregion
201
201
  //#region src/resolveConfig.ts
202
202
  /**
@@ -1058,13 +1058,17 @@ function applySet(source, call, path, value, quote) {
1058
1058
  }
1059
1059
  return { source: splice(source, rangeOf(target.value).start, rangeOf(target.value).end, printValue(value, quote)) };
1060
1060
  }
1061
- /** Deletes the option at `path`, falling the plugin back to its default for it. */
1061
+ /**
1062
+ * Deletes the option at `path`, falling the plugin back to its default for it. An option the file
1063
+ * does not set already has its default, so removing it changes nothing.
1064
+ */
1062
1065
  function applyRemove(source, call, path) {
1063
1066
  const options = call.arguments[0];
1064
- if (options?.type !== "ObjectExpression") return { reason: "the factory call has no options to remove" };
1067
+ if (!options) return { source };
1068
+ if (options.type !== "ObjectExpression") return { reason: "the factory was not called with an object literal" };
1065
1069
  const target = walk(options, path);
1066
1070
  if ("reason" in target) return target;
1067
- if ("missing" in target) return { reason: `${path.join(".")} is not set` };
1071
+ if ("missing" in target) return { source };
1068
1072
  return { source: removeMember(source, target.object, target.index) };
1069
1073
  }
1070
1074
  /**
@@ -1104,6 +1108,22 @@ function applyAddPlugin({ source, parsed, config, edit, quote, extensions }) {
1104
1108
  const next = insertItem(source, plugins, `${importName}(${Object.keys(options).length ? printValue(options, quote) : ""})`);
1105
1109
  return { source: taken ? next : insertImport(next, parsed, importName, edit.plugin) };
1106
1110
  }
1111
+ /**
1112
+ * Rewrites the config's `plugins` array with one item per line, so a call can be commented out without
1113
+ * taking its neighbors with it. Not done when a comment sits between the items, which would have to move.
1114
+ */
1115
+ function expandPluginsArray(source, config) {
1116
+ const array = property(config, "plugins");
1117
+ if (array?.type !== "ArrayExpression") return void 0;
1118
+ const items = array.elements.flatMap((element) => element ? [element] : []);
1119
+ const { start, end } = rangeOf(array);
1120
+ const ranges = items.map(rangeOf);
1121
+ const gaps = [source.slice(start + 1, ranges[0]?.start ?? end - 1), ...ranges.map((range, index) => source.slice(range.end, ranges[index + 1]?.start ?? end - 1))];
1122
+ if (!items.length || gaps.some((gap) => !/^[\s,]*$/.test(gap))) return void 0;
1123
+ const lineStart = source.lastIndexOf("\n", start - 1) + 1;
1124
+ const indent = source.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
1125
+ return splice(source, start, end, `[\n${ranges.map((range) => `${indent} ${source.slice(range.start, range.end)},`).join("\n")}\n${indent}]`);
1126
+ }
1107
1127
  /** Comments out a plugin call in place, keeping its options on disk so `enable-plugin` can restore them exactly. */
1108
1128
  function disablePlugin(source, mod, config, plugin) {
1109
1129
  const loc = factoryCalls(mod, config).find((entry) => entry.packageName === plugin)?.call.loc;
@@ -1131,9 +1151,17 @@ function applyEdit({ source, edit, quote, extensions }) {
1131
1151
  const parsed = parseConfigs(source);
1132
1152
  if ("reason" in parsed) return parsed;
1133
1153
  const ref = edit.config;
1134
- const config = typeof ref === "string" ? parsed.configs.find((entry) => configName(entry) === ref) : parsed.configs[ref ?? 0];
1154
+ const findConfig = (entries) => typeof ref === "string" ? entries.configs.find((entry) => configName(entry) === ref) : entries.configs[ref ?? 0];
1155
+ const config = findConfig(parsed);
1135
1156
  if (!config) return { reason: `no config entry found for ${JSON.stringify(ref)}` };
1136
- if (edit.operation === "disable-plugin") return disablePlugin(source, parsed.mod, config, edit.plugin);
1157
+ if (edit.operation === "disable-plugin") {
1158
+ const result = disablePlugin(source, parsed.mod, config, edit.plugin);
1159
+ if (!("reason" in result) || !result.reason.includes("shares a line")) return result;
1160
+ const expanded = expandPluginsArray(source, config);
1161
+ const reparsed = expanded === void 0 ? void 0 : parseConfigs(expanded);
1162
+ const reparsedConfig = reparsed && !("reason" in reparsed) ? findConfig(reparsed) : void 0;
1163
+ return expanded !== void 0 && reparsed && !("reason" in reparsed) && reparsedConfig ? disablePlugin(expanded, reparsed.mod, reparsedConfig, edit.plugin) : result;
1164
+ }
1137
1165
  if (edit.operation === "enable-plugin") return enablePlugin(source, config, edit.plugin);
1138
1166
  if (edit.operation === "add-plugin") return applyAddPlugin({
1139
1167
  source,
@@ -1955,7 +1983,7 @@ var Agent = class extends RpcTarget {
1955
1983
  });
1956
1984
  return this.#store;
1957
1985
  }
1958
- async [Symbol.asyncDispose]() {
1986
+ async clear() {
1959
1987
  if (this.#sandbox) await rm(resolveCacheDir(this.#sandboxCacheRoot), {
1960
1988
  recursive: true,
1961
1989
  force: true
@@ -1985,10 +2013,12 @@ var Agent = class extends RpcTarget {
1985
2013
  message: "Studio must call connect() before any other command"
1986
2014
  });
1987
2015
  }
1988
- #logCommand(command, info) {
2016
+ #logCommand(command, info, requestedBy, failed) {
1989
2017
  return emit(this.#options.hooks, "studio:command", {
1990
2018
  command,
1991
- info
2019
+ info,
2020
+ requestedBy,
2021
+ failed
1992
2022
  });
1993
2023
  }
1994
2024
  async #readConfigFileView(source) {
@@ -2015,19 +2045,25 @@ var Agent = class extends RpcTarget {
2015
2045
  }
2016
2046
  async connect(info) {
2017
2047
  this.#session.info = info;
2018
- const { configPath, root, version: version$1, loadConfig, configFile } = this.#options;
2048
+ const { configPath, root, version: version$1, loadConfigs, configFile } = this.#options;
2019
2049
  const source = read(configFile);
2020
- const [config, file, extensions] = await Promise.all([
2021
- loadConfig(),
2050
+ const [configs, file, extensions] = await Promise.all([
2051
+ loadConfigs(),
2022
2052
  this.#readConfigFileView(source),
2023
2053
  this.#installedExtensions(source)
2024
2054
  ]);
2025
- const output = await this.#readDiskOutput(config).catch(async (error) => {
2026
- await this.#warn({ message: `Could not read the output directory: ${getErrorMessage(error)}` });
2027
- });
2055
+ const entries = [];
2056
+ for (const config of configs) {
2057
+ const disk = await this.#readDiskOutput(config).catch(async (error) => {
2058
+ await this.#warn({ message: `Could not read the output directory: ${getErrorMessage(error)}` });
2059
+ });
2060
+ entries.push({
2061
+ ...toEntryView(config, extensions),
2062
+ ...disk && { disk }
2063
+ });
2064
+ }
2028
2065
  this.#session.connected.resolve(info);
2029
2066
  return {
2030
- output,
2031
2067
  versions: {
2032
2068
  kubb: version,
2033
2069
  agent: version$1
@@ -2036,30 +2072,8 @@ var Agent = class extends RpcTarget {
2036
2072
  config: {
2037
2073
  path: configPath,
2038
2074
  file,
2039
- plugins: config.plugins.map((plugin) => ({
2040
- name: toStudioName({
2041
- name: resolveMeta(plugin).name,
2042
- extensions
2043
- }),
2044
- options: toJSONOptions(plugin.options)
2045
- })),
2046
- adapter: config.adapter && {
2047
- name: toStudioName({
2048
- name: resolveMeta(config.adapter).name,
2049
- extensions,
2050
- kind: "adapter"
2051
- }),
2052
- options: toJSONOptions(config.adapter.options)
2053
- },
2054
- parsers: config.parsers.map((parser) => ({
2055
- name: toStudioName({
2056
- name: resolveMeta(parser).name,
2057
- extensions,
2058
- kind: "parser"
2059
- }),
2060
- options: toJSONOptions(parser.options)
2061
- })),
2062
- extensions: [...extensions.values()]
2075
+ extensions: [...extensions.values()],
2076
+ configs: entries
2063
2077
  },
2064
2078
  permissions: this.#permissions
2065
2079
  };
@@ -2147,7 +2161,9 @@ var Agent = class extends RpcTarget {
2147
2161
  const { jobId, config: patch } = input;
2148
2162
  const studioInput = this.#sandbox ? patch?.input : void 0;
2149
2163
  if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || getInputKind(studioInput) !== "inline")) return this.#refuse({ message: AgentMessage.SANDBOX_INPUT_ONLY });
2150
- const [config, extensions] = await Promise.all([this.#options.loadConfig(), this.#installedExtensions()]);
2164
+ const [configs, extensions] = await Promise.all([this.#options.loadConfigs(), this.#installedExtensions()]);
2165
+ const config = pickConfig(configs, patch?.config);
2166
+ if (!config) return this.#refuse({ message: AgentMessage.CONFIG_ENTRY_NOT_FOUND });
2151
2167
  const [plugins, adapter, parsers] = await Promise.all([
2152
2168
  mergePlugins({
2153
2169
  diskPlugins: config.plugins,
@@ -2192,6 +2208,7 @@ var Agent = class extends RpcTarget {
2192
2208
  allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
2193
2209
  }).catch(async (error) => {
2194
2210
  await this.#generations.drop(jobId);
2211
+ await this.#logCommand("generate", getErrorMessage(error), input.requestedBy, true);
2195
2212
  throw error;
2196
2213
  });
2197
2214
  const output = await this.#generations.keep({
@@ -2210,7 +2227,7 @@ var Agent = class extends RpcTarget {
2210
2227
  output,
2211
2228
  inputBytes
2212
2229
  });
2213
- await this.#logCommand("generate", `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "staged on disk"}${studioInput !== void 0 ? ", from a Studio spec" : ""}`);
2230
+ await this.#logCommand("generate", `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "staged on disk"}${studioInput !== void 0 ? ", from a Studio spec" : ""}`, input.requestedBy);
2214
2231
  return {
2215
2232
  status: "success",
2216
2233
  files: [...files.paths],
@@ -2249,7 +2266,7 @@ var Agent = class extends RpcTarget {
2249
2266
  extensions: await this.#installedExtensions()
2250
2267
  });
2251
2268
  const applied = outcomes.filter((outcome) => outcome.applied).length;
2252
- await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}`);
2269
+ await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}`, input.requestedBy);
2253
2270
  return {
2254
2271
  outcomes,
2255
2272
  changed
@@ -2441,7 +2458,7 @@ async function runSession({ options, openRpc, url, instanceId }) {
2441
2458
  await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
2442
2459
  }
2443
2460
  await active?.settled;
2444
- await agent[Symbol.asyncDispose]();
2461
+ await agent.clear();
2445
2462
  return {
2446
2463
  ...close,
2447
2464
  connected: session.info !== void 0
@@ -2450,6 +2467,55 @@ async function runSession({ options, openRpc, url, instanceId }) {
2450
2467
  /**
2451
2468
  * The connection loop with a swappable transport. {@link connectAgent} passes a real WebSocket.
2452
2469
  */
2470
+ /** The entry Studio named by position or `name`, else the first. `undefined` for one the file does not have. */
2471
+ function pickConfig(configs, ref) {
2472
+ if (typeof ref === "string") return configs.find((config) => config.name === ref);
2473
+ if (ref !== void 0 && !Number.isSafeInteger(ref)) return void 0;
2474
+ return configs[ref ?? 0];
2475
+ }
2476
+ /** The input path or URL Studio may show: no inline document, and no credentials from a URL. */
2477
+ function reportedInput(input) {
2478
+ if (typeof input !== "string") return void 0;
2479
+ const kind = getInputKind(input);
2480
+ if (kind === "inline") return void 0;
2481
+ if (kind !== "url") return input;
2482
+ const url = new URL(input);
2483
+ url.username = "";
2484
+ url.password = "";
2485
+ return url.href;
2486
+ }
2487
+ /** A JSON copy of one resolved entry, each extension named by the package its manifest is in. */
2488
+ function toEntryView(config, extensions) {
2489
+ const input = reportedInput(config.input);
2490
+ return {
2491
+ ...config.name !== void 0 && { name: config.name },
2492
+ ...input !== void 0 && { input },
2493
+ output: config.output.path,
2494
+ plugins: config.plugins.map((plugin) => ({
2495
+ name: toStudioName({
2496
+ name: resolveMeta(plugin).name,
2497
+ extensions
2498
+ }),
2499
+ options: toJSONOptions(plugin.options)
2500
+ })),
2501
+ adapter: config.adapter && {
2502
+ name: toStudioName({
2503
+ name: resolveMeta(config.adapter).name,
2504
+ extensions,
2505
+ kind: "adapter"
2506
+ }),
2507
+ options: toJSONOptions(config.adapter.options)
2508
+ },
2509
+ parsers: config.parsers.map((parser) => ({
2510
+ name: toStudioName({
2511
+ name: resolveMeta(parser).name,
2512
+ extensions,
2513
+ kind: "parser"
2514
+ }),
2515
+ options: toJSONOptions(parser.options)
2516
+ }))
2517
+ };
2518
+ }
2453
2519
  async function runAgent({ options, openRpc }) {
2454
2520
  const root = options.root ?? process$1.cwd();
2455
2521
  const resolved = {
@@ -2511,7 +2577,7 @@ async function runAgent({ options, openRpc }) {
2511
2577
  * const hooks = new Hookable<KubbHooks>()
2512
2578
  * hooks.hook('studio:connected', ({ agentSlug }) => console.log(`Connected as ${agentSlug}`))
2513
2579
  *
2514
- * await connectAgent({ token, configPath: 'kubb.config.ts', loadConfig, version: '1.0.0', hooks, signal })
2580
+ * await connectAgent({ token, configPath: 'kubb.config.ts', loadConfigs, version: '1.0.0', hooks, signal })
2515
2581
  * ```
2516
2582
  */
2517
2583
  function connectAgent(options) {
@@ -2904,6 +2970,6 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
2904
2970
  return _data.snapshot;
2905
2971
  }
2906
2972
  //#endregion
2907
- export { IncompatibleAgentError, InvalidAgentTokenError, PairingCanceledError, PairingDeniedError, PairingExpiredError, connectAgent, createSnapshot, defaultStudioUrl, pairAgent, writeConfigEdits };
2973
+ export { IncompatibleAgentError, InvalidAgentTokenError, PairingCanceledError, PairingDeniedError, PairingExpiredError, applyConfigEdits, connectAgent, createSnapshot, defaultStudioUrl, pairAgent, writeConfigEdits };
2908
2974
 
2909
2975
  //# sourceMappingURL=index.js.map