@kubb/studio 5.12.1 → 5.12.2

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
@@ -23,20 +23,20 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
23
23
  //#endregion
24
24
  const require_protocol = require("./protocol.cjs");
25
25
  let node_crypto = require("node:crypto");
26
+ let node_fs = require("node:fs");
27
+ let node_module = require("node:module");
26
28
  let node_os = require("node:os");
27
29
  let node_path = require("node:path");
28
30
  node_path = __toESM(node_path, 1);
29
31
  let node_process = require("node:process");
30
32
  node_process = __toESM(node_process, 1);
31
33
  let node_timers_promises = require("node:timers/promises");
32
- let node_url = require("node:url");
33
- let node_stream = require("node:stream");
34
- let node_fs = require("node:fs");
34
+ let tinyexec = require("tinyexec");
35
35
  let node_fs_promises = require("node:fs/promises");
36
36
  let _kubb_core = require("@kubb/core");
37
37
  let capnweb = require("capnweb");
38
38
  let magicast = require("magicast");
39
- let node_module = require("node:module");
39
+ let node_url = require("node:url");
40
40
  let diff = require("diff");
41
41
  let node_util = require("node:util");
42
42
  let _kubb_ast = require("@kubb/ast");
@@ -48,6 +48,7 @@ let node_https = require("node:https");
48
48
  node_https = __toESM(node_https, 1);
49
49
  let node_net = require("node:net");
50
50
  node_net = __toESM(node_net, 1);
51
+ let node_stream = require("node:stream");
51
52
  let ofetch = require("ofetch");
52
53
  let node_zlib = require("node:zlib");
53
54
  //#region ../../internals/utils/src/casing.ts
@@ -229,7 +230,7 @@ function getElapsedMs(hrStart) {
229
230
  }
230
231
  //#endregion
231
232
  //#region package.json
232
- var version = "5.12.1";
233
+ var version = "5.12.2";
233
234
  //#endregion
234
235
  //#region src/resolveConfig.ts
235
236
  /**
@@ -2041,6 +2042,43 @@ var IncompatibleAgentError = class extends Error {
2041
2042
  this.name = "IncompatibleAgentError";
2042
2043
  }
2043
2044
  };
2045
+ const INSTALL_TIMEOUT_MS = 3e5;
2046
+ /** `owner/slug` or `slug`, as `kubb add` takes it. Checked before it becomes an argument. */
2047
+ const REGISTRY_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)?$/;
2048
+ /** The CLI the project depends on, so `kubb add` installs next to what the config imports. */
2049
+ function findKubbBin(root) {
2050
+ const require$1 = (0, node_module.createRequire)(node_path.default.resolve(root, "package.json"));
2051
+ const names = ["kubb", "@kubb/cli"];
2052
+ return (require$1.resolve.paths("kubb") ?? []).flatMap((dir) => names.map((name) => node_path.default.join(dir, name, "bin", "kubb.js"))).find((bin) => (0, node_fs.existsSync)(bin));
2053
+ }
2054
+ /** Runs a `kubb` command without a shell, and keeps the end of its output for the failure reason. */
2055
+ async function runKubb({ bin, args, cwd, signal }) {
2056
+ try {
2057
+ const { stdout, stderr, exitCode } = await (0, tinyexec.x)(node_process.default.execPath, [bin, ...args], {
2058
+ signal,
2059
+ timeout: INSTALL_TIMEOUT_MS,
2060
+ throwOnError: false,
2061
+ nodeOptions: {
2062
+ cwd,
2063
+ env: {
2064
+ ...node_process.default.env,
2065
+ NO_COLOR: "1",
2066
+ FORCE_COLOR: "0"
2067
+ }
2068
+ }
2069
+ });
2070
+ const output = `${stdout}\n${stderr}`.trim().split("\n").slice(-20).join("\n");
2071
+ return {
2072
+ failed: exitCode !== 0,
2073
+ output: output || `kubb ${args[0]} exited with ${exitCode ?? "a signal"}`
2074
+ };
2075
+ } catch (error) {
2076
+ return {
2077
+ failed: true,
2078
+ output: `kubb ${args[0]} did not run: ${getErrorMessage(error)}`
2079
+ };
2080
+ }
2081
+ }
2044
2082
  function emit(hooks, name, ...args) {
2045
2083
  return Promise.resolve(hooks.callHook(name, ...args)).catch(() => {});
2046
2084
  }
@@ -2111,6 +2149,8 @@ var Agent = class extends capnweb.RpcTarget {
2111
2149
  ...permissions,
2112
2150
  allowWrite: false,
2113
2151
  allowConfigEdit: false,
2152
+ allowInstall: false,
2153
+ allowUpdate: false,
2114
2154
  allowRead: true,
2115
2155
  allowReadAst: true
2116
2156
  } : permissions;
@@ -2262,6 +2302,8 @@ var Agent = class extends capnweb.RpcTarget {
2262
2302
  message: "Connection is closed"
2263
2303
  });
2264
2304
  if (this.#session.active) return this.#refuse({ message: require_protocol.AgentMessage.GENERATION_IN_PROGRESS });
2305
+ if (this.#session.install) return this.#refuse({ message: require_protocol.AgentMessage.INSTALL_IN_PROGRESS });
2306
+ if (this.#session.saving) return this.#refuse({ message: require_protocol.AgentMessage.SAVE_IN_PROGRESS });
2265
2307
  const settled = Promise.withResolvers();
2266
2308
  this.#session.active = {
2267
2309
  jobId: input.jobId,
@@ -2402,7 +2444,9 @@ var Agent = class extends capnweb.RpcTarget {
2402
2444
  await this.#warn({ message: "Ignored save: editing kubb.config.ts was not granted" });
2403
2445
  return refuse("the agent was not granted permission to edit kubb.config.ts");
2404
2446
  }
2405
- if (this.#session.active) return refuse("a generation is in progress");
2447
+ const busy = this.#busy();
2448
+ if (busy) return refuse(busy);
2449
+ this.#session.saving = true;
2406
2450
  try {
2407
2451
  const { outcomes, changed } = await writeConfigEdits({
2408
2452
  filePath: configFile,
@@ -2418,6 +2462,110 @@ var Agent = class extends capnweb.RpcTarget {
2418
2462
  } catch (error) {
2419
2463
  await emit(this.#options.hooks, "studio:error", { error: toError(error) });
2420
2464
  return refuse(getErrorMessage(error));
2465
+ } finally {
2466
+ this.#session.saving = false;
2467
+ }
2468
+ }
2469
+ /**
2470
+ * Installs a registry extension into the project with `kubb add <id> --yes`, which also adds a
2471
+ * plugin to `kubb.config.ts`. Studio calls `connect()` again afterwards to see the result.
2472
+ */
2473
+ async installExtension(input) {
2474
+ this.#assertConnected();
2475
+ const { root, configFile, kubbBin = findKubbBin(root) } = this.#options;
2476
+ const refuse = async (reason, permission) => {
2477
+ await this.#warn({
2478
+ message: `Ignored install: ${reason}`,
2479
+ permission
2480
+ });
2481
+ return {
2482
+ installed: false,
2483
+ reason
2484
+ };
2485
+ };
2486
+ if (typeof input?.id !== "string" || !REGISTRY_ID.test(input.id)) return refuse("the message carried no registry id");
2487
+ if (!this.#permissions.allowInstall) return refuse("installing extensions was not granted", require_protocol.AgentPermission.INSTALL);
2488
+ if (!this.#permissions.allowConfigEdit) return refuse("editing kubb.config.ts was not granted", require_protocol.AgentPermission.CONFIG_EDIT);
2489
+ const busy = this.#busy();
2490
+ if (busy) return refuse(busy);
2491
+ if (!kubbBin) return refuse("kubb is not installed in the project");
2492
+ const { failed, output } = await this.#runKubb({
2493
+ bin: kubbBin,
2494
+ args: [
2495
+ "add",
2496
+ input.id,
2497
+ "--yes",
2498
+ "--config",
2499
+ configFile
2500
+ ]
2501
+ });
2502
+ await this.#logCommand("installExtension", `${failed ? "failed to install" : "installed"} ${input.id}`, input.requestedBy, failed);
2503
+ return failed ? {
2504
+ installed: false,
2505
+ reason: output
2506
+ } : { installed: true };
2507
+ }
2508
+ /**
2509
+ * Moves installed extensions to the registry's version with `kubb update [ids] --yes`, which
2510
+ * installs packages and leaves `kubb.config.ts` alone. Studio calls `connect()` again afterwards.
2511
+ */
2512
+ async updateExtensions(input) {
2513
+ this.#assertConnected();
2514
+ const { root, kubbBin = findKubbBin(root) } = this.#options;
2515
+ const refuse = async (reason, permission) => {
2516
+ await this.#warn({
2517
+ message: `Ignored update: ${reason}`,
2518
+ permission
2519
+ });
2520
+ return {
2521
+ updated: false,
2522
+ reason
2523
+ };
2524
+ };
2525
+ const ids = input?.ids ?? [];
2526
+ if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && REGISTRY_ID.test(id))) return refuse("the message carried an invalid registry id");
2527
+ if (!this.#permissions.allowUpdate) return refuse("updating extensions was not granted", require_protocol.AgentPermission.UPDATE);
2528
+ const busy = this.#busy();
2529
+ if (busy) return refuse(busy);
2530
+ if (!kubbBin) return refuse("kubb is not installed in the project");
2531
+ const { failed, output } = await this.#runKubb({
2532
+ bin: kubbBin,
2533
+ args: [
2534
+ "update",
2535
+ ...ids,
2536
+ "--yes"
2537
+ ]
2538
+ });
2539
+ await this.#logCommand("updateExtensions", `${failed ? "failed to update" : "updated"} ${ids.join(", ") || "all extensions"}`, input?.requestedBy, failed);
2540
+ return failed ? {
2541
+ updated: false,
2542
+ reason: output
2543
+ } : { updated: true };
2544
+ }
2545
+ /** Why a command that touches the project cannot start right now. */
2546
+ #busy() {
2547
+ if (this.#session.active) return "a generation is in progress";
2548
+ if (this.#session.install) return "an install or update is in progress";
2549
+ if (this.#session.saving) return "a save is in progress";
2550
+ }
2551
+ /** Runs `kubb` in the project while holding off saves and generations, and ends it when the socket closes. */
2552
+ async #runKubb({ bin, args }) {
2553
+ const controller = new AbortController();
2554
+ const settled = Promise.withResolvers();
2555
+ this.#session.install = {
2556
+ abort: () => controller.abort(),
2557
+ settled: settled.promise
2558
+ };
2559
+ try {
2560
+ return await runKubb({
2561
+ bin,
2562
+ args,
2563
+ cwd: this.#options.root,
2564
+ signal: controller.signal
2565
+ });
2566
+ } finally {
2567
+ this.#session.install = void 0;
2568
+ settled.resolve();
2421
2569
  }
2422
2570
  }
2423
2571
  /**
@@ -2602,6 +2750,8 @@ async function runSession({ options, openRpc, url, instanceId }) {
2602
2750
  await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
2603
2751
  }
2604
2752
  await active?.settled;
2753
+ session.install?.abort();
2754
+ await session.install?.settled;
2605
2755
  await agent.clear();
2606
2756
  return {
2607
2757
  ...close,
@@ -2670,6 +2820,8 @@ async function runAgent({ options, openRpc }) {
2670
2820
  permissions: {
2671
2821
  allowWrite: false,
2672
2822
  allowConfigEdit: false,
2823
+ allowInstall: false,
2824
+ allowUpdate: false,
2673
2825
  allowExec: false,
2674
2826
  allowRead: false,
2675
2827
  allowReadAst: false,