@kubb/studio 5.5.6 → 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/{configFile-BTVeov27.cjs → configFile-dJtYeqX1.cjs} +27 -45
- package/dist/configFile-dJtYeqX1.cjs.map +1 -0
- package/dist/{configFile-BymXUi9P.js → configFile-zhp5DQIe.js} +25 -43
- package/dist/configFile-zhp5DQIe.js.map +1 -0
- package/dist/index.cjs +290 -669
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +54 -62
- package/dist/index.js +292 -670
- package/dist/index.js.map +1 -1
- package/dist/{resolveConfig-D7I3Nj60.cjs → resolveConfig-CjF6j-3y.cjs} +49 -10
- package/dist/resolveConfig-CjF6j-3y.cjs.map +1 -0
- package/dist/{resolveConfig-DMIG_MH-.js → resolveConfig-DOUv_Tid.js} +38 -11
- package/dist/resolveConfig-DOUv_Tid.js.map +1 -0
- package/dist/{snapshotPackage-BwM3LFmZ.cjs → snapshotPackage-BFjiRhGd.cjs} +4 -3
- package/dist/snapshotPackage-BFjiRhGd.cjs.map +1 -0
- package/dist/{snapshotPackage-I8bt8KTR.js → snapshotPackage-BwZ8IJX-.js} +4 -3
- package/dist/snapshotPackage-BwZ8IJX-.js.map +1 -0
- package/dist/url-DC87bYiU.cjs +19 -0
- package/dist/url-DC87bYiU.cjs.map +1 -0
- package/dist/url-DhFfJpxp.js +14 -0
- package/dist/url-DhFfJpxp.js.map +1 -0
- package/package.json +5 -8
- package/dist/configFile-BTVeov27.cjs.map +0 -1
- package/dist/configFile-BymXUi9P.js.map +0 -1
- package/dist/resolveConfig-D7I3Nj60.cjs.map +0 -1
- package/dist/resolveConfig-DMIG_MH-.js.map +0 -1
- package/dist/snapshotPackage-BwM3LFmZ.cjs.map +0 -1
- package/dist/snapshotPackage-I8bt8KTR.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { a as
|
|
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 {
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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())
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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(
|
|
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
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
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
|
-
|
|
825
|
-
|
|
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
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
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
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
"kubb:
|
|
887
|
-
"kubb:
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
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
|
|
1006
|
-
const socket =
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
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/
|
|
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
|
-
*
|
|
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
|
-
|
|
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:
|
|
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(
|
|
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-
|
|
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
|
|
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.#
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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
|
-
|
|
1693
|
-
|
|
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
|
|
1747
|
-
const
|
|
1748
|
-
|
|
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-
|
|
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
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
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
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
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-
|
|
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
|
-
|
|
1829
|
-
|
|
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
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
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
|
-
|
|
1861
|
-
|
|
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
|