@kubb/studio 5.5.5 → 5.6.0

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.js CHANGED
@@ -1,22 +1,19 @@
1
- import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
2
- import { a as toPackageName, n as mergeAdapter, o as read, r as mergePlugins } from "./resolveConfig-DMIG_MH-.js";
1
+ import "./rolldown-runtime-CRm0XQPb.js";
2
+ import { a as resolvePeerDependencies, c as read, i as mergePlugins, r as mergeAdapter, s as toPackageName } from "./resolveConfig-DOUv_Tid.js";
3
3
  import { AGENT_INSTANCE_HEADER, AgentCloseCode, GENERATION_GONE_MESSAGE, MAX_PAGE_LINES, generationEventTypes } from "./protocol.js";
4
- import { createRequire } from "node:module";
4
+ import { t as isLoopbackHost } from "./url-DhFfJpxp.js";
5
5
  import { createHash, hash, randomBytes, randomUUID } from "node:crypto";
6
6
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
7
7
  import process$1 from "node:process";
8
- import { spawn } from "node:child_process";
9
- import { mkdtemp, readFile, rm } from "node:fs/promises";
10
8
  import path, { isAbsolute, relative, resolve, sep } from "node:path";
9
+ import { mkdtemp, rm } from "node:fs/promises";
11
10
  import { FetchError, ofetch } from "ofetch";
12
- import { styleText } from "node:util";
11
+ import { stripVTControlCharacters, styleText } from "node:util";
13
12
  import { createStorage } from "unstorage";
14
13
  import fsDriver from "unstorage/drivers/fs";
15
14
  import { tmpdir } from "node:os";
16
15
  import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, memoryStorage, resolveCacheDir } from "@kubb/core";
17
16
  import { RpcTarget, newWebSocketRpcSession } from "capnweb";
18
- import WebSocket from "ws";
19
- import { x } from "tinyexec";
20
17
  //#region ../../internals/utils/src/errors.ts
21
18
  /**
22
19
  * Coerces an unknown thrown value to an `Error` instance.
@@ -47,44 +44,6 @@ function getErrorMessage(value) {
47
44
  //#endregion
48
45
  //#region ../../internals/utils/src/promise.ts
49
46
  /**
50
- * Wraps `factory` with a keyed cache backed by the provided store.
51
- *
52
- * Pass a `WeakMap` for object keys (results are GC-eligible when the key is
53
- * collected) or a `Map` for primitive keys. For multi-argument functions,
54
- * nest two `memoize` calls — the outer keyed by the first argument, the
55
- * inner (created once per outer miss) keyed by the second.
56
- *
57
- * Because the cache is owned by the caller, it can be shared, inspected, or
58
- * cleared independently of the memoized function.
59
- *
60
- * @example Single WeakMap key
61
- * ```ts
62
- * const cache = new WeakMap<SchemaNode, Set<string>>()
63
- * const getRefs = memoize(cache, (node) => collectRefs(node))
64
- * ```
65
- *
66
- * @example Single Map key (primitive)
67
- * ```ts
68
- * const cache = new Map<string, Resolver>()
69
- * const getResolver = memoize(cache, (name) => buildResolver(name))
70
- * ```
71
- *
72
- * @example Two-level (object + primitive)
73
- * ```ts
74
- * const outer = new WeakMap<Params[], Map<string, Params[]>>()
75
- * const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
76
- * fn(params)('camelcase')
77
- * ```
78
- */
79
- function memoize(store, factory) {
80
- return (key) => {
81
- if (store.has(key)) return store.get(key);
82
- const value = factory(key);
83
- store.set(key, value);
84
- return value;
85
- };
86
- }
87
- /**
88
47
  * Runs `run` over every item with at most `limit` in flight. Workers share one iterator, so each
89
48
  * takes the next item the moment it frees up instead of waiting for a batch to drain.
90
49
  *
@@ -101,114 +60,6 @@ async function inParallel({ items, limit, run }) {
101
60
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
102
61
  }
103
62
  //#endregion
104
- //#region ../../internals/utils/src/tools.ts
105
- /**
106
- * CLI command descriptors for each supported code formatter.
107
- */
108
- const formatters = {
109
- prettier: {
110
- command: "prettier",
111
- args: (outputPath) => [
112
- "--ignore-unknown",
113
- "--write",
114
- outputPath
115
- ],
116
- errorMessage: "Prettier not found"
117
- },
118
- biome: {
119
- command: "biome",
120
- args: (outputPath) => [
121
- "format",
122
- "--write",
123
- outputPath
124
- ],
125
- errorMessage: "Biome not found"
126
- },
127
- oxfmt: {
128
- command: "oxfmt",
129
- args: (outputPath) => [outputPath],
130
- errorMessage: "Oxfmt not found"
131
- }
132
- };
133
- /**
134
- * CLI command descriptors for each supported linter.
135
- */
136
- const linters = {
137
- eslint: {
138
- command: "eslint",
139
- args: (outputPath) => [outputPath, "--fix"],
140
- errorMessage: "Eslint not found"
141
- },
142
- biome: {
143
- command: "biome",
144
- args: (outputPath) => [
145
- "lint",
146
- "--fix",
147
- outputPath
148
- ],
149
- errorMessage: "Biome not found"
150
- },
151
- oxlint: {
152
- command: "oxlint",
153
- args: (outputPath) => [
154
- "--fix",
155
- "--no-ignore",
156
- outputPath
157
- ],
158
- errorMessage: "Oxlint not found"
159
- }
160
- };
161
- /**
162
- * Preference order for `format: 'auto'`, most-preferred first. Spelled out rather than taken from
163
- * the table's key order, which is arbitrary and would silently change what `auto` picks.
164
- */
165
- const FORMATTER_PREFERENCE = [
166
- "oxfmt",
167
- "biome",
168
- "prettier"
169
- ];
170
- /**
171
- * Preference order for `lint: 'auto'`, most-preferred first.
172
- */
173
- const LINTER_PREFERENCE = [
174
- "oxlint",
175
- "biome",
176
- "eslint"
177
- ];
178
- /**
179
- * Whether `name` is on PATH and answers `--version` with a zero exit.
180
- */
181
- function isToolAvailable(name) {
182
- return new Promise((resolve) => {
183
- const child = spawn(name, ["--version"], { stdio: "ignore" });
184
- child.on("close", (code) => resolve(code === 0));
185
- child.on("error", () => resolve(false));
186
- });
187
- }
188
- /**
189
- * Returns the first installed executable from `candidates`, or `null` when none are found.
190
- *
191
- * Not memoized: a long-running host that probes repeatedly should cache the result itself, and a
192
- * `--watch` build should keep noticing a tool installed mid-session.
193
- */
194
- async function detectTool$1(candidates) {
195
- for (const candidate of candidates) if (await isToolAvailable(candidate)) return candidate;
196
- return null;
197
- }
198
- __name(detectTool$1, "detectTool");
199
- /**
200
- * Tokenizes a shell command string, respecting single and double quotes.
201
- *
202
- * @example
203
- * ```ts
204
- * tokenize('git commit -m "initial commit"')
205
- * // → ['git', 'commit', '-m', 'initial commit']
206
- * ```
207
- */
208
- function tokenize(command) {
209
- return (command.match(/[^\s"']+|"([^"]*)"|'([^']*)'/g) ?? []).map((token) => token.replace(/^["']|["']$/g, ""));
210
- }
211
- //#endregion
212
63
  //#region ../../internals/utils/src/time.ts
213
64
  /**
214
65
  * Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.
@@ -542,8 +393,8 @@ const agentDefaults = {
542
393
  heartbeatIntervalMs: 3e4,
543
394
  /**
544
395
  * Slowest heartbeat a host may ask for. Studio drops an agent from the active list once its
545
- * stored ping is older than its liveness window, and it stores a ping at most once a minute, so
546
- * a slower cadence would make a healthy agent look dead after a single missed ping.
396
+ * stored ping is older than its liveness window, and it persists a ping at most once an hour,
397
+ * so a slower cadence would make a healthy agent look dead after a single missed ping.
547
398
  */
548
399
  maxHeartbeatIntervalMs: 6e4,
549
400
  /** How long a heartbeat ping may take before the session is treated as dead. */
@@ -578,13 +429,20 @@ function resolveAgentCapacity(env = process.env) {
578
429
  }
579
430
  //#endregion
580
431
  //#region package.json
581
- var version = "5.5.5";
432
+ var version = "5.6.0";
582
433
  //#endregion
583
434
  //#region src/operations/generations.ts
584
435
  const READ_CONCURRENCY = 50;
585
- const MB$1 = 1048576;
436
+ const MB = 1048576;
586
437
  const INDEX_KEY = "studio/generations.json";
438
+ /**
439
+ * The path form {@link SourceFiles} keys on: relative to `root`, with forward slashes.
440
+ */
441
+ function relativeStoragePath({ root, filePath }) {
442
+ return (isAbsolute(filePath) ? relative(resolve(root), filePath) : filePath).replaceAll("\\", "/");
443
+ }
587
444
  const hashOf = (content) => createHash("sha1").update(content).digest("hex").slice(0, 16);
445
+ const totalBytes = (entries) => entries.reduce((sum, { output, disk }) => sum + output.bytes + (disk?.bytes ?? 0), 0);
588
446
  /**
589
447
  * What the output directory holds on disk before a run. `undefined` when `outputPath` is not a real
590
448
  * subdirectory of `root` (listing the root would take in every source file) or holds more than `maxFiles`.
@@ -627,7 +485,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
627
485
  * Copies `files` into the store as one set of `jobId`. Above `maxSetMb` only the hashes are kept.
628
486
  */
629
487
  async function keep({ jobId, source, files, maxSetMb }) {
630
- const maxSetBytes = maxSetMb * MB$1;
488
+ const maxSetBytes = maxSetMb * MB;
631
489
  const hashes = {};
632
490
  let bytes = 0;
633
491
  await inParallel({
@@ -665,7 +523,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
665
523
  get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId && isLive(generation)),
666
524
  latest: async () => (await load()).filter(isLive).at(-1),
667
525
  /** Total bytes of every set the store holds, expired ones too until the next add drops them. */
668
- bytes: async () => (await load()).reduce((sum, { output, disk }) => sum + output.bytes + (disk?.bytes ?? 0), 0),
526
+ bytes: async () => totalBytes(await load()),
669
527
  async add(generation) {
670
528
  const current = await load();
671
529
  for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
@@ -674,8 +532,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
674
532
  ...generation,
675
533
  keptAt: now()
676
534
  });
677
- const weight = () => entries.reduce((sum, { output, disk }) => sum + output.bytes + (disk?.bytes ?? 0), 0);
678
- while (entries.length > 1 && (entries.length > maxCount || weight() > maxMb * MB$1)) await drop(entries.shift().jobId);
535
+ while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
679
536
  index = entries;
680
537
  await storage.writeItem(INDEX_KEY, JSON.stringify(entries));
681
538
  },
@@ -714,40 +571,11 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
714
571
  }
715
572
  //#endregion
716
573
  //#region src/operations/generationEvents.ts
717
- const require = createRequire(import.meta.url);
718
- function relativeStoragePath(root, filePath) {
719
- return (isAbsolute(filePath) ? relative(resolve(root), filePath) : filePath).replaceAll("\\", "/");
720
- }
721
- async function resolvePeerDependencies(names) {
722
- const uniqueNames = [...new Set(names.map(toPackageName))];
723
- const peerDependencies = {};
724
- const missingDependencies = [];
725
- const versions = await Promise.all(uniqueNames.map(async (name) => {
726
- try {
727
- const path = require.resolve(`${name}/package.json`);
728
- return JSON.parse(await readFile(path, "utf8")).version;
729
- } catch {
730
- return;
731
- }
732
- }));
733
- for (const [index, name] of uniqueNames.entries()) {
734
- const version = versions[index];
735
- if (version) {
736
- peerDependencies[name] = version;
737
- continue;
738
- }
739
- missingDependencies.push(name);
740
- }
741
- return {
742
- peerDependencies,
743
- missingDependencies
744
- };
745
- }
746
574
  const MAX_QUEUED_EVENTS = 1024;
747
575
  const RESERVED_EVENTS = 64;
748
576
  const isDiscardable = (event) => event.type === "kubb:files:processing:update" || event.type === "kubb:info" || event.type === "kubb:success";
749
577
  /** Forwards selected Kubb lifecycle events to a native Cap'n Web stream. */
750
- function createGenerationStream(hooks, jobId, options = {}) {
578
+ function createGenerationStream(hooks, jobId) {
751
579
  const unhooks = [];
752
580
  let root = "";
753
581
  let controller;
@@ -784,129 +612,92 @@ function createGenerationStream(hooks, jobId, options = {}) {
784
612
  }
785
613
  controller.enqueue(event);
786
614
  }
787
- on("kubb:plugin:start", (ctx) => {
788
- emitEvent("kubb:plugin:start", [{ plugin: { name: ctx.plugin.name } }]);
789
- });
790
- on("kubb:plugin:end", (ctx) => {
791
- emitEvent("kubb:plugin:end", [{
792
- plugin: { name: ctx.plugin.name },
793
- duration: ctx.duration,
794
- success: ctx.success
795
- }]);
796
- });
797
- on("kubb:build:start", ({ config, adapter }) => {
798
- root = config.root;
799
- emitEvent("kubb:build:start", [{
615
+ const projections = {
616
+ "kubb:plugin:start": ({ plugin }) => [{ plugin: { name: plugin.name } }],
617
+ "kubb:plugin:end": ({ plugin, duration, success }) => [{
618
+ plugin: { name: plugin.name },
619
+ duration,
620
+ success
621
+ }],
622
+ "kubb:build:start": ({ config, adapter }) => [{
800
623
  config: { name: config.name },
801
624
  adapter: { name: adapter.name }
802
- }]);
803
- });
804
- on("kubb:build:end", ({ files, config, outputDir }) => {
805
- emitEvent("kubb:build:end", [{
625
+ }],
626
+ "kubb:build:end": ({ files, config, outputDir }) => [{
806
627
  files: files.map((file) => ({
807
- path: relativeStoragePath(config.root, file.path),
628
+ path: relativeStoragePath({
629
+ root: config.root,
630
+ filePath: file.path
631
+ }),
808
632
  name: file.name
809
633
  })),
810
634
  outputDir
811
- }]);
812
- });
813
- on("kubb:files:processing:start", ({ files }) => {
814
- emitEvent("kubb:files:processing:start", [{ total: files.length }]);
815
- });
816
- on("kubb:files:processing:update", ({ files }) => {
817
- emitEvent("kubb:files:processing:update", [{ files: files.map(({ file, processed, total, percentage }) => ({
818
- file: relativeStoragePath(root, file.path),
635
+ }],
636
+ "kubb:files:processing:start": ({ files }) => [{ total: files.length }],
637
+ "kubb:files:processing:update": ({ files }) => [{ files: files.map(({ file, processed, total, percentage }) => ({
638
+ file: relativeStoragePath({
639
+ root,
640
+ filePath: file.path
641
+ }),
819
642
  processed,
820
643
  total,
821
644
  percentage
822
- })) }]);
823
- });
824
- on("kubb:files:processing:end", ({ files }) => {
825
- emitEvent("kubb:files:processing:end", [{ total: files.length }]);
826
- });
827
- for (const type of [
828
- "kubb:info",
829
- "kubb:success",
830
- "kubb:warn"
831
- ]) on(type, ({ message, info }) => {
832
- emitEvent(type, [{
833
- message,
645
+ })) }],
646
+ "kubb:files:processing:end": ({ files }) => [{ total: files.length }],
647
+ "kubb:info": ({ message, info }) => [{
648
+ message: stripVTControlCharacters(message),
834
649
  info
835
- }]);
836
- });
837
- on("kubb:generation:start", ({ config }) => {
838
- emitEvent("kubb:generation:start", [{
839
- name: config.name,
840
- plugins: config.plugins.length
841
- }]);
842
- });
843
- on("kubb:generation:end", async ({ config, storage, diagnostics = [], status, hrStart, filesCreated }) => {
844
- const { peerDependencies, missingDependencies } = await resolvePeerDependencies(config.plugins.map(({ name }) => name));
845
- const keys = await storage.readKeys();
846
- const paths = new Set(keys.map((key) => relativeStoragePath(config.root, key)));
847
- options.onGenerationEnd?.({
848
- output: {
849
- storage,
850
- root: config.root,
851
- paths
852
- },
853
- peerDependencies,
854
- missingDependencies
855
- });
856
- emitEvent("kubb:generation:end", []);
857
- if (!hrStart) return;
858
- emitEvent("kubb:generation:summary", [{
859
- duration: Math.round(getElapsedMs(hrStart)),
860
- fileCount: filesCreated ?? 0,
861
- failedPlugins: Diagnostics.failedPlugins(diagnostics).length,
862
- status: status ?? "success"
863
- }]);
864
- });
865
- on("kubb:error", ({ error }) => {
866
- emitEvent("kubb:error", [{
650
+ }],
651
+ "kubb:success": ({ message, info }) => [{
652
+ message: stripVTControlCharacters(message),
653
+ info
654
+ }],
655
+ "kubb:warn": ({ message, info }) => [{
656
+ message: stripVTControlCharacters(message),
657
+ info
658
+ }],
659
+ "kubb:error": ({ error }) => [{
867
660
  message: error.message,
868
661
  stack: error.stack
869
- }]);
870
- });
871
- on("kubb:diagnostic", ({ diagnostic }) => {
872
- const cause = "cause" in diagnostic ? diagnostic.cause : void 0;
873
- emitEvent("kubb:diagnostic", [{
662
+ }],
663
+ "kubb:diagnostic": ({ diagnostic }) => [{
874
664
  code: diagnostic.code,
875
665
  message: diagnostic.message,
876
666
  severity: diagnostic.severity,
877
667
  location: "location" in diagnostic ? diagnostic.location : void 0,
878
668
  help: "help" in diagnostic ? diagnostic.help : void 0,
879
669
  plugin: "plugin" in diagnostic ? diagnostic.plugin : void 0,
880
- stack: cause?.stack
881
- }]);
882
- });
883
- for (const type of [
884
- "kubb:lifecycle:start",
885
- "kubb:lifecycle:end",
886
- "kubb:format:start",
887
- "kubb:format:end",
888
- "kubb:lint:start",
889
- "kubb:lint:end",
890
- "kubb:hooks:start",
891
- "kubb:hooks:end"
892
- ]) on(type, () => {
893
- emitEvent(type, []);
894
- });
895
- on("kubb:hook:start", ({ id, command, args }) => {
896
- emitEvent("kubb:hook:start", [{
670
+ stack: "cause" in diagnostic ? diagnostic.cause?.stack : void 0
671
+ }],
672
+ "kubb:generation:start": ({ config }) => [{
673
+ name: config.name,
674
+ plugins: config.plugins.length
675
+ }],
676
+ "kubb:generation:end": () => [],
677
+ "kubb:generation:summary": ({ duration, fileCount, failedPlugins, status }) => [{
678
+ duration,
679
+ fileCount,
680
+ failedPlugins,
681
+ status
682
+ }],
683
+ "kubb:lifecycle:start": () => [],
684
+ "kubb:lifecycle:end": () => [],
685
+ "kubb:format:start": () => [],
686
+ "kubb:format:end": () => [],
687
+ "kubb:lint:start": () => [],
688
+ "kubb:lint:end": () => [],
689
+ "kubb:hooks:start": () => [],
690
+ "kubb:hooks:end": () => [],
691
+ "kubb:hook:start": ({ id, command, args }) => [{
897
692
  id,
898
693
  command,
899
694
  args: args ? [...args] : void 0
900
- }]);
901
- });
902
- on("kubb:hook:line", ({ id, line }) => {
903
- emitEvent("kubb:hook:line", [{
695
+ }],
696
+ "kubb:hook:line": ({ id, line }) => [{
904
697
  id,
905
698
  line
906
- }]);
907
- });
908
- on("kubb:hook:end", ({ id, command, args, success, error }) => {
909
- emitEvent("kubb:hook:end", [{
699
+ }],
700
+ "kubb:hook:end": ({ id, command, args, success, error }) => [{
910
701
  id,
911
702
  command,
912
703
  args: args ? [...args] : void 0,
@@ -915,6 +706,22 @@ function createGenerationStream(hooks, jobId, options = {}) {
915
706
  message: error.message,
916
707
  stack: error.stack
917
708
  } : void 0
709
+ }]
710
+ };
711
+ function forward(type) {
712
+ on(type, (...args) => emitEvent(type, projections[type](...args)));
713
+ }
714
+ on("kubb:build:start", ({ config }) => {
715
+ root = config.root;
716
+ });
717
+ for (const type of generationEventTypes) forward(type);
718
+ on("kubb:generation:end", ({ diagnostics = [], status, hrStart, filesCreated }) => {
719
+ if (!hrStart) return;
720
+ emitEvent("kubb:generation:summary", [{
721
+ duration: Math.round(getElapsedMs(hrStart)),
722
+ fileCount: filesCreated ?? 0,
723
+ failedPlugins: Diagnostics.failedPlugins(diagnostics).length,
724
+ status: status ?? "success"
918
725
  }]);
919
726
  });
920
727
  /**
@@ -944,22 +751,8 @@ function createGenerationStream(hooks, jobId, options = {}) {
944
751
  };
945
752
  }
946
753
  //#endregion
947
- //#region src/operations/websocket.ts
948
- const CONNECT_TIMEOUT_MS = 5e3;
949
- /**
950
- * Opens a Studio WebSocket connection and closes it when the initial handshake exceeds the configured timeout.
951
- */
952
- function createWebsocket(url, options) {
953
- const ws = new WebSocket(url, options);
954
- const timer = setTimeout(() => {
955
- if (ws.readyState === WebSocket.CONNECTING) ws.close(3008, "Connection timeout");
956
- }, CONNECT_TIMEOUT_MS);
957
- ws.once("open", () => clearTimeout(timer));
958
- ws.once("close", () => clearTimeout(timer));
959
- return ws;
960
- }
961
- //#endregion
962
754
  //#region src/operations/rpc.ts
755
+ const CONNECT_TIMEOUT_MS = 5e3;
963
756
  /**
964
757
  * The only methods Studio may call on an agent. A `StudioSession` carries far more than
965
758
  * {@link AgentApi}, so it is wrapped rather than exposed: what Cap'n Web can reach is exactly what
@@ -991,6 +784,41 @@ var AgentRpcTarget = class extends RpcTarget {
991
784
  }
992
785
  };
993
786
  /**
787
+ * Opens a Studio WebSocket, closing it when the handshake exceeds {@link CONNECT_TIMEOUT_MS}. Node
788
+ * fires no `close` event when the handshake itself fails, only `error`, so `closed` settles as 1006 there.
789
+ */
790
+ function openSocket({ url, headers }) {
791
+ const socket = new WebSocket(url, { headers });
792
+ return {
793
+ socket,
794
+ closed: new Promise((resolve) => {
795
+ let opened = false;
796
+ const timer = setTimeout(() => {
797
+ if (socket.readyState === WebSocket.CONNECTING) socket.close(3008, "Connection timeout");
798
+ }, CONNECT_TIMEOUT_MS);
799
+ socket.addEventListener("open", () => {
800
+ opened = true;
801
+ clearTimeout(timer);
802
+ }, { once: true });
803
+ socket.addEventListener("close", (event) => {
804
+ clearTimeout(timer);
805
+ resolve({
806
+ code: event.code,
807
+ reason: event.reason
808
+ });
809
+ }, { once: true });
810
+ socket.addEventListener("error", () => {
811
+ if (opened) return;
812
+ clearTimeout(timer);
813
+ resolve({
814
+ code: 1006,
815
+ reason: ""
816
+ });
817
+ });
818
+ })
819
+ };
820
+ }
821
+ /**
994
822
  * Opens an authenticated Cap'n Web session to Studio over a WebSocket. Rejects an unencrypted URL
995
823
  * before opening the socket, so a bearer token never reaches a plaintext host.
996
824
  *
@@ -1002,15 +830,14 @@ var AgentRpcTarget = class extends RpcTarget {
1002
830
  */
1003
831
  const connectWebSocketRpc = async ({ url, token, instanceId, local }) => {
1004
832
  const { protocol, hostname, host } = new URL(url);
1005
- if (protocol !== "wss:" && !(protocol === "ws:" && (hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]"))) throw new Error(`Refusing unencrypted WebSocket to ${host}`);
1006
- const socket = createWebsocket(url, { headers: {
1007
- Authorization: `Bearer ${token}`,
1008
- [AGENT_INSTANCE_HEADER]: instanceId
1009
- } });
1010
- const closed = new Promise((resolve) => socket.once("close", (code, reason) => resolve({
1011
- code,
1012
- reason: reason.toString()
1013
- })));
833
+ if (protocol !== "wss:" && !(protocol === "ws:" && isLoopbackHost(hostname))) throw new Error(`Refusing unencrypted WebSocket to ${host}`);
834
+ const { socket, closed } = openSocket({
835
+ url,
836
+ headers: {
837
+ Authorization: `Bearer ${token}`,
838
+ [AGENT_INSTANCE_HEADER]: instanceId
839
+ }
840
+ });
1014
841
  const studio = newWebSocketRpcSession(socket, new AgentRpcTarget(local));
1015
842
  studio.onRpcBroken(() => socket.close());
1016
843
  return {
@@ -1020,256 +847,16 @@ const connectWebSocketRpc = async ({ url, token, instanceId, local }) => {
1020
847
  };
1021
848
  };
1022
849
  //#endregion
1023
- //#region src/operations/hooks.ts
1024
- /**
1025
- * Register a `kubb:hook:start` listener that spawns the requested command via tinyexec,
1026
- * streams each stdout line as a `kubb:hook:line` event, and calls `kubb:hook:end` with the result.
1027
- * A failure travels on `kubb:hook:end` only: the caller waiting on it reports `kubb:error` once.
1028
- * Streaming the output lets Kubb Studio render live hook progress over the WebSocket connection.
1029
- *
1030
- * Returns a remover, so a session that runs one generation after another on the same emitter does
1031
- * not stack a listener per run.
1032
- */
1033
- function setupHookListener(hooks, root, signal) {
1034
- return hooks.hook("kubb:hook:start", async (ctx) => {
1035
- const { id, command, args } = ctx;
1036
- if (!id) return;
1037
- const commandWithArgs = args?.length ? `${command} ${args.join(" ")}` : command;
1038
- try {
1039
- const proc = x(command, [...args ?? []], {
1040
- signal,
1041
- nodeOptions: {
1042
- cwd: root,
1043
- detached: true
1044
- }
1045
- });
1046
- for await (const line of proc) await hooks.callHook("kubb:hook:line", {
1047
- id,
1048
- line
1049
- });
1050
- const { exitCode } = await proc;
1051
- if (exitCode !== 0) {
1052
- const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
1053
- await hooks.callHook("kubb:hook:end", {
1054
- id,
1055
- command,
1056
- args,
1057
- success: false,
1058
- error
1059
- });
1060
- return;
1061
- }
1062
- await hooks.callHook("kubb:hook:end", {
1063
- id,
1064
- command,
1065
- args,
1066
- success: true,
1067
- error: null
1068
- });
1069
- } catch (caughtError) {
1070
- const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
1071
- error.cause = caughtError;
1072
- await hooks.callHook("kubb:hook:end", {
1073
- id,
1074
- command,
1075
- args,
1076
- success: false,
1077
- error
1078
- });
1079
- }
1080
- });
1081
- }
1082
- /**
1083
- * Waits for the `kubb:hook:end` matching `hookId`. Register this before calling `kubb:hook:start`:
1084
- * `callHook` awaits its listeners, and {@link setupHookListener} calls `kubb:hook:end` from inside
1085
- * that same listener, so a handler added afterward would already have missed it.
1086
- */
1087
- function waitForHookEnd(hooks, hookId) {
1088
- return new Promise((resolve, reject) => {
1089
- const handleHookEnd = (ctx) => {
1090
- if (ctx.id !== hookId) return;
1091
- hooks.removeHook("kubb:hook:end", handleHookEnd);
1092
- if (ctx.success) {
1093
- resolve();
1094
- return;
1095
- }
1096
- reject(ctx.error);
1097
- };
1098
- hooks.hook("kubb:hook:end", handleHookEnd);
1099
- });
1100
- }
1101
- //#endregion
1102
- //#region src/operations/generate.ts
850
+ //#region src/runtime/StudioSession.ts
1103
851
  const DISK_SNAPSHOT_MAX_FILES = 1e4;
1104
852
  /**
1105
- * `isToolAvailable` spawns a process, and a long-lived connection generates repeatedly, so each
1106
- * executable is probed once per process. The CLI deliberately does not memoize: a `--watch` build
1107
- * should keep noticing a tool installed mid-session.
853
+ * Folds error-severity diagnostics into one thrown error so logs name the failing plugin or pass.
1108
854
  */
1109
- const detectTool = memoize(/* @__PURE__ */ new Map(), detectTool$1);
1110
- /**
1111
- * The two post-build tool steps. Formatting and linting differ only in which tools they look for,
1112
- * so they run through one loop rather than two near-identical blocks.
1113
- *
1114
- * `noun` and `verbing` are spelled out instead of built from `kind`. Concatenating `` `${kind}ter` ``
1115
- * and `` `${kind}ting` `` works for `format`, but doubles the `t` in `lint`, giving "lintter" and
1116
- * "lintting" instead of "linter" and "linting".
1117
- */
1118
- const TOOL_STEPS = [{
1119
- kind: "format",
1120
- noun: "formatter",
1121
- verbing: "Formatting",
1122
- tools: formatters,
1123
- detect: FORMATTER_PREFERENCE
1124
- }, {
1125
- kind: "lint",
1126
- noun: "linter",
1127
- verbing: "Linting",
1128
- tools: linters,
1129
- detect: LINTER_PREFERENCE
1130
- }];
1131
- /**
1132
- * Absolute path of the directory the formatter and linter are pointed at.
1133
- */
1134
- function outputPath(config) {
1135
- return path.isAbsolute(config.output.path) ? config.output.path : path.resolve(process$1.cwd(), config.root, config.output.path);
1136
- }
1137
- /**
1138
- * Emits `kubb:hook:start` and waits for the matching `kubb:hook:end`. The host spawns the process:
1139
- * this only describes what to run and when it finished.
1140
- *
1141
- * @throws whatever the command failed with, so callers can report it their own way.
1142
- */
1143
- async function runHook({ hooks, id, command, args }) {
1144
- const hookId = hash("sha256", id);
1145
- const hookEnd = waitForHookEnd(hooks, hookId);
1146
- await hooks.callHook("kubb:hook:start", {
1147
- id: hookId,
1148
- command,
1149
- args: [...args]
1150
- });
1151
- await hookEnd;
1152
- }
1153
- function isProblemErrorDiagnostic(diagnostic) {
1154
- return (diagnostic.kind ?? "problem") === "problem" && diagnostic.severity === "error";
1155
- }
1156
- /**
1157
- * Folds error-severity diagnostics into one thrown error so logs name the failing plugin.
1158
- */
1159
- function formatGenerationFailure(diagnostics) {
1160
- const reasons = diagnostics.filter(isProblemErrorDiagnostic).map((diagnostic) => diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message);
855
+ function generationFailure(diagnostics) {
856
+ const reasons = diagnostics.filter(Diagnostics.isProblem).filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message);
1161
857
  if (!reasons.length) return /* @__PURE__ */ new Error("Generation failed");
1162
858
  return /* @__PURE__ */ new Error(`Generation failed: ${reasons.length} error${reasons.length === 1 ? "" : "s"}: ${reasons.join("; ")}`);
1163
859
  }
1164
- /**
1165
- * Runs a full Kubb code-generation cycle for the given config.
1166
- *
1167
- * Emits lifecycle events on the provided `hooks` emitter so callers (e.g. the WebSocket stream)
1168
- * can forward progress to connected clients. After a successful build, auto-formatting and
1169
- * linting are applied when configured, followed by any user-defined `hooks.done` commands.
1170
- */
1171
- async function generate({ config, hooks, signal }) {
1172
- signal?.throwIfAborted();
1173
- const hrStart = process$1.hrtime();
1174
- await hooks.callHook("kubb:generation:start", { config });
1175
- await hooks.callHook("kubb:info", { message: config.name ? `Setup generation ${config.name}` : "Setup generation" });
1176
- const kubb = createKubb(config, {
1177
- hooks,
1178
- signal
1179
- });
1180
- await kubb.setup();
1181
- await hooks.callHook("kubb:info", { message: config.name ? `Build generation ${config.name}` : "Build generation" });
1182
- const { files, diagnostics, storage } = await kubb.safeBuild();
1183
- signal?.throwIfAborted();
1184
- await hooks.callHook("kubb:info", { message: "Load summary" });
1185
- for (const diagnostic of diagnostics.filter(isProblemErrorDiagnostic)) await hooks.callHook("kubb:error", { error: new Error(diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message) });
1186
- const status = Diagnostics.hasError(diagnostics) ? "failed" : "success";
1187
- await hooks.callHook("kubb:generation:end", {
1188
- config,
1189
- storage: {
1190
- ...storage,
1191
- readKeys: async () => [...new Set(files.map((file) => file.path))]
1192
- },
1193
- diagnostics,
1194
- status,
1195
- hrStart,
1196
- filesCreated: files.length
1197
- });
1198
- if (status === "failed") throw formatGenerationFailure(diagnostics);
1199
- await hooks.callHook("kubb:success", { message: "Generation successfully" });
1200
- for (const step of TOOL_STEPS) {
1201
- const setting = config.output[step.kind];
1202
- if (!setting) continue;
1203
- await hooks.callHook(`kubb:${step.kind}:start`);
1204
- const tool = setting === "auto" ? await detectTool(step.detect) : setting;
1205
- if (!tool) await hooks.callHook("kubb:warn", { message: `No ${step.noun} found (${step.detect.join(", ")}). Skipping ${step.verbing.toLowerCase()}.` });
1206
- if (tool && setting === "auto") await hooks.callHook("kubb:info", { message: `Auto-detected ${step.noun}: ${styleText("dim", tool)}` });
1207
- const command = tool ? step.tools[tool] : void 0;
1208
- if (command) try {
1209
- await runHook({
1210
- hooks,
1211
- id: [config.name, tool].filter(Boolean).join("-"),
1212
- command: command.command,
1213
- args: command.args(outputPath(config))
1214
- });
1215
- await hooks.callHook("kubb:success", { message: `${step.verbing} with ${tool} successfully` });
1216
- } catch (caughtError) {
1217
- await hooks.callHook("kubb:error", { error: new Error(command.errorMessage, { cause: caughtError }) });
1218
- signal?.throwIfAborted();
1219
- }
1220
- await hooks.callHook(`kubb:${step.kind}:end`);
1221
- }
1222
- if (config.output.postGenerate?.length) {
1223
- await hooks.callHook("kubb:hooks:start");
1224
- for (const entry of config.output.postGenerate) {
1225
- const line = typeof entry === "string" ? entry : entry.command;
1226
- const [cmd, ...args] = tokenize(line);
1227
- if (!cmd) continue;
1228
- try {
1229
- await runHook({
1230
- hooks,
1231
- id: line,
1232
- command: cmd,
1233
- args
1234
- });
1235
- } catch (error) {
1236
- await hooks.callHook("kubb:error", { error: toError(error) });
1237
- throw error;
1238
- }
1239
- await hooks.callHook("kubb:success", { message: `${line} successfully executed` });
1240
- }
1241
- await hooks.callHook("kubb:hooks:end");
1242
- }
1243
- }
1244
- async function runGenerationOperation({ config, hooks, signal, jobId, store, snapshotRoot, maxSnapshotMb }) {
1245
- const diskFiles = snapshotRoot ? await listDisk({
1246
- root: snapshotRoot,
1247
- outputPath: config.output.path,
1248
- maxFiles: DISK_SNAPSHOT_MAX_FILES
1249
- }) : void 0;
1250
- const disk = diskFiles ? await store.keep({
1251
- jobId,
1252
- source: "disk",
1253
- files: diskFiles,
1254
- maxSetMb: maxSnapshotMb
1255
- }) : void 0;
1256
- const removeHookListener = setupHookListener(hooks, config.root, signal);
1257
- try {
1258
- await generate({
1259
- config,
1260
- hooks,
1261
- signal
1262
- });
1263
- } catch (error) {
1264
- await store.drop(jobId);
1265
- throw error;
1266
- } finally {
1267
- removeHookListener();
1268
- }
1269
- return disk;
1270
- }
1271
- //#endregion
1272
- //#region src/runtime/StudioSession.ts
1273
860
  /** A sandbox shares one in-memory generation store across tenants, so old generations expire. */
1274
861
  const SANDBOX_GENERATION_TTL_MS = 9e5;
1275
862
  /**
@@ -1336,7 +923,6 @@ function applyStudioDefaults(options) {
1336
923
  allowRead: false,
1337
924
  ...options.permissions
1338
925
  },
1339
- retryInterval: options.retryInterval ?? agentDefaults.retryIntervalMs,
1340
926
  heartbeatInterval: Math.min(options.heartbeatInterval ?? agentDefaults.heartbeatIntervalMs, agentDefaults.maxHeartbeatIntervalMs),
1341
927
  capacity: {
1342
928
  ...resolveAgentCapacity(),
@@ -1350,10 +936,6 @@ function applyStudioDefaults(options) {
1350
936
  * and with it each other's events, so the connection keeps this limit.
1351
937
  */
1352
938
  const RUNTIME_MAX_CONCURRENT = 1;
1353
- const MB = 1048576;
1354
- function rssMb() {
1355
- return process$1.memoryUsage().rss / MB;
1356
- }
1357
939
  /**
1358
940
  * Reads what Studio meant by closing the connection. A code Studio did not send on purpose is an
1359
941
  * ordinary drop, and the agent reconnects as it always has.
@@ -1401,12 +983,10 @@ var StudioSession = class {
1401
983
  */
1402
984
  #registration;
1403
985
  #rpc;
1404
- #studioVersion;
1405
986
  #disposed = false;
1406
987
  #isGenerating = false;
1407
988
  #activeJob;
1408
989
  #heartbeatTimer;
1409
- #lastGeneration;
1410
990
  #store;
1411
991
  #limits = resolveGenerationLimits();
1412
992
  /**
@@ -1473,7 +1053,6 @@ var StudioSession = class {
1473
1053
  });
1474
1054
  signal?.throwIfAborted();
1475
1055
  this.#registration = registration;
1476
- this.#studioVersion = registration.version;
1477
1056
  const rpc = await (this.#options.connector ?? connectWebSocketRpc)({
1478
1057
  url: registration.socketUrl,
1479
1058
  token,
@@ -1490,7 +1069,7 @@ var StudioSession = class {
1490
1069
  await this.#hooks.callHook("studio:connected", {
1491
1070
  url: studioUrl,
1492
1071
  versions: {
1493
- studio: this.#studioVersion,
1072
+ studio: registration.version,
1494
1073
  kubb: version,
1495
1074
  agent: this.#options.version
1496
1075
  },
@@ -1522,6 +1101,19 @@ var StudioSession = class {
1522
1101
  await this.#warn(reason, permission);
1523
1102
  throw new Error(message);
1524
1103
  }
1104
+ /**
1105
+ * Brackets one Studio command with `studio:command:start` and `studio:command:end`. A command
1106
+ * that declined its request reports no `info`, and only the start is heard.
1107
+ */
1108
+ async #command({ command, run }) {
1109
+ await this.#hooks.callHook("studio:command:start", { command });
1110
+ const { value, info } = await run();
1111
+ if (info !== void 0) await this.#hooks.callHook("studio:command:end", {
1112
+ command,
1113
+ info
1114
+ });
1115
+ return value;
1116
+ }
1525
1117
  #scheduleHeartbeat(interval) {
1526
1118
  const rpc = this.#rpc;
1527
1119
  if (!rpc) return;
@@ -1551,7 +1143,7 @@ var StudioSession = class {
1551
1143
  async #load() {
1552
1144
  return {
1553
1145
  running: this.#isGenerating ? 1 : 0,
1554
- rssMb: Math.round(rssMb()),
1146
+ rssMb: Math.round(process$1.memoryUsage().rss / 1048576),
1555
1147
  storeBytes: await this.#generations.bytes(),
1556
1148
  accepting: true
1557
1149
  };
@@ -1565,7 +1157,7 @@ var StudioSession = class {
1565
1157
  async #readConfigFileView(source) {
1566
1158
  if (!this.#canEditConfig) return;
1567
1159
  try {
1568
- const { readConfig } = await import("./configFile-BymXUi9P.js");
1160
+ const { readConfig } = await import("./configFile-zhp5DQIe.js");
1569
1161
  return readConfig(source ?? await read(this.#options.configFile));
1570
1162
  } catch (error) {
1571
1163
  await this.#warn(`Could not read ${this.#options.configFile}: ${getErrorMessage(error)}`);
@@ -1633,9 +1225,7 @@ var StudioSession = class {
1633
1225
  }
1634
1226
  }
1635
1227
  startGeneration(data) {
1636
- const generationStream = createGenerationStream(this.#hooks, data.jobId, { onGenerationEnd: (result) => {
1637
- this.#lastGeneration = result;
1638
- } });
1228
+ const generationStream = createGenerationStream(this.#hooks, data.jobId);
1639
1229
  const controller = new AbortController();
1640
1230
  const cancelRun = async () => {
1641
1231
  controller.abort(/* @__PURE__ */ new Error("Generation canceled"));
@@ -1670,89 +1260,116 @@ var StudioSession = class {
1670
1260
  jobId: data.jobId,
1671
1261
  cancel: cancelRun
1672
1262
  };
1673
- const command = "generate";
1674
- const { loadConfig, permissions } = this.#options;
1675
1263
  let root = this.#options.root;
1676
1264
  try {
1677
1265
  if (this.#isSandbox) root = await createJobRoot();
1678
- await this.#hooks.callHook("studio:command:start", { command });
1679
- const config = await loadConfig();
1680
- const patch = data.config;
1681
- const plugins = await mergePlugins(config.plugins, patch?.plugins);
1682
- const adapter = await mergeAdapter(config.adapter, patch?.adapter);
1683
- const inputOverride = this.#isSandbox ? patch?.input ?? "" : void 0;
1684
- if (permissions.allowWrite && this.#isSandbox) await this.#warn("Running in a sandbox, so writing files is disabled");
1685
- if (patch?.input && !this.#isSandbox) await this.#warn("Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox agent");
1686
- const resolvedPlugins = plugins ?? config.plugins;
1687
- this.#lastGeneration = void 0;
1688
- const disk = await runGenerationOperation({
1689
- config: {
1690
- ...config,
1266
+ return await this.#command({
1267
+ command: "generate",
1268
+ run: () => this.#generate({
1269
+ data,
1691
1270
  root,
1692
- input: inputOverride ?? config.input,
1693
- storage: this.#canWrite ? fsStorage() : memoryStorage(),
1694
- output: permissions.allowExec ? { ...config.output } : {
1695
- ...config.output,
1696
- format: false,
1697
- lint: false,
1698
- postGenerate: []
1699
- },
1700
- plugins: resolvedPlugins,
1701
- adapter
1702
- },
1703
- hooks: this.#hooks,
1704
- signal: controller.signal,
1705
- jobId: data.jobId,
1706
- store: this.#generations,
1707
- snapshotRoot: this.#hasProjectOnDisk ? root : void 0,
1708
- maxSnapshotMb: this.#limits.maxSnapshotMb
1709
- });
1710
- await this.#hooks.callHook("studio:command:end", {
1711
- command,
1712
- info: `${resolvedPlugins.length} plugin${resolvedPlugins.length === 1 ? "" : "s"}, ${this.#canWrite ? "written to disk" : "in memory"}${inputOverride !== void 0 ? ", from a Studio spec" : ""}`
1271
+ signal: controller.signal
1272
+ })
1713
1273
  });
1714
- const generation = this.#lastGeneration;
1715
- const output = generation ? await this.#generations.keep({
1716
- jobId: data.jobId,
1717
- source: "output",
1718
- files: generation.output,
1719
- maxSetMb: this.#limits.maxMb
1720
- }) : void 0;
1721
- if (generation && output) {
1722
- if (!output.paths.length && Object.keys(output.hashes).length) await this.#warn("Kept only the hashes of this generation: its output is too large to keep");
1723
- const { peerDependencies, missingDependencies } = generation;
1724
- await this.#generations.add({
1725
- jobId: data.jobId,
1726
- output,
1727
- disk,
1728
- peerDependencies,
1729
- missingDependencies
1730
- });
1731
- }
1732
- const files = [...generation?.output.paths ?? []];
1733
- return {
1734
- status: "success",
1735
- files,
1736
- fileCount: files.length,
1737
- hashes: output?.hashes ?? {},
1738
- disk: disk ? { hashes: disk.hashes } : void 0
1739
- };
1740
1274
  } finally {
1741
- this.#lastGeneration = void 0;
1742
1275
  if (root !== this.#options.root) await removeJobRoot(root);
1743
1276
  this.#isGenerating = false;
1744
1277
  }
1745
1278
  }
1746
- async saveConfig(data) {
1747
- const command = "saveConfig";
1748
- await this.#hooks.callHook("studio:command:start", { command });
1279
+ async #generate({ data, root, signal }) {
1280
+ const { loadConfig, permissions } = this.#options;
1281
+ const config = await loadConfig();
1282
+ const patch = data.config;
1283
+ const plugins = await mergePlugins(config.plugins, patch?.plugins);
1284
+ const adapter = await mergeAdapter(config.adapter, patch?.adapter);
1285
+ const inputOverride = this.#isSandbox ? patch?.input ?? "" : void 0;
1286
+ if (permissions.allowWrite && this.#isSandbox) await this.#warn("Running in a sandbox, so writing files is disabled");
1287
+ if (patch?.input && !this.#isSandbox) await this.#warn("Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox agent");
1288
+ const resolvedPlugins = plugins ?? config.plugins;
1289
+ const storage = this.#canWrite ? fsStorage() : memoryStorage();
1290
+ const diskFiles = this.#hasProjectOnDisk ? await listDisk({
1291
+ root,
1292
+ outputPath: config.output.path,
1293
+ maxFiles: DISK_SNAPSHOT_MAX_FILES
1294
+ }) : void 0;
1295
+ const disk = diskFiles ? await this.#generations.keep({
1296
+ jobId: data.jobId,
1297
+ source: "disk",
1298
+ files: diskFiles,
1299
+ maxSetMb: this.#limits.maxSnapshotMb
1300
+ }) : void 0;
1301
+ const result = await createKubb({
1302
+ ...config,
1303
+ root,
1304
+ input: inputOverride ?? config.input,
1305
+ storage,
1306
+ output: permissions.allowExec ? { ...config.output } : {
1307
+ ...config.output,
1308
+ format: false,
1309
+ lint: false,
1310
+ postGenerate: []
1311
+ },
1312
+ plugins: resolvedPlugins,
1313
+ adapter
1314
+ }, {
1315
+ hooks: this.#hooks,
1316
+ signal
1317
+ }).generate().catch(async (error) => {
1318
+ await this.#generations.drop(data.jobId);
1319
+ throw error;
1320
+ });
1321
+ if (!result.success) {
1322
+ await this.#generations.drop(data.jobId);
1323
+ throw generationFailure(result.diagnostics);
1324
+ }
1325
+ const paths = new Set(result.files.map((file) => relativeStoragePath({
1326
+ root,
1327
+ filePath: file.path
1328
+ })));
1329
+ const output = await this.#generations.keep({
1330
+ jobId: data.jobId,
1331
+ source: "output",
1332
+ files: {
1333
+ storage,
1334
+ root,
1335
+ paths
1336
+ },
1337
+ maxSetMb: this.#limits.maxMb
1338
+ });
1339
+ if (!output.paths.length && Object.keys(output.hashes).length) await this.#warn("Kept only the hashes of this generation: its output is too large to keep");
1340
+ const { peerDependencies, missingDependencies } = await resolvePeerDependencies(resolvedPlugins.map(({ name }) => name));
1341
+ await this.#generations.add({
1342
+ jobId: data.jobId,
1343
+ output,
1344
+ disk,
1345
+ peerDependencies,
1346
+ missingDependencies
1347
+ });
1348
+ return {
1349
+ value: {
1350
+ status: "success",
1351
+ files: [...paths],
1352
+ fileCount: paths.size,
1353
+ hashes: output.hashes,
1354
+ disk: disk ? { hashes: disk.hashes } : void 0
1355
+ },
1356
+ info: `${resolvedPlugins.length} plugin${resolvedPlugins.length === 1 ? "" : "s"}, ${this.#canWrite ? "written to disk" : "in memory"}${inputOverride !== void 0 ? ", from a Studio spec" : ""}`
1357
+ };
1358
+ }
1359
+ saveConfig(data) {
1360
+ return this.#command({
1361
+ command: "saveConfig",
1362
+ run: () => this.#saveConfig(data)
1363
+ });
1364
+ }
1365
+ async #saveConfig(data) {
1749
1366
  const { configPath, configFile } = this.#options;
1750
1367
  if (!Array.isArray(data.edits)) {
1751
1368
  await this.#warn("Ignored save: the message carried no edits");
1752
- return {
1369
+ return { value: {
1753
1370
  outcomes: [],
1754
1371
  changed: false
1755
- };
1372
+ } };
1756
1373
  }
1757
1374
  const edits = data.edits;
1758
1375
  const refuse = (reason) => ({
@@ -1765,33 +1382,36 @@ var StudioSession = class {
1765
1382
  });
1766
1383
  if (!this.#canEditConfig) {
1767
1384
  await this.#warn("Ignored save: editing kubb.config.ts was not granted");
1768
- return refuse("the agent was not granted permission to edit kubb.config.ts");
1385
+ return { value: refuse("the agent was not granted permission to edit kubb.config.ts") };
1769
1386
  }
1770
- if (this.#isGenerating) return refuse("a generation is in progress");
1387
+ if (this.#isGenerating) return { value: refuse("a generation is in progress") };
1771
1388
  try {
1772
- const { writeConfigEdits } = await import("./configFile-BymXUi9P.js");
1389
+ const { writeConfigEdits } = await import("./configFile-zhp5DQIe.js");
1773
1390
  const { source: patched, outcomes, changed } = await writeConfigEdits({
1774
1391
  filePath: configFile,
1775
1392
  edits
1776
1393
  });
1777
1394
  const applied = outcomes.filter((outcome) => outcome.applied).length;
1778
- await this.#hooks.callHook("studio:command:end", {
1779
- command,
1780
- info: `applied ${applied}/${outcomes.length} edits to ${configPath}`
1781
- });
1782
1395
  return {
1783
- outcomes,
1784
- changed,
1785
- file: changed ? await this.#readConfigFileView(patched) : void 0
1396
+ value: {
1397
+ outcomes,
1398
+ changed,
1399
+ file: changed ? await this.#readConfigFileView(patched) : void 0
1400
+ },
1401
+ info: `applied ${applied}/${outcomes.length} edits to ${configPath}`
1786
1402
  };
1787
1403
  } catch (error) {
1788
1404
  await this.#hooks.callHook("studio:error", { error: toError(error) });
1789
- return refuse(getErrorMessage(error));
1405
+ return { value: refuse(getErrorMessage(error)) };
1790
1406
  }
1791
1407
  }
1792
- async publishSnapshot(data) {
1793
- const command = "snapshot";
1794
- await this.#hooks.callHook("studio:command:start", { command });
1408
+ publishSnapshot(data) {
1409
+ return this.#command({
1410
+ command: "snapshot",
1411
+ run: () => this.#publishSnapshot(data)
1412
+ });
1413
+ }
1414
+ async #publishSnapshot(data) {
1795
1415
  if (this.#isSandbox) return this.#refuse("Ignored snapshot: a sandbox agent has no project to build a package from", "A sandbox agent has no project to build a package from");
1796
1416
  const { name, version, bundledDependencies, uploadPath } = data;
1797
1417
  if (!name || !version || !uploadPath) return this.#refuse("Ignored snapshot: the message was missing required fields", "The request was missing required fields");
@@ -1801,7 +1421,7 @@ var StudioSession = class {
1801
1421
  const missing = generation.missingDependencies.filter((dependency) => !bundled.has(dependency));
1802
1422
  if (missing.length) return this.#refuse(`Ignored snapshot: missing dependencies: ${missing.join(", ")}`, `Missing dependencies: ${missing.join(", ")}`);
1803
1423
  try {
1804
- const { createSnapshotPackage, uploadSnapshot } = await import("./snapshotPackage-I8bt8KTR.js");
1424
+ const { createSnapshotPackage, uploadSnapshot } = await import("./snapshotPackage-BwZ8IJX-.js");
1805
1425
  const { files } = await this.#generations.read({
1806
1426
  generation,
1807
1427
  source: "output",
@@ -1820,13 +1440,12 @@ var StudioSession = class {
1820
1440
  token,
1821
1441
  shutdown: signal
1822
1442
  });
1823
- await this.#hooks.callHook("studio:command:end", {
1824
- command,
1825
- info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
1826
- });
1827
1443
  return {
1828
- integrity,
1829
- peerDependencies: generation.peerDependencies
1444
+ value: {
1445
+ integrity,
1446
+ peerDependencies: generation.peerDependencies
1447
+ },
1448
+ info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
1830
1449
  };
1831
1450
  } catch (error) {
1832
1451
  await this.#hooks.callHook("studio:error", { error: toError(error) });
@@ -1837,9 +1456,13 @@ var StudioSession = class {
1837
1456
  get #hasProjectOnDisk() {
1838
1457
  return !this.#isSandbox && this.#canRead;
1839
1458
  }
1840
- async readFiles(data) {
1841
- const command = "readFiles";
1842
- await this.#hooks.callHook("studio:command:start", { command });
1459
+ readFiles(data) {
1460
+ return this.#command({
1461
+ command: "readFiles",
1462
+ run: () => this.#readFiles(data)
1463
+ });
1464
+ }
1465
+ async #readFiles(data) {
1843
1466
  if (!this.#canRead) return this.#refuse("Ignored files: reading generated files was not granted", "The agent was not granted permission to read generated files", "allowRead");
1844
1467
  if (!Array.isArray(data.paths)) return this.#refuse("Ignored files: the message carried no paths", "The request carried no paths");
1845
1468
  const { paths } = data;
@@ -1857,11 +1480,10 @@ var StudioSession = class {
1857
1480
  cursor: data.cursor,
1858
1481
  limit: data.limit
1859
1482
  });
1860
- await this.#hooks.callHook("studio:command:end", {
1861
- command,
1483
+ return {
1484
+ value: result,
1862
1485
  info: `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`
1863
- });
1864
- return result;
1486
+ };
1865
1487
  }
1866
1488
  };
1867
1489
  //#endregion