@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.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 { A as InstalledExtensions, L as PublishedSnapshot, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, q as SnapshotUploadMetadata, s as AgentPermission, u as ConfigEdit } from "./protocol-DhNrwhqp.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 { A as InstalledExtensions, L as PublishedSnapshot, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, q as SnapshotUploadMetadata, s as AgentPermission, u as ConfigEdit } from "./protocol-DhNrwhqp.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
1
  import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, 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";
4
5
  import { tmpdir } from "node:os";
5
6
  import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
6
7
  import process$1 from "node:process";
7
8
  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";
9
+ import { x } from "tinyexec";
11
10
  import { mkdir, mkdir as mkdir$1, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
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.2";
205
206
  //#endregion
206
207
  //#region src/resolveConfig.ts
207
208
  /**
@@ -2013,6 +2014,43 @@ var IncompatibleAgentError = class extends Error {
2013
2014
  this.name = "IncompatibleAgentError";
2014
2015
  }
2015
2016
  };
2017
+ const INSTALL_TIMEOUT_MS = 3e5;
2018
+ /** `owner/slug` or `slug`, as `kubb add` takes it. Checked before it becomes an argument. */
2019
+ const REGISTRY_ID = /^[a-z0-9]+(?:-[a-z0-9]+)*(?:\/[a-z0-9]+(?:-[a-z0-9]+)*)?$/;
2020
+ /** The CLI the project depends on, so `kubb add` installs next to what the config imports. */
2021
+ function findKubbBin(root) {
2022
+ const require = createRequire(path.resolve(root, "package.json"));
2023
+ const names = ["kubb", "@kubb/cli"];
2024
+ return (require.resolve.paths("kubb") ?? []).flatMap((dir) => names.map((name) => path.join(dir, name, "bin", "kubb.js"))).find((bin) => existsSync(bin));
2025
+ }
2026
+ /** Runs a `kubb` command without a shell, and keeps the end of its output for the failure reason. */
2027
+ async function runKubb({ bin, args, cwd, signal }) {
2028
+ try {
2029
+ const { stdout, stderr, exitCode } = await x(process$1.execPath, [bin, ...args], {
2030
+ signal,
2031
+ timeout: INSTALL_TIMEOUT_MS,
2032
+ throwOnError: false,
2033
+ nodeOptions: {
2034
+ cwd,
2035
+ env: {
2036
+ ...process$1.env,
2037
+ NO_COLOR: "1",
2038
+ FORCE_COLOR: "0"
2039
+ }
2040
+ }
2041
+ });
2042
+ const output = `${stdout}\n${stderr}`.trim().split("\n").slice(-20).join("\n");
2043
+ return {
2044
+ failed: exitCode !== 0,
2045
+ output: output || `kubb ${args[0]} exited with ${exitCode ?? "a signal"}`
2046
+ };
2047
+ } catch (error) {
2048
+ return {
2049
+ failed: true,
2050
+ output: `kubb ${args[0]} did not run: ${getErrorMessage(error)}`
2051
+ };
2052
+ }
2053
+ }
2016
2054
  function emit(hooks, name, ...args) {
2017
2055
  return Promise.resolve(hooks.callHook(name, ...args)).catch(() => {});
2018
2056
  }
@@ -2083,6 +2121,8 @@ var Agent = class extends RpcTarget {
2083
2121
  ...permissions,
2084
2122
  allowWrite: false,
2085
2123
  allowConfigEdit: false,
2124
+ allowInstall: false,
2125
+ allowUpdate: false,
2086
2126
  allowRead: true,
2087
2127
  allowReadAst: true
2088
2128
  } : permissions;
@@ -2234,6 +2274,8 @@ var Agent = class extends RpcTarget {
2234
2274
  message: "Connection is closed"
2235
2275
  });
2236
2276
  if (this.#session.active) return this.#refuse({ message: AgentMessage.GENERATION_IN_PROGRESS });
2277
+ if (this.#session.install) return this.#refuse({ message: AgentMessage.INSTALL_IN_PROGRESS });
2278
+ if (this.#session.saving) return this.#refuse({ message: AgentMessage.SAVE_IN_PROGRESS });
2237
2279
  const settled = Promise.withResolvers();
2238
2280
  this.#session.active = {
2239
2281
  jobId: input.jobId,
@@ -2374,7 +2416,9 @@ var Agent = class extends RpcTarget {
2374
2416
  await this.#warn({ message: "Ignored save: editing kubb.config.ts was not granted" });
2375
2417
  return refuse("the agent was not granted permission to edit kubb.config.ts");
2376
2418
  }
2377
- if (this.#session.active) return refuse("a generation is in progress");
2419
+ const busy = this.#busy();
2420
+ if (busy) return refuse(busy);
2421
+ this.#session.saving = true;
2378
2422
  try {
2379
2423
  const { outcomes, changed } = await writeConfigEdits({
2380
2424
  filePath: configFile,
@@ -2390,6 +2434,110 @@ var Agent = class extends RpcTarget {
2390
2434
  } catch (error) {
2391
2435
  await emit(this.#options.hooks, "studio:error", { error: toError(error) });
2392
2436
  return refuse(getErrorMessage(error));
2437
+ } finally {
2438
+ this.#session.saving = false;
2439
+ }
2440
+ }
2441
+ /**
2442
+ * Installs a registry extension into the project with `kubb add <id> --yes`, which also adds a
2443
+ * plugin to `kubb.config.ts`. Studio calls `connect()` again afterwards to see the result.
2444
+ */
2445
+ async installExtension(input) {
2446
+ this.#assertConnected();
2447
+ const { root, configFile, kubbBin = findKubbBin(root) } = this.#options;
2448
+ const refuse = async (reason, permission) => {
2449
+ await this.#warn({
2450
+ message: `Ignored install: ${reason}`,
2451
+ permission
2452
+ });
2453
+ return {
2454
+ installed: false,
2455
+ reason
2456
+ };
2457
+ };
2458
+ if (typeof input?.id !== "string" || !REGISTRY_ID.test(input.id)) return refuse("the message carried no registry id");
2459
+ if (!this.#permissions.allowInstall) return refuse("installing extensions was not granted", AgentPermission.INSTALL);
2460
+ if (!this.#permissions.allowConfigEdit) return refuse("editing kubb.config.ts was not granted", AgentPermission.CONFIG_EDIT);
2461
+ const busy = this.#busy();
2462
+ if (busy) return refuse(busy);
2463
+ if (!kubbBin) return refuse("kubb is not installed in the project");
2464
+ const { failed, output } = await this.#runKubb({
2465
+ bin: kubbBin,
2466
+ args: [
2467
+ "add",
2468
+ input.id,
2469
+ "--yes",
2470
+ "--config",
2471
+ configFile
2472
+ ]
2473
+ });
2474
+ await this.#logCommand("installExtension", `${failed ? "failed to install" : "installed"} ${input.id}`, input.requestedBy, failed);
2475
+ return failed ? {
2476
+ installed: false,
2477
+ reason: output
2478
+ } : { installed: true };
2479
+ }
2480
+ /**
2481
+ * Moves installed extensions to the registry's version with `kubb update [ids] --yes`, which
2482
+ * installs packages and leaves `kubb.config.ts` alone. Studio calls `connect()` again afterwards.
2483
+ */
2484
+ async updateExtensions(input) {
2485
+ this.#assertConnected();
2486
+ const { root, kubbBin = findKubbBin(root) } = this.#options;
2487
+ const refuse = async (reason, permission) => {
2488
+ await this.#warn({
2489
+ message: `Ignored update: ${reason}`,
2490
+ permission
2491
+ });
2492
+ return {
2493
+ updated: false,
2494
+ reason
2495
+ };
2496
+ };
2497
+ const ids = input?.ids ?? [];
2498
+ if (!Array.isArray(ids) || !ids.every((id) => typeof id === "string" && REGISTRY_ID.test(id))) return refuse("the message carried an invalid registry id");
2499
+ if (!this.#permissions.allowUpdate) return refuse("updating extensions was not granted", AgentPermission.UPDATE);
2500
+ const busy = this.#busy();
2501
+ if (busy) return refuse(busy);
2502
+ if (!kubbBin) return refuse("kubb is not installed in the project");
2503
+ const { failed, output } = await this.#runKubb({
2504
+ bin: kubbBin,
2505
+ args: [
2506
+ "update",
2507
+ ...ids,
2508
+ "--yes"
2509
+ ]
2510
+ });
2511
+ await this.#logCommand("updateExtensions", `${failed ? "failed to update" : "updated"} ${ids.join(", ") || "all extensions"}`, input?.requestedBy, failed);
2512
+ return failed ? {
2513
+ updated: false,
2514
+ reason: output
2515
+ } : { updated: true };
2516
+ }
2517
+ /** Why a command that touches the project cannot start right now. */
2518
+ #busy() {
2519
+ if (this.#session.active) return "a generation is in progress";
2520
+ if (this.#session.install) return "an install or update is in progress";
2521
+ if (this.#session.saving) return "a save is in progress";
2522
+ }
2523
+ /** Runs `kubb` in the project while holding off saves and generations, and ends it when the socket closes. */
2524
+ async #runKubb({ bin, args }) {
2525
+ const controller = new AbortController();
2526
+ const settled = Promise.withResolvers();
2527
+ this.#session.install = {
2528
+ abort: () => controller.abort(),
2529
+ settled: settled.promise
2530
+ };
2531
+ try {
2532
+ return await runKubb({
2533
+ bin,
2534
+ args,
2535
+ cwd: this.#options.root,
2536
+ signal: controller.signal
2537
+ });
2538
+ } finally {
2539
+ this.#session.install = void 0;
2540
+ settled.resolve();
2393
2541
  }
2394
2542
  }
2395
2543
  /**
@@ -2574,6 +2722,8 @@ async function runSession({ options, openRpc, url, instanceId }) {
2574
2722
  await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
2575
2723
  }
2576
2724
  await active?.settled;
2725
+ session.install?.abort();
2726
+ await session.install?.settled;
2577
2727
  await agent.clear();
2578
2728
  return {
2579
2729
  ...close,
@@ -2642,6 +2792,8 @@ async function runAgent({ options, openRpc }) {
2642
2792
  permissions: {
2643
2793
  allowWrite: false,
2644
2794
  allowConfigEdit: false,
2795
+ allowInstall: false,
2796
+ allowUpdate: false,
2645
2797
  allowExec: false,
2646
2798
  allowRead: false,
2647
2799
  allowReadAst: false,