@kubb/studio 5.12.1 → 5.12.3

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 { 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-CG1bPO_H.cjs";
1
+ import { J as SnapshotUploadMetadata, R as PublishedSnapshot, a as AgentConnectInfo, c as AgentPermissions, f as ConfigEditOutcome, j as InstalledExtensions, s as AgentPermission, u as ConfigEdit } from "./protocol-WYICLbSg.cjs";
2
2
  import { Config, Hookable, KubbHooks } from "@kubb/core";
3
3
  //#region src/connect.d.ts
4
4
  /**
@@ -45,7 +45,7 @@ declare global {
45
45
  * A finished command and what it did, such as `applied 2/3 edits to kubb.config.ts`.
46
46
  */
47
47
  'studio:command': [ctx: {
48
- command: 'generate' | 'saveConfig' | 'readFiles' | 'readInput' | 'readDiff';
48
+ command: 'generate' | 'saveConfig' | 'installExtension' | 'updateExtensions' | 'readFiles' | 'readInput' | 'readDiff';
49
49
  info: string;
50
50
  /**
51
51
  * The Studio user who asked for the command, when Studio names one.
@@ -97,6 +97,8 @@ type ConnectAgentOptions = {
97
97
  * What Studio may do in this project. Every permission is off unless granted.
98
98
  */
99
99
  permissions?: Partial<AgentPermissions>;
100
+ /** The CLI entry `installExtension` runs `kubb add` with. Found through the project's `kubb` or `@kubb/cli` dependency when omitted. */
101
+ kubbBin?: string;
100
102
  /**
101
103
  * Receives the `studio:*` hooks and the `kubb:*` hooks of every generation.
102
104
  */
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
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-CG1bPO_H.js";
1
+ import { J as SnapshotUploadMetadata, R as PublishedSnapshot, a as AgentConnectInfo, c as AgentPermissions, f as ConfigEditOutcome, j as InstalledExtensions, s as AgentPermission, u as ConfigEdit } from "./protocol-WYICLbSg.js";
2
2
  import { Config, Hookable, KubbHooks } from "@kubb/core";
3
3
  //#region src/connect.d.ts
4
4
  /**
@@ -45,7 +45,7 @@ declare global {
45
45
  * A finished command and what it did, such as `applied 2/3 edits to kubb.config.ts`.
46
46
  */
47
47
  'studio:command': [ctx: {
48
- command: 'generate' | 'saveConfig' | 'readFiles' | 'readInput' | 'readDiff';
48
+ command: 'generate' | 'saveConfig' | 'installExtension' | 'updateExtensions' | 'readFiles' | 'readInput' | 'readDiff';
49
49
  info: string;
50
50
  /**
51
51
  * The Studio user who asked for the command, when Studio names one.
@@ -97,6 +97,8 @@ type ConnectAgentOptions = {
97
97
  * What Studio may do in this project. Every permission is off unless granted.
98
98
  */
99
99
  permissions?: Partial<AgentPermissions>;
100
+ /** The CLI entry `installExtension` runs `kubb add` with. Found through the project's `kubb` or `@kubb/cli` dependency when omitted. */
101
+ kubbBin?: string;
100
102
  /**
101
103
  * Receives the `studio:*` hooks and the `kubb:*` hooks of every generation.
102
104
  */
package/dist/index.js CHANGED
@@ -1,17 +1,17 @@
1
- import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
1
+ import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, ConfigEditOperation, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
2
2
  import { createRequire } from "node:module";
3
3
  import { createHash, hash, randomUUID } from "node:crypto";
4
+ import { existsSync } from "node:fs";
5
+ import { mkdir, mkdir as mkdir$1, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
4
6
  import { tmpdir } from "node:os";
5
7
  import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
8
  import process$1 from "node:process";
7
9
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
8
- import { pathToFileURL } from "node:url";
9
- import { Readable, Transform } from "node:stream";
10
- import { existsSync } from "node:fs";
11
- import { mkdir, mkdir as mkdir$1, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
10
+ import { x } from "tinyexec";
12
11
  import { Diagnostics, Hookable, cacheStorage, checkoutGitInput, createKubb, fsStorage, getInputKind, isPackageName, isPublicAddress, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
13
12
  import { RpcTarget, newWebSocketRpcSession } from "capnweb";
14
13
  import { detectCodeFormat, parseExpression, parseModule } from "magicast";
14
+ import { pathToFileURL } from "node:url";
15
15
  import { structuredPatch } from "diff";
16
16
  import { stripVTControlCharacters } from "node:util";
17
17
  import { ast } from "@kubb/ast";
@@ -19,6 +19,7 @@ import dns from "node:dns";
19
19
  import http from "node:http";
20
20
  import https from "node:https";
21
21
  import net from "node:net";
22
+ import { Readable, Transform } from "node:stream";
22
23
  import { ofetch } from "ofetch";
23
24
  import { gzipSync } from "node:zlib";
24
25
  //#endregion
@@ -201,7 +202,7 @@ function getElapsedMs(hrStart) {
201
202
  }
202
203
  //#endregion
203
204
  //#region package.json
204
- var version = "5.12.1";
205
+ var version = "5.12.3";
205
206
  //#endregion
206
207
  //#region src/resolveConfig.ts
207
208
  /**
@@ -599,13 +600,7 @@ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
599
600
  * line count, so `enable-plugin` restores exactly those lines and not whatever comments follow.
600
601
  */
601
602
  const DISABLED_MARKER = /^\/\/ kubb:disabled (.+)\s+(\d+)$/;
602
- const OPERATIONS = /* @__PURE__ */ new Set([
603
- "set",
604
- "remove",
605
- "add-plugin",
606
- "disable-plugin",
607
- "enable-plugin"
608
- ]);
603
+ const OPERATIONS = new Set(Object.values(ConfigEditOperation));
609
604
  const KINDS = /* @__PURE__ */ new Set([
610
605
  "plugin",
611
606
  "adapter",
@@ -616,13 +611,14 @@ const KINDS = /* @__PURE__ */ new Set([
616
611
  * The edit crossed the wire, so its declared type is a promise the sender may not have kept.
617
612
  */
618
613
  function invalidEdit(edit) {
619
- const { operation, plugin, config, path, kind } = typeof edit === "object" && edit !== null ? edit : {};
614
+ const { operation, plugin, config, path, kind, input } = typeof edit === "object" && edit !== null ? edit : {};
620
615
  if (typeof operation !== "string" || !OPERATIONS.has(operation)) return `unknown operation ${JSON.stringify(operation)}`;
616
+ if (config !== void 0 && typeof config !== "string" && !Number.isSafeInteger(config)) return "config must name an entry or give its index";
617
+ if (operation === ConfigEditOperation.SET_INPUT) return typeof input === "string" && input.trim() ? void 0 : "the edit names no input";
621
618
  if (typeof plugin !== "string" || !plugin) return "the edit names no plugin";
622
619
  if (kind !== void 0 && (typeof kind !== "string" || !KINDS.has(kind))) return `unknown kind ${JSON.stringify(kind)}`;
623
- if (kind !== void 0 && operation !== "set" && operation !== "remove") return `${operation} edits the plugins array, so it takes no kind`;
624
- if (config !== void 0 && typeof config !== "string" && !Number.isSafeInteger(config)) return "config must name an entry or give its index";
625
- if (operation === "set" || operation === "remove") {
620
+ if (kind !== void 0 && operation !== ConfigEditOperation.SET && operation !== ConfigEditOperation.REMOVE) return `${operation} edits the plugins array, so it takes no kind`;
621
+ if (operation === ConfigEditOperation.SET || operation === ConfigEditOperation.REMOVE) {
626
622
  if (!Array.isArray(path) || !path.every((key) => typeof key === "string" && key)) return "path must be a list of option names";
627
623
  if (!path.length) return "no option path given";
628
624
  const unsafe = path.find((key) => !isSafeKey(key));
@@ -1063,6 +1059,15 @@ function applySet(source, call, path, value, quote) {
1063
1059
  }
1064
1060
  return { source: splice(source, rangeOf(target.value).start, rangeOf(target.value).end, printValue(value, quote)) };
1065
1061
  }
1062
+ /** Writes the config's top-level `input` as a string literal, replacing what it held. */
1063
+ function applySetInput(source, config, input, quote) {
1064
+ const value = printValue(input, quote);
1065
+ const found = findMember(config, "input");
1066
+ if (!found) return { source: insertItem(source, config, `input: ${value}`) };
1067
+ if (found.member.type !== "ObjectProperty") return { reason: "input is not a plain property" };
1068
+ if (isShadowed(config, found.index)) return { reason: "a spread after input could override it" };
1069
+ return { source: splice(source, rangeOf(found.member.value).start, rangeOf(found.member.value).end, value) };
1070
+ }
1066
1071
  /**
1067
1072
  * Deletes the option at `path`, falling the plugin back to its default for it. An option the file
1068
1073
  * does not set already has its default, so removing it changes nothing.
@@ -1159,7 +1164,8 @@ function applyEdit({ source, edit, quote, extensions }) {
1159
1164
  const findConfig = (entries) => typeof ref === "string" ? entries.configs.find((entry) => configName(entry) === ref) : entries.configs[ref ?? 0];
1160
1165
  const config = findConfig(parsed);
1161
1166
  if (!config) return { reason: `no config entry found for ${JSON.stringify(ref)}` };
1162
- if (edit.operation === "disable-plugin") {
1167
+ if (edit.operation === ConfigEditOperation.SET_INPUT) return applySetInput(source, config, edit.input, quote);
1168
+ if (edit.operation === ConfigEditOperation.DISABLE_PLUGIN) {
1163
1169
  const result = disablePlugin(source, parsed.mod, config, edit.plugin);
1164
1170
  if (!("reason" in result) || !result.reason.includes("shares a line")) return result;
1165
1171
  const expanded = expandPluginsArray(source, config);
@@ -1167,8 +1173,8 @@ function applyEdit({ source, edit, quote, extensions }) {
1167
1173
  const reparsedConfig = reparsed && !("reason" in reparsed) ? findConfig(reparsed) : void 0;
1168
1174
  return expanded !== void 0 && reparsed && !("reason" in reparsed) && reparsedConfig ? disablePlugin(expanded, reparsed.mod, reparsedConfig, edit.plugin) : result;
1169
1175
  }
1170
- if (edit.operation === "enable-plugin") return enablePlugin(source, config, edit.plugin);
1171
- if (edit.operation === "add-plugin") return applyAddPlugin({
1176
+ if (edit.operation === ConfigEditOperation.ENABLE_PLUGIN) return enablePlugin(source, config, edit.plugin);
1177
+ if (edit.operation === ConfigEditOperation.ADD_PLUGIN) return applyAddPlugin({
1172
1178
  source,
1173
1179
  parsed,
1174
1180
  config,
@@ -1184,7 +1190,7 @@ function applyEdit({ source, edit, quote, extensions }) {
1184
1190
  if (kind === "adapter") return { reason: `${edit.plugin} is not the config's adapter` };
1185
1191
  return { reason: kind === "plugin" && disabledBlocks(source, config).some((block) => block.packageName === edit.plugin) ? `${edit.plugin} is disabled, enable it first` : `${edit.plugin} is not in the ${kind}s array` };
1186
1192
  }
1187
- return edit.operation === "set" ? applySet(source, call, edit.path, edit.value, quote) : applyRemove(source, call, edit.path);
1193
+ return edit.operation === ConfigEditOperation.SET ? applySet(source, call, edit.path, edit.value, quote) : applyRemove(source, call, edit.path);
1188
1194
  }
1189
1195
  /**
1190
1196
  * Applies edits to a `kubb.config.ts` in place. Each edit rewrites only the text it targets, so
@@ -2013,6 +2019,43 @@ var IncompatibleAgentError = class extends Error {
2013
2019
  this.name = "IncompatibleAgentError";
2014
2020
  }
2015
2021
  };
2022
+ const INSTALL_TIMEOUT_MS = 3e5;
2023
+ /** `owner/slug` or `slug`, as `kubb add` takes it. Checked before it becomes an argument. */
2024
+ const REGISTRY_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)?$/;
2025
+ /** The CLI the project depends on, so `kubb add` installs next to what the config imports. */
2026
+ function findKubbBin(root) {
2027
+ const require = createRequire(path.resolve(root, "package.json"));
2028
+ const names = ["kubb", "@kubb/cli"];
2029
+ return (require.resolve.paths("kubb") ?? []).flatMap((dir) => names.map((name) => path.join(dir, name, "bin", "kubb.js"))).find((bin) => existsSync(bin));
2030
+ }
2031
+ /** Runs a `kubb` command without a shell, and keeps the end of its output for the failure reason. */
2032
+ async function runKubb({ bin, args, cwd, signal }) {
2033
+ try {
2034
+ const { stdout, stderr, exitCode } = await x(process$1.execPath, [bin, ...args], {
2035
+ signal,
2036
+ timeout: INSTALL_TIMEOUT_MS,
2037
+ throwOnError: false,
2038
+ nodeOptions: {
2039
+ cwd,
2040
+ env: {
2041
+ ...process$1.env,
2042
+ NO_COLOR: "1",
2043
+ FORCE_COLOR: "0"
2044
+ }
2045
+ }
2046
+ });
2047
+ const output = `${stdout}\n${stderr}`.trim().split("\n").slice(-20).join("\n");
2048
+ return {
2049
+ failed: exitCode !== 0,
2050
+ output: output || `kubb ${args[0]} exited with ${exitCode ?? "a signal"}`
2051
+ };
2052
+ } catch (error) {
2053
+ return {
2054
+ failed: true,
2055
+ output: `kubb ${args[0]} did not run: ${getErrorMessage(error)}`
2056
+ };
2057
+ }
2058
+ }
2016
2059
  function emit(hooks, name, ...args) {
2017
2060
  return Promise.resolve(hooks.callHook(name, ...args)).catch(() => {});
2018
2061
  }
@@ -2083,6 +2126,9 @@ var Agent = class extends RpcTarget {
2083
2126
  ...permissions,
2084
2127
  allowWrite: false,
2085
2128
  allowConfigEdit: false,
2129
+ allowInstall: false,
2130
+ allowUpdate: false,
2131
+ allowSaveInput: false,
2086
2132
  allowRead: true,
2087
2133
  allowReadAst: true
2088
2134
  } : permissions;
@@ -2108,13 +2154,13 @@ var Agent = class extends RpcTarget {
2108
2154
  permission
2109
2155
  });
2110
2156
  }
2111
- async #refuse({ message, permission }) {
2157
+ async #refuse({ message, permission, code = Diagnostics.code.unknown }) {
2112
2158
  await this.#warn({
2113
2159
  message,
2114
2160
  permission
2115
2161
  });
2116
2162
  throw new Diagnostics.Error({
2117
- code: Diagnostics.code.unknown,
2163
+ code,
2118
2164
  severity: "error",
2119
2165
  message
2120
2166
  });
@@ -2234,6 +2280,8 @@ var Agent = class extends RpcTarget {
2234
2280
  message: "Connection is closed"
2235
2281
  });
2236
2282
  if (this.#session.active) return this.#refuse({ message: AgentMessage.GENERATION_IN_PROGRESS });
2283
+ if (this.#session.install) return this.#refuse({ message: AgentMessage.INSTALL_IN_PROGRESS });
2284
+ if (this.#session.saving) return this.#refuse({ message: AgentMessage.SAVE_IN_PROGRESS });
2237
2285
  const settled = Promise.withResolvers();
2238
2286
  this.#session.active = {
2239
2287
  jobId: input.jobId,
@@ -2304,7 +2352,13 @@ var Agent = class extends RpcTarget {
2304
2352
  });
2305
2353
  const activePlugins = plugins ?? config.plugins;
2306
2354
  if (patch?.input && !this.#sandbox && !this.#managed) await emit(this.#options.hooks, "kubb:warn", { message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox or managed agent" });
2307
- const checkout = this.#managed && studioInput !== void 0 && getInputKind(studioInput) === "git" ? await checkoutGitInput(studioInput, signal, { publicOnly: true }).catch((error) => this.#refuse({ message: getErrorMessage(error) })) : void 0;
2355
+ const checkout = this.#managed && studioInput !== void 0 && getInputKind(studioInput) === "git" ? await checkoutGitInput(studioInput, signal, {
2356
+ publicOnly: true,
2357
+ authorization: input.gitAuth?.authorization
2358
+ }).catch((error) => this.#refuse({
2359
+ message: getErrorMessage(error),
2360
+ code: Diagnostics.code.gitInputFailed
2361
+ })) : void 0;
2308
2362
  const run = () => generate({
2309
2363
  config: {
2310
2364
  ...config,
@@ -2374,13 +2428,33 @@ var Agent = class extends RpcTarget {
2374
2428
  await this.#warn({ message: "Ignored save: editing kubb.config.ts was not granted" });
2375
2429
  return refuse("the agent was not granted permission to edit kubb.config.ts");
2376
2430
  }
2377
- if (this.#session.active) return refuse("a generation is in progress");
2431
+ const busy = this.#busy();
2432
+ if (busy) return refuse(busy);
2433
+ this.#session.saving = true;
2378
2434
  try {
2379
- const { outcomes, changed } = await writeConfigEdits({
2435
+ const kept = [];
2436
+ for (const edit of edits) kept.push(await this.#keepInput(edit));
2437
+ const written = await writeConfigEdits({
2380
2438
  filePath: configFile,
2381
- edits,
2439
+ edits: kept.flatMap((edit) => "reason" in edit ? [] : [edit]),
2382
2440
  extensions: await this.#installedExtensions()
2383
2441
  });
2442
+ let next = 0;
2443
+ const outcomes = kept.map((edit, index) => "reason" in edit ? {
2444
+ edit: edits[index],
2445
+ applied: false,
2446
+ reason: edit.reason
2447
+ } : {
2448
+ ...written.outcomes[next++],
2449
+ edit: edits[index]
2450
+ });
2451
+ let wroteDocument = false;
2452
+ for (const [index, edit] of kept.entries()) {
2453
+ if (!("document" in edit) || !edit.document || !outcomes[index].applied) continue;
2454
+ await writeFile(path.join(this.#options.root, edit.document.fileName), edit.document.text, "utf-8");
2455
+ wroteDocument = true;
2456
+ }
2457
+ const changed = written.changed || wroteDocument;
2384
2458
  const applied = outcomes.filter((outcome) => outcome.applied).length;
2385
2459
  await this.#logCommand("saveConfig", `applied ${applied}/${outcomes.length} edits to ${configPath}`, input.requestedBy);
2386
2460
  return {
@@ -2390,6 +2464,137 @@ var Agent = class extends RpcTarget {
2390
2464
  } catch (error) {
2391
2465
  await emit(this.#options.hooks, "studio:error", { error: toError(error) });
2392
2466
  return refuse(getErrorMessage(error));
2467
+ } finally {
2468
+ this.#session.saving = false;
2469
+ }
2470
+ }
2471
+ /**
2472
+ * Readies a `set-input` edit for the config file: a document is pointed at `openapi.yaml` (`openapi.json`
2473
+ * for JSON) in the project, one file per config entry, for `saveConfig` to write once the edit applies.
2474
+ * A managed agent refuses a file path, as in a generation.
2475
+ */
2476
+ async #keepInput(edit) {
2477
+ if (edit?.operation !== ConfigEditOperation.SET_INPUT || typeof edit.input !== "string" || !edit.input.trim()) return edit;
2478
+ if (!this.#permissions.allowSaveInput) {
2479
+ await this.#warn({
2480
+ message: "Ignored the spec to save: saving the input was not granted",
2481
+ permission: AgentPermission.SAVE_INPUT
2482
+ });
2483
+ return { reason: "the agent was not granted permission to save the input" };
2484
+ }
2485
+ const kind = getInputKind(edit.input);
2486
+ if (this.#managed && !MANAGED_INPUT_KINDS.has(kind)) return { reason: AgentMessage.MANAGED_INPUT_KIND };
2487
+ if (kind !== "inline") return edit;
2488
+ const fileName = `openapi${edit.config === void 0 || edit.config === 0 ? "" : `-${String(edit.config).replace(/[^\w.-]/g, "-")}`}.${isJson(edit.input) ? "json" : "yaml"}`;
2489
+ return {
2490
+ ...edit,
2491
+ input: `./${fileName}`,
2492
+ document: {
2493
+ fileName,
2494
+ text: edit.input
2495
+ }
2496
+ };
2497
+ }
2498
+ /**
2499
+ * Installs a registry extension into the project with `kubb add <id> --yes`, which also adds a
2500
+ * plugin to `kubb.config.ts`. Studio calls `connect()` again afterwards to see the result.
2501
+ */
2502
+ async installExtension(input) {
2503
+ this.#assertConnected();
2504
+ const { root, configFile, kubbBin = findKubbBin(root) } = this.#options;
2505
+ const refuse = async (reason, permission) => {
2506
+ await this.#warn({
2507
+ message: `Ignored install: ${reason}`,
2508
+ permission
2509
+ });
2510
+ return {
2511
+ installed: false,
2512
+ reason
2513
+ };
2514
+ };
2515
+ if (typeof input?.id !== "string" || !REGISTRY_ID.test(input.id)) return refuse("the message carried no registry id");
2516
+ if (!this.#permissions.allowInstall) return refuse("installing extensions was not granted", AgentPermission.INSTALL);
2517
+ if (!this.#permissions.allowConfigEdit) return refuse("editing kubb.config.ts was not granted", AgentPermission.CONFIG_EDIT);
2518
+ const busy = this.#busy();
2519
+ if (busy) return refuse(busy);
2520
+ if (!kubbBin) return refuse("kubb is not installed in the project");
2521
+ const { failed, output } = await this.#runKubb({
2522
+ bin: kubbBin,
2523
+ args: [
2524
+ "add",
2525
+ input.id,
2526
+ "--yes",
2527
+ "--config",
2528
+ configFile
2529
+ ]
2530
+ });
2531
+ await this.#logCommand("installExtension", `${failed ? "failed to install" : "installed"} ${input.id}`, input.requestedBy, failed);
2532
+ return failed ? {
2533
+ installed: false,
2534
+ reason: output
2535
+ } : { installed: true };
2536
+ }
2537
+ /**
2538
+ * Moves installed extensions to the registry's version with `kubb update [ids] --yes`, which
2539
+ * installs packages and leaves `kubb.config.ts` alone. Studio calls `connect()` again afterwards.
2540
+ */
2541
+ async updateExtensions(input) {
2542
+ this.#assertConnected();
2543
+ const { root, kubbBin = findKubbBin(root) } = this.#options;
2544
+ const refuse = async (reason, permission) => {
2545
+ await this.#warn({
2546
+ message: `Ignored update: ${reason}`,
2547
+ permission
2548
+ });
2549
+ return {
2550
+ updated: false,
2551
+ reason
2552
+ };
2553
+ };
2554
+ const ids = input?.ids ?? [];
2555
+ if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && REGISTRY_ID.test(id))) return refuse("the message carried an invalid registry id");
2556
+ if (!this.#permissions.allowUpdate) return refuse("updating extensions was not granted", AgentPermission.UPDATE);
2557
+ const busy = this.#busy();
2558
+ if (busy) return refuse(busy);
2559
+ if (!kubbBin) return refuse("kubb is not installed in the project");
2560
+ const { failed, output } = await this.#runKubb({
2561
+ bin: kubbBin,
2562
+ args: [
2563
+ "update",
2564
+ ...ids,
2565
+ "--yes"
2566
+ ]
2567
+ });
2568
+ await this.#logCommand("updateExtensions", `${failed ? "failed to update" : "updated"} ${ids.join(", ") || "all extensions"}`, input?.requestedBy, failed);
2569
+ return failed ? {
2570
+ updated: false,
2571
+ reason: output
2572
+ } : { updated: true };
2573
+ }
2574
+ /** Why a command that touches the project cannot start right now. */
2575
+ #busy() {
2576
+ if (this.#session.active) return "a generation is in progress";
2577
+ if (this.#session.install) return "an install or update is in progress";
2578
+ if (this.#session.saving) return "a save is in progress";
2579
+ }
2580
+ /** Runs `kubb` in the project while holding off saves and generations, and ends it when the socket closes. */
2581
+ async #runKubb({ bin, args }) {
2582
+ const controller = new AbortController();
2583
+ const settled = Promise.withResolvers();
2584
+ this.#session.install = {
2585
+ abort: () => controller.abort(),
2586
+ settled: settled.promise
2587
+ };
2588
+ try {
2589
+ return await runKubb({
2590
+ bin,
2591
+ args,
2592
+ cwd: this.#options.root,
2593
+ signal: controller.signal
2594
+ });
2595
+ } finally {
2596
+ this.#session.install = void 0;
2597
+ settled.resolve();
2393
2598
  }
2394
2599
  }
2395
2600
  /**
@@ -2574,6 +2779,8 @@ async function runSession({ options, openRpc, url, instanceId }) {
2574
2779
  await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
2575
2780
  }
2576
2781
  await active?.settled;
2782
+ session.install?.abort();
2783
+ await session.install?.settled;
2577
2784
  await agent.clear();
2578
2785
  return {
2579
2786
  ...close,
@@ -2589,6 +2796,15 @@ function pickConfig(configs, ref) {
2589
2796
  if (ref !== void 0 && !Number.isSafeInteger(ref)) return void 0;
2590
2797
  return configs[ref ?? 0];
2591
2798
  }
2799
+ /** Whether `text` parses as JSON, so a pasted document is saved under the matching extension. */
2800
+ function isJson(text) {
2801
+ try {
2802
+ JSON.parse(text);
2803
+ return true;
2804
+ } catch {
2805
+ return false;
2806
+ }
2807
+ }
2592
2808
  /** The input path or URL Studio may show: no inline document, and no credentials from a URL. */
2593
2809
  function reportedInput(input) {
2594
2810
  if (typeof input !== "string") return void 0;
@@ -2642,6 +2858,9 @@ async function runAgent({ options, openRpc }) {
2642
2858
  permissions: {
2643
2859
  allowWrite: false,
2644
2860
  allowConfigEdit: false,
2861
+ allowInstall: false,
2862
+ allowUpdate: false,
2863
+ allowSaveInput: false,
2645
2864
  allowExec: false,
2646
2865
  allowRead: false,
2647
2866
  allowReadAst: false,