@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.cjs CHANGED
@@ -220,7 +220,7 @@ function getElapsedMs(hrStart) {
220
220
  }
221
221
  //#endregion
222
222
  //#region package.json
223
- var version = "5.11.4";
223
+ var version = "5.11.6";
224
224
  //#endregion
225
225
  //#region src/resolveConfig.ts
226
226
  /**
@@ -1082,13 +1082,17 @@ function applySet(source, call, path, value, quote) {
1082
1082
  }
1083
1083
  return { source: splice(source, rangeOf(target.value).start, rangeOf(target.value).end, printValue(value, quote)) };
1084
1084
  }
1085
- /** Deletes the option at `path`, falling the plugin back to its default for it. */
1085
+ /**
1086
+ * Deletes the option at `path`, falling the plugin back to its default for it. An option the file
1087
+ * does not set already has its default, so removing it changes nothing.
1088
+ */
1086
1089
  function applyRemove(source, call, path) {
1087
1090
  const options = call.arguments[0];
1088
- if (options?.type !== "ObjectExpression") return { reason: "the factory call has no options to remove" };
1091
+ if (!options) return { source };
1092
+ if (options.type !== "ObjectExpression") return { reason: "the factory was not called with an object literal" };
1089
1093
  const target = walk(options, path);
1090
1094
  if ("reason" in target) return target;
1091
- if ("missing" in target) return { reason: `${path.join(".")} is not set` };
1095
+ if ("missing" in target) return { source };
1092
1096
  return { source: removeMember(source, target.object, target.index) };
1093
1097
  }
1094
1098
  /**
@@ -1128,6 +1132,22 @@ function applyAddPlugin({ source, parsed, config, edit, quote, extensions }) {
1128
1132
  const next = insertItem(source, plugins, `${importName}(${Object.keys(options).length ? printValue(options, quote) : ""})`);
1129
1133
  return { source: taken ? next : insertImport(next, parsed, importName, edit.plugin) };
1130
1134
  }
1135
+ /**
1136
+ * Rewrites the config's `plugins` array with one item per line, so a call can be commented out without
1137
+ * taking its neighbors with it. Not done when a comment sits between the items, which would have to move.
1138
+ */
1139
+ function expandPluginsArray(source, config) {
1140
+ const array = property(config, "plugins");
1141
+ if (array?.type !== "ArrayExpression") return void 0;
1142
+ const items = array.elements.flatMap((element) => element ? [element] : []);
1143
+ const { start, end } = rangeOf(array);
1144
+ const ranges = items.map(rangeOf);
1145
+ 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))];
1146
+ if (!items.length || gaps.some((gap) => !/^[\s,]*$/.test(gap))) return void 0;
1147
+ const lineStart = source.lastIndexOf("\n", start - 1) + 1;
1148
+ const indent = source.slice(lineStart).match(/^[ \t]*/)?.[0] ?? "";
1149
+ return splice(source, start, end, `[\n${ranges.map((range) => `${indent} ${source.slice(range.start, range.end)},`).join("\n")}\n${indent}]`);
1150
+ }
1131
1151
  /** Comments out a plugin call in place, keeping its options on disk so `enable-plugin` can restore them exactly. */
1132
1152
  function disablePlugin(source, mod, config, plugin) {
1133
1153
  const loc = factoryCalls(mod, config).find((entry) => entry.packageName === plugin)?.call.loc;
@@ -1155,9 +1175,17 @@ function applyEdit({ source, edit, quote, extensions }) {
1155
1175
  const parsed = parseConfigs(source);
1156
1176
  if ("reason" in parsed) return parsed;
1157
1177
  const ref = edit.config;
1158
- const config = typeof ref === "string" ? parsed.configs.find((entry) => configName(entry) === ref) : parsed.configs[ref ?? 0];
1178
+ const findConfig = (entries) => typeof ref === "string" ? entries.configs.find((entry) => configName(entry) === ref) : entries.configs[ref ?? 0];
1179
+ const config = findConfig(parsed);
1159
1180
  if (!config) return { reason: `no config entry found for ${JSON.stringify(ref)}` };
1160
- if (edit.operation === "disable-plugin") return disablePlugin(source, parsed.mod, config, edit.plugin);
1181
+ if (edit.operation === "disable-plugin") {
1182
+ const result = disablePlugin(source, parsed.mod, config, edit.plugin);
1183
+ if (!("reason" in result) || !result.reason.includes("shares a line")) return result;
1184
+ const expanded = expandPluginsArray(source, config);
1185
+ const reparsed = expanded === void 0 ? void 0 : parseConfigs(expanded);
1186
+ const reparsedConfig = reparsed && !("reason" in reparsed) ? findConfig(reparsed) : void 0;
1187
+ return expanded !== void 0 && reparsed && !("reason" in reparsed) && reparsedConfig ? disablePlugin(expanded, reparsed.mod, reparsedConfig, edit.plugin) : result;
1188
+ }
1161
1189
  if (edit.operation === "enable-plugin") return enablePlugin(source, config, edit.plugin);
1162
1190
  if (edit.operation === "add-plugin") return applyAddPlugin({
1163
1191
  source,
@@ -1979,7 +2007,7 @@ var Agent = class extends capnweb.RpcTarget {
1979
2007
  });
1980
2008
  return this.#store;
1981
2009
  }
1982
- async [Symbol.asyncDispose]() {
2010
+ async clear() {
1983
2011
  if (this.#sandbox) await (0, node_fs_promises.rm)((0, _kubb_core.resolveCacheDir)(this.#sandboxCacheRoot), {
1984
2012
  recursive: true,
1985
2013
  force: true
@@ -2009,10 +2037,12 @@ var Agent = class extends capnweb.RpcTarget {
2009
2037
  message: "Studio must call connect() before any other command"
2010
2038
  });
2011
2039
  }
2012
- #logCommand(command, info) {
2040
+ #logCommand(command, info, requestedBy, failed) {
2013
2041
  return emit(this.#options.hooks, "studio:command", {
2014
2042
  command,
2015
- info
2043
+ info,
2044
+ requestedBy,
2045
+ failed
2016
2046
  });
2017
2047
  }
2018
2048
  async #readConfigFileView(source) {
@@ -2039,19 +2069,25 @@ var Agent = class extends capnweb.RpcTarget {
2039
2069
  }
2040
2070
  async connect(info) {
2041
2071
  this.#session.info = info;
2042
- const { configPath, root, version: version$1, loadConfig, configFile } = this.#options;
2072
+ const { configPath, root, version: version$1, loadConfigs, configFile } = this.#options;
2043
2073
  const source = read(configFile);
2044
- const [config, file, extensions] = await Promise.all([
2045
- loadConfig(),
2074
+ const [configs, file, extensions] = await Promise.all([
2075
+ loadConfigs(),
2046
2076
  this.#readConfigFileView(source),
2047
2077
  this.#installedExtensions(source)
2048
2078
  ]);
2049
- const output = await this.#readDiskOutput(config).catch(async (error) => {
2050
- await this.#warn({ message: `Could not read the output directory: ${getErrorMessage(error)}` });
2051
- });
2079
+ const entries = [];
2080
+ for (const config of configs) {
2081
+ const disk = await this.#readDiskOutput(config).catch(async (error) => {
2082
+ await this.#warn({ message: `Could not read the output directory: ${getErrorMessage(error)}` });
2083
+ });
2084
+ entries.push({
2085
+ ...toEntryView(config, extensions),
2086
+ ...disk && { disk }
2087
+ });
2088
+ }
2052
2089
  this.#session.connected.resolve(info);
2053
2090
  return {
2054
- output,
2055
2091
  versions: {
2056
2092
  kubb: version,
2057
2093
  agent: version$1
@@ -2060,30 +2096,8 @@ var Agent = class extends capnweb.RpcTarget {
2060
2096
  config: {
2061
2097
  path: configPath,
2062
2098
  file,
2063
- plugins: config.plugins.map((plugin) => ({
2064
- name: toStudioName({
2065
- name: (0, _kubb_core.resolveMeta)(plugin).name,
2066
- extensions
2067
- }),
2068
- options: toJSONOptions(plugin.options)
2069
- })),
2070
- adapter: config.adapter && {
2071
- name: toStudioName({
2072
- name: (0, _kubb_core.resolveMeta)(config.adapter).name,
2073
- extensions,
2074
- kind: "adapter"
2075
- }),
2076
- options: toJSONOptions(config.adapter.options)
2077
- },
2078
- parsers: config.parsers.map((parser) => ({
2079
- name: toStudioName({
2080
- name: (0, _kubb_core.resolveMeta)(parser).name,
2081
- extensions,
2082
- kind: "parser"
2083
- }),
2084
- options: toJSONOptions(parser.options)
2085
- })),
2086
- extensions: [...extensions.values()]
2099
+ extensions: [...extensions.values()],
2100
+ configs: entries
2087
2101
  },
2088
2102
  permissions: this.#permissions
2089
2103
  };
@@ -2171,7 +2185,9 @@ var Agent = class extends capnweb.RpcTarget {
2171
2185
  const { jobId, config: patch } = input;
2172
2186
  const studioInput = this.#sandbox ? patch?.input : void 0;
2173
2187
  if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || (0, _kubb_core.getInputKind)(studioInput) !== "inline")) return this.#refuse({ message: require_protocol.AgentMessage.SANDBOX_INPUT_ONLY });
2174
- const [config, extensions] = await Promise.all([this.#options.loadConfig(), this.#installedExtensions()]);
2188
+ const [configs, extensions] = await Promise.all([this.#options.loadConfigs(), this.#installedExtensions()]);
2189
+ const config = pickConfig(configs, patch?.config);
2190
+ if (!config) return this.#refuse({ message: require_protocol.AgentMessage.CONFIG_ENTRY_NOT_FOUND });
2175
2191
  const [plugins, adapter, parsers] = await Promise.all([
2176
2192
  mergePlugins({
2177
2193
  diskPlugins: config.plugins,
@@ -2216,6 +2232,7 @@ var Agent = class extends capnweb.RpcTarget {
2216
2232
  allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
2217
2233
  }).catch(async (error) => {
2218
2234
  await this.#generations.drop(jobId);
2235
+ await this.#logCommand("generate", getErrorMessage(error), input.requestedBy, true);
2219
2236
  throw error;
2220
2237
  });
2221
2238
  const output = await this.#generations.keep({
@@ -2234,7 +2251,7 @@ var Agent = class extends capnweb.RpcTarget {
2234
2251
  output,
2235
2252
  inputBytes
2236
2253
  });
2237
- 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" : ""}`);
2254
+ 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);
2238
2255
  return {
2239
2256
  status: "success",
2240
2257
  files: [...files.paths],
@@ -2273,7 +2290,7 @@ var Agent = class extends capnweb.RpcTarget {
2273
2290
  extensions: await this.#installedExtensions()
2274
2291
  });
2275
2292
  const applied = outcomes.filter((outcome) => outcome.applied).length;
2276
- await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}`);
2293
+ await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}`, input.requestedBy);
2277
2294
  return {
2278
2295
  outcomes,
2279
2296
  changed
@@ -2465,7 +2482,7 @@ async function runSession({ options, openRpc, url, instanceId }) {
2465
2482
  await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
2466
2483
  }
2467
2484
  await active?.settled;
2468
- await agent[Symbol.asyncDispose]();
2485
+ await agent.clear();
2469
2486
  return {
2470
2487
  ...close,
2471
2488
  connected: session.info !== void 0
@@ -2474,6 +2491,55 @@ async function runSession({ options, openRpc, url, instanceId }) {
2474
2491
  /**
2475
2492
  * The connection loop with a swappable transport. {@link connectAgent} passes a real WebSocket.
2476
2493
  */
2494
+ /** The entry Studio named by position or `name`, else the first. `undefined` for one the file does not have. */
2495
+ function pickConfig(configs, ref) {
2496
+ if (typeof ref === "string") return configs.find((config) => config.name === ref);
2497
+ if (ref !== void 0 && !Number.isSafeInteger(ref)) return void 0;
2498
+ return configs[ref ?? 0];
2499
+ }
2500
+ /** The input path or URL Studio may show: no inline document, and no credentials from a URL. */
2501
+ function reportedInput(input) {
2502
+ if (typeof input !== "string") return void 0;
2503
+ const kind = (0, _kubb_core.getInputKind)(input);
2504
+ if (kind === "inline") return void 0;
2505
+ if (kind !== "url") return input;
2506
+ const url = new URL(input);
2507
+ url.username = "";
2508
+ url.password = "";
2509
+ return url.href;
2510
+ }
2511
+ /** A JSON copy of one resolved entry, each extension named by the package its manifest is in. */
2512
+ function toEntryView(config, extensions) {
2513
+ const input = reportedInput(config.input);
2514
+ return {
2515
+ ...config.name !== void 0 && { name: config.name },
2516
+ ...input !== void 0 && { input },
2517
+ output: config.output.path,
2518
+ plugins: config.plugins.map((plugin) => ({
2519
+ name: toStudioName({
2520
+ name: (0, _kubb_core.resolveMeta)(plugin).name,
2521
+ extensions
2522
+ }),
2523
+ options: toJSONOptions(plugin.options)
2524
+ })),
2525
+ adapter: config.adapter && {
2526
+ name: toStudioName({
2527
+ name: (0, _kubb_core.resolveMeta)(config.adapter).name,
2528
+ extensions,
2529
+ kind: "adapter"
2530
+ }),
2531
+ options: toJSONOptions(config.adapter.options)
2532
+ },
2533
+ parsers: config.parsers.map((parser) => ({
2534
+ name: toStudioName({
2535
+ name: (0, _kubb_core.resolveMeta)(parser).name,
2536
+ extensions,
2537
+ kind: "parser"
2538
+ }),
2539
+ options: toJSONOptions(parser.options)
2540
+ }))
2541
+ };
2542
+ }
2477
2543
  async function runAgent({ options, openRpc }) {
2478
2544
  const root = options.root ?? node_process.default.cwd();
2479
2545
  const resolved = {
@@ -2535,7 +2601,7 @@ async function runAgent({ options, openRpc }) {
2535
2601
  * const hooks = new Hookable<KubbHooks>()
2536
2602
  * hooks.hook('studio:connected', ({ agentSlug }) => console.log(`Connected as ${agentSlug}`))
2537
2603
  *
2538
- * await connectAgent({ token, configPath: 'kubb.config.ts', loadConfig, version: '1.0.0', hooks, signal })
2604
+ * await connectAgent({ token, configPath: 'kubb.config.ts', loadConfigs, version: '1.0.0', hooks, signal })
2539
2605
  * ```
2540
2606
  */
2541
2607
  function connectAgent(options) {
@@ -2933,6 +2999,7 @@ exports.InvalidAgentTokenError = InvalidAgentTokenError;
2933
2999
  exports.PairingCanceledError = PairingCanceledError;
2934
3000
  exports.PairingDeniedError = PairingDeniedError;
2935
3001
  exports.PairingExpiredError = PairingExpiredError;
3002
+ exports.applyConfigEdits = applyConfigEdits;
2936
3003
  exports.connectAgent = connectAgent;
2937
3004
  exports.createSnapshot = createSnapshot;
2938
3005
  exports.defaultStudioUrl = defaultStudioUrl;