@kubb/studio 5.6.0 → 5.6.1
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/README.md +80 -169
- package/dist/index.cjs +1466 -964
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +112 -406
- package/dist/index.js +1441 -947
- package/dist/index.js.map +1 -1
- package/dist/protocol-P0znFp9w.d.ts +626 -0
- package/dist/protocol.cjs +13 -13
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.ts +2 -628
- package/dist/protocol.js +12 -14
- package/dist/protocol.js.map +1 -1
- package/package.json +7 -12
- package/dist/configFile-dJtYeqX1.cjs +0 -567
- package/dist/configFile-dJtYeqX1.cjs.map +0 -1
- package/dist/configFile-zhp5DQIe.js +0 -566
- package/dist/configFile-zhp5DQIe.js.map +0 -1
- package/dist/resolveConfig-CjF6j-3y.cjs +0 -399
- package/dist/resolveConfig-CjF6j-3y.cjs.map +0 -1
- package/dist/resolveConfig-DOUv_Tid.js +0 -352
- package/dist/resolveConfig-DOUv_Tid.js.map +0 -1
- package/dist/rolldown-runtime-qbf5tadS.cjs +0 -38
- package/dist/snapshotPackage-BFjiRhGd.cjs +0 -177
- package/dist/snapshotPackage-BFjiRhGd.cjs.map +0 -1
- package/dist/snapshotPackage-BwZ8IJX-.js +0 -176
- package/dist/snapshotPackage-BwZ8IJX-.js.map +0 -1
- package/dist/url-DC87bYiU.cjs +0 -19
- package/dist/url-DC87bYiU.cjs.map +0 -1
- package/dist/url-DhFfJpxp.js +0 -14
- package/dist/url-DhFfJpxp.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,23 +1,71 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
4
24
|
const require_protocol = require("./protocol.cjs");
|
|
5
|
-
const require_url = require("./url-DC87bYiU.cjs");
|
|
6
25
|
let node_crypto = require("node:crypto");
|
|
7
|
-
let node_timers_promises = require("node:timers/promises");
|
|
8
|
-
let node_process = require("node:process");
|
|
9
|
-
node_process = require_rolldown_runtime.__toESM(node_process, 1);
|
|
10
|
-
let node_path = require("node:path");
|
|
11
|
-
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
12
26
|
let node_fs_promises = require("node:fs/promises");
|
|
13
|
-
let ofetch = require("ofetch");
|
|
14
|
-
let node_util = require("node:util");
|
|
15
|
-
let unstorage = require("unstorage");
|
|
16
|
-
let unstorage_drivers_fs = require("unstorage/drivers/fs");
|
|
17
|
-
unstorage_drivers_fs = require_rolldown_runtime.__toESM(unstorage_drivers_fs, 1);
|
|
18
27
|
let node_os = require("node:os");
|
|
28
|
+
let node_path = require("node:path");
|
|
29
|
+
node_path = __toESM(node_path, 1);
|
|
30
|
+
let node_process = require("node:process");
|
|
31
|
+
node_process = __toESM(node_process, 1);
|
|
32
|
+
let node_timers_promises = require("node:timers/promises");
|
|
33
|
+
let node_url = require("node:url");
|
|
34
|
+
let node_fs = require("node:fs");
|
|
19
35
|
let _kubb_core = require("@kubb/core");
|
|
20
36
|
let capnweb = require("capnweb");
|
|
37
|
+
let magicast = require("magicast");
|
|
38
|
+
let node_module = require("node:module");
|
|
39
|
+
let node_util = require("node:util");
|
|
40
|
+
let ofetch = require("ofetch");
|
|
41
|
+
let node_zlib = require("node:zlib");
|
|
42
|
+
//#region ../../internals/utils/src/casing.ts
|
|
43
|
+
/**
|
|
44
|
+
* Shared implementation for camelCase and PascalCase conversion.
|
|
45
|
+
* Splits on common word boundaries (spaces, hyphens, underscores, dots, slashes, colons)
|
|
46
|
+
* and capitalizes each word according to `pascal`.
|
|
47
|
+
*
|
|
48
|
+
* When `pascal` is `true` the first word is also capitalized (PascalCase), otherwise only subsequent words are.
|
|
49
|
+
*/
|
|
50
|
+
function toCamelOrPascal(text, pascal) {
|
|
51
|
+
return text.trim().replace(/([a-z\d])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").replace(/(\d)([a-z])/g, "$1 $2").split(/[\s\-_./\\:]+/).filter(Boolean).map((word, i) => {
|
|
52
|
+
if (word.length > 1 && word === word.toUpperCase()) return word;
|
|
53
|
+
return (i === 0 && !pascal ? word.charAt(0).toLowerCase() : word.charAt(0).toUpperCase()) + word.slice(1);
|
|
54
|
+
}).join("").replace(/[^a-zA-Z0-9]/g, "");
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Converts `text` to camelCase.
|
|
58
|
+
*
|
|
59
|
+
* @example Word boundaries
|
|
60
|
+
* `camelCase('hello-world') // 'helloWorld'`
|
|
61
|
+
*
|
|
62
|
+
* @example With a prefix
|
|
63
|
+
* `camelCase('tag', { prefix: 'create' }) // 'createTag'`
|
|
64
|
+
*/
|
|
65
|
+
function camelCase(text, { prefix = "", suffix = "" } = {}) {
|
|
66
|
+
return toCamelOrPascal(`${prefix} ${text} ${suffix}`, false);
|
|
67
|
+
}
|
|
68
|
+
//#endregion
|
|
21
69
|
//#region ../../internals/utils/src/errors.ts
|
|
22
70
|
/**
|
|
23
71
|
* Coerces an unknown thrown value to an `Error` instance.
|
|
@@ -46,6 +94,91 @@ function getErrorMessage(value) {
|
|
|
46
94
|
return value instanceof Error ? value.message : String(value);
|
|
47
95
|
}
|
|
48
96
|
//#endregion
|
|
97
|
+
//#region ../../internals/utils/src/runtime.ts
|
|
98
|
+
/**
|
|
99
|
+
* Detects the JavaScript runtime executing the current process and exposes its name and version.
|
|
100
|
+
*
|
|
101
|
+
* Prefer the shared {@link runtime} instance over constructing your own.
|
|
102
|
+
*/
|
|
103
|
+
var Runtime = class {
|
|
104
|
+
/**
|
|
105
|
+
* `true` when the current process is running under Bun.
|
|
106
|
+
*
|
|
107
|
+
* Detection keys off the global `Bun` object rather than `process.versions`,
|
|
108
|
+
* because Bun polyfills `process.versions.node` for Node compatibility and would
|
|
109
|
+
* otherwise look like Node.
|
|
110
|
+
*
|
|
111
|
+
* @example
|
|
112
|
+
* ```ts
|
|
113
|
+
* if (runtime.isBun) {
|
|
114
|
+
* await Bun.write(path, data)
|
|
115
|
+
* }
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
get isBun() {
|
|
119
|
+
return typeof Bun !== "undefined";
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* `true` when the current process is running under Deno.
|
|
123
|
+
*/
|
|
124
|
+
get isDeno() {
|
|
125
|
+
return typeof globalThis.Deno !== "undefined";
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* `true` when the current process is running under Node.
|
|
129
|
+
*
|
|
130
|
+
* Bun and Deno are excluded first so a polyfilled `process` does not register as Node.
|
|
131
|
+
*/
|
|
132
|
+
get isNode() {
|
|
133
|
+
return !this.isBun && !this.isDeno && typeof process !== "undefined" && process.versions?.node != null;
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Name of the runtime executing the current process.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* runtime.name // 'bun' when run with `bun kubb`, 'node' otherwise
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
get name() {
|
|
144
|
+
if (this.isBun) return "bun";
|
|
145
|
+
if (this.isDeno) return "deno";
|
|
146
|
+
return "node";
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Version of the active runtime, or an empty string when it cannot be read.
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```ts
|
|
153
|
+
* runtime.version // '1.3.11' under Bun, '22.22.2' under Node
|
|
154
|
+
* ```
|
|
155
|
+
*/
|
|
156
|
+
get version() {
|
|
157
|
+
if (this.isBun) return process.versions.bun ?? "";
|
|
158
|
+
if (this.isDeno) return globalThis.Deno?.version?.deno ?? "";
|
|
159
|
+
return process.versions?.node ?? "";
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
/**
|
|
163
|
+
* Shared {@link Runtime} instance describing the JavaScript runtime executing the current process.
|
|
164
|
+
*/
|
|
165
|
+
const runtime = new Runtime();
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region ../../internals/utils/src/fs.ts
|
|
168
|
+
/**
|
|
169
|
+
* Reads the file at `path` as a UTF-8 string.
|
|
170
|
+
* Uses `Bun.file().text()` when running under Bun, `fs.readFile` otherwise.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```ts
|
|
174
|
+
* const source = await read('./src/Pet.ts')
|
|
175
|
+
* ```
|
|
176
|
+
*/
|
|
177
|
+
async function read(path) {
|
|
178
|
+
if (runtime.isBun) return Bun.file(path).text();
|
|
179
|
+
return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
49
182
|
//#region ../../internals/utils/src/promise.ts
|
|
50
183
|
/**
|
|
51
184
|
* Runs `run` over every item with at most `limit` in flight. Workers share one iterator, so each
|
|
@@ -82,360 +215,800 @@ function getElapsedMs(hrStart) {
|
|
|
82
215
|
return Math.round(ms * 100) / 100;
|
|
83
216
|
}
|
|
84
217
|
//#endregion
|
|
85
|
-
//#region
|
|
218
|
+
//#region package.json
|
|
219
|
+
var version = "5.6.1";
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/resolveConfig.ts
|
|
86
222
|
/**
|
|
87
|
-
*
|
|
223
|
+
* Imports a package, falling back to how the user's project would resolve it.
|
|
88
224
|
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* The
|
|
92
|
-
* identity that changes on every restart.
|
|
225
|
+
* `import()` resolves from this file, so a linked or globally installed Studio (`pnpm link`,
|
|
226
|
+
* `npm i -g`) only sees its own `node_modules` and misses the plugins installed next to the user's
|
|
227
|
+
* config. The retry resolves from `process.cwd()` instead.
|
|
93
228
|
*/
|
|
94
|
-
|
|
95
|
-
|
|
229
|
+
async function importFromProject(packageName) {
|
|
230
|
+
try {
|
|
231
|
+
return await import(packageName);
|
|
232
|
+
} catch {
|
|
233
|
+
const resolved = (0, node_module.createRequire)((0, node_url.pathToFileURL)(`${process.cwd()}/`)).resolve(packageName);
|
|
234
|
+
const esm = resolved.replace(/\.cjs$/, ".js");
|
|
235
|
+
return await import((0, node_url.pathToFileURL)(esm !== resolved && (0, node_fs.existsSync)(esm) ? esm : resolved).href);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
96
238
|
/**
|
|
97
|
-
*
|
|
239
|
+
* Strips the `@kubb/` scope from a plugin package name, matching the `name` convention Kubb
|
|
240
|
+
* plugin factories use internally.
|
|
241
|
+
*
|
|
242
|
+
* @example
|
|
243
|
+
* ```ts
|
|
244
|
+
* toPluginName('@kubb/plugin-ts') // 'plugin-ts'
|
|
245
|
+
* ```
|
|
98
246
|
*/
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
hasInstalledStorage = true;
|
|
247
|
+
function toPluginName(packageName) {
|
|
248
|
+
return packageName.split("/").pop() ?? packageName;
|
|
102
249
|
}
|
|
103
250
|
/**
|
|
104
|
-
*
|
|
105
|
-
*
|
|
251
|
+
* Adds the `@kubb/` scope a plugin's package carries but its `name` does not, the inverse of
|
|
252
|
+
* {@link toPluginName}. A name outside the `plugin-` convention is left alone, so a third-party
|
|
253
|
+
* plugin is not reported as one of Kubb's.
|
|
254
|
+
*
|
|
255
|
+
* @example
|
|
256
|
+
* ```ts
|
|
257
|
+
* toPackageName('plugin-ts') // '@kubb/plugin-ts'
|
|
258
|
+
* toPackageName('@acme/my-plugin') // '@acme/my-plugin'
|
|
259
|
+
* ```
|
|
106
260
|
*/
|
|
107
|
-
function
|
|
108
|
-
return (
|
|
261
|
+
function toPackageName(name) {
|
|
262
|
+
return name.startsWith("plugin-") ? `@kubb/${name}` : name;
|
|
109
263
|
}
|
|
110
|
-
let fallbackSecretPromise = null;
|
|
111
264
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
265
|
+
* Derives the conventional named export for a `@kubb/*` plugin package from its package name.
|
|
266
|
+
*
|
|
267
|
+
* @example
|
|
268
|
+
* ```ts
|
|
269
|
+
* toExportName('@kubb/plugin-react-query') // 'pluginReactQuery'
|
|
270
|
+
* toExportName('@kubb/plugin-ts') // 'pluginTs'
|
|
271
|
+
* ```
|
|
116
272
|
*/
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const stored = await storage.getItem("machine-secret").catch(() => null);
|
|
120
|
-
if (typeof stored === "string" && stored) return stored;
|
|
121
|
-
const secret = (0, node_crypto.randomBytes)(32).toString("hex");
|
|
122
|
-
await storage.setItem("machine-secret", secret).catch(() => {
|
|
123
|
-
console.warn((0, node_util.styleText)("yellow", "Could not persist the generated machine secret"), "set KUBB_AGENT_SECRET to keep a stable machine identity across restarts");
|
|
124
|
-
});
|
|
125
|
-
return secret;
|
|
273
|
+
function toExportName(packageName) {
|
|
274
|
+
return camelCase(toPluginName(packageName));
|
|
126
275
|
}
|
|
127
276
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
277
|
+
* A `@kubb/plugin-*` package specifier. Nothing else may reach `import()`: only Kubb's own
|
|
278
|
+
* plugins are supported, so a payload naming anything else, a third-party package or a path, is
|
|
279
|
+
* refused before it can execute.
|
|
130
280
|
*/
|
|
131
|
-
|
|
132
|
-
return (0, node_crypto.hash)("sha256", secret);
|
|
133
|
-
}
|
|
281
|
+
const KUBB_PLUGIN_SPECIFIER = /^@kubb\/plugin-[\w.-]+$/;
|
|
134
282
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
* The token is hashed with SHA-256.
|
|
283
|
+
* Whether `name` is a `@kubb/plugin-*` specifier. Exported so `configFile.ts` can refuse the same
|
|
284
|
+
* shape before printing a Studio-supplied plugin name into the config file's source text.
|
|
138
285
|
*/
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
fallbackSecretPromise ??= loadOrCreateFallbackSecret();
|
|
142
|
-
return machineTokenFrom(await fallbackSecretPromise);
|
|
286
|
+
function isKubbPluginSpecifier(name) {
|
|
287
|
+
return KUBB_PLUGIN_SPECIFIER.test(name);
|
|
143
288
|
}
|
|
144
|
-
//#endregion
|
|
145
|
-
//#region src/operations/api.ts
|
|
146
289
|
/**
|
|
147
|
-
*
|
|
148
|
-
*
|
|
149
|
-
*
|
|
290
|
+
* Dynamically imports a `@kubb/plugin-*` package and returns its factory function.
|
|
291
|
+
*
|
|
292
|
+
* Packages must be pre-installed in the Docker image at build time via the `KUBB_PACKAGES`
|
|
293
|
+
* build ARG, no runtime installation is possible in the distroless container.
|
|
294
|
+
*
|
|
295
|
+
* Resolution order: the camelCase named export the package name implies (e.g. `pluginTs`), then
|
|
296
|
+
* the default export.
|
|
297
|
+
*
|
|
298
|
+
* @throws if the package cannot be imported or exports no callable factory.
|
|
150
299
|
*/
|
|
151
|
-
function
|
|
152
|
-
if (!
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
300
|
+
async function loadPluginFactory(packageName) {
|
|
301
|
+
if (!isKubbPluginSpecifier(packageName)) throw new Error(`Plugin "${packageName}" is not a @kubb/plugin-* package. Kubb Studio only supports Kubb's own plugins.`);
|
|
302
|
+
let mod;
|
|
303
|
+
try {
|
|
304
|
+
mod = await importFromProject(packageName);
|
|
305
|
+
} catch (cause) {
|
|
306
|
+
throw new Error(`Plugin "${packageName}" could not be loaded. Make sure it is installed: \`npm install ${packageName}\``, { cause });
|
|
307
|
+
}
|
|
308
|
+
const exportName = toExportName(packageName);
|
|
309
|
+
if (typeof mod[exportName] === "function") return mod[exportName];
|
|
310
|
+
if (typeof mod["default"] === "function") return mod["default"];
|
|
311
|
+
throw new Error(`Plugin "${packageName}" does not export a callable factory. Tried the named export "${exportName}" and "default".`);
|
|
159
312
|
}
|
|
160
313
|
/**
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
314
|
+
* Resolves each plugin entry by dynamically importing the `@kubb/plugin-*` package and
|
|
315
|
+
* calling its factory with the provided options.
|
|
316
|
+
*
|
|
317
|
+
* Packages must be pre-installed in the Docker image at build time, use the `KUBB_PACKAGES`
|
|
318
|
+
* build ARG to control which ones are available at runtime.
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```ts
|
|
322
|
+
* { name: '@kubb/plugin-react-query', options: { output: { path: './hooks' } } }
|
|
323
|
+
* { name: '@kubb/plugin-ts', options: { output: { path: './types' } } }
|
|
324
|
+
* ```
|
|
164
325
|
*/
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
326
|
+
async function resolvePlugins(plugins) {
|
|
327
|
+
return Promise.all(plugins.map(async ({ name, options }) => {
|
|
328
|
+
return (await loadPluginFactory(name))(options ?? {});
|
|
329
|
+
}));
|
|
330
|
+
}
|
|
331
|
+
/** The installed version of each plugin's package, plus the names that could not be resolved. */
|
|
332
|
+
async function resolvePeerDependencies(names) {
|
|
333
|
+
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
334
|
+
const uniqueNames = [...new Set(names.map(toPackageName))];
|
|
335
|
+
const peerDependencies = {};
|
|
336
|
+
const missingDependencies = [];
|
|
337
|
+
const versions = await Promise.all(uniqueNames.map(async (name) => {
|
|
338
|
+
try {
|
|
339
|
+
const path = require$1.resolve(`${name}/package.json`);
|
|
340
|
+
return JSON.parse(await (0, node_fs_promises.readFile)(path, "utf8")).version;
|
|
341
|
+
} catch {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
}));
|
|
345
|
+
for (const [index, name] of uniqueNames.entries()) {
|
|
346
|
+
const version = versions[index];
|
|
347
|
+
if (version) {
|
|
348
|
+
peerDependencies[name] = version;
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
missingDependencies.push(name);
|
|
169
352
|
}
|
|
170
|
-
|
|
353
|
+
return {
|
|
354
|
+
peerDependencies,
|
|
355
|
+
missingDependencies
|
|
356
|
+
};
|
|
357
|
+
}
|
|
171
358
|
/**
|
|
172
|
-
*
|
|
173
|
-
* agent is upgraded, so hosts stop instead of reconnecting.
|
|
359
|
+
* Whether `value` is an object literal or a null-prototype object, not an array or class instance.
|
|
174
360
|
*/
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
361
|
+
function isPlainObject(value) {
|
|
362
|
+
if (typeof value !== "object" || value === null) return false;
|
|
363
|
+
const prototype = Object.getPrototypeOf(value);
|
|
364
|
+
return prototype === null || prototype === Object.prototype;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Whether `value` survives a JSON round trip, so it can be printed into a config file as a literal.
|
|
368
|
+
* A function, `undefined`, a non-finite number or a class instance comes back from Studio as a
|
|
369
|
+
* lossy copy, so an edit carrying one is refused rather than written into the user's source.
|
|
370
|
+
*/
|
|
371
|
+
function isOptionValue(value) {
|
|
372
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return true;
|
|
373
|
+
if (typeof value === "number") return Number.isFinite(value);
|
|
374
|
+
if (Array.isArray(value)) return value.every(isOptionValue);
|
|
375
|
+
if (isPlainObject(value)) return Object.values(value).every(isOptionValue);
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Deep-merges Studio's options over the disk options, with Studio taking priority, except where the
|
|
380
|
+
* disk value cannot survive JSON. Studio only ever saw a lossy copy of such a value, so the disk
|
|
381
|
+
* value is kept rather than replaced by what Studio echoes back. Plain objects merge key by key, so
|
|
382
|
+
* Studio can still change a literal that sits next to a function (e.g. `group.type` beside `group.name`).
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* ```ts
|
|
386
|
+
* mergeOptions({ macros: [camelCaseMacro], syntaxType: 'type' }, { macros: [{ name: 'camelCase' }], syntaxType: 'interface' })
|
|
387
|
+
* // { macros: [camelCaseMacro], syntaxType: 'interface' }
|
|
388
|
+
* ```
|
|
389
|
+
*/
|
|
390
|
+
function mergeOptions(disk, studio) {
|
|
391
|
+
const merged = { ...disk };
|
|
392
|
+
for (const [key, value] of Object.entries(studio)) {
|
|
393
|
+
const current = disk[key];
|
|
394
|
+
if (isPlainObject(current) && isPlainObject(value)) {
|
|
395
|
+
merged[key] = mergeOptions(current, value);
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (current !== void 0 && !isOptionValue(current)) continue;
|
|
399
|
+
merged[key] = value;
|
|
179
400
|
}
|
|
180
|
-
|
|
401
|
+
return merged;
|
|
402
|
+
}
|
|
181
403
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
404
|
+
* Merges studio plugin options with disk config plugins.
|
|
405
|
+
* Studio takes priority: options from studio win over disk (except values only the disk config
|
|
406
|
+
* can express, see {@link mergeOptions}), and a plugin Studio explicitly
|
|
407
|
+
* disabled is dropped even when the disk config still lists it. Disk plugins without a studio
|
|
408
|
+
* counterpart are kept as-is. Studio plugins not present on disk are appended.
|
|
409
|
+
*
|
|
410
|
+
* For plugins present in both configs, the plugin is re-instantiated with merged options
|
|
411
|
+
* so that all internal closures correctly reference the merged values.
|
|
184
412
|
*/
|
|
185
|
-
function
|
|
186
|
-
|
|
413
|
+
async function mergePlugins(diskPlugins, studioPlugins) {
|
|
414
|
+
const disabledNames = new Set((studioPlugins ?? []).filter((entry) => entry.disabled).map((entry) => toPluginName(entry.name)));
|
|
415
|
+
const activeDiskPlugins = disabledNames.size ? diskPlugins?.filter((plugin) => !disabledNames.has(plugin.name)) : diskPlugins;
|
|
416
|
+
const activeStudioPlugins = studioPlugins?.filter((entry) => !entry.disabled);
|
|
417
|
+
if (!activeDiskPlugins && !activeStudioPlugins?.length) return void 0;
|
|
418
|
+
if (!activeStudioPlugins?.length) return activeDiskPlugins;
|
|
419
|
+
if (!activeDiskPlugins) return resolvePlugins(activeStudioPlugins);
|
|
420
|
+
const studioEntryByName = new Map(activeStudioPlugins.map((entry) => [toPluginName(entry.name), entry]));
|
|
421
|
+
const diskNames = new Set(activeDiskPlugins.map((plugin) => plugin.name));
|
|
422
|
+
const merged = await Promise.all(activeDiskPlugins.map(async (diskPlugin) => {
|
|
423
|
+
const studioEntry = studioEntryByName.get(diskPlugin.name);
|
|
424
|
+
if (!studioEntry) return diskPlugin;
|
|
425
|
+
const options = mergeOptions(diskPlugin.options ?? {}, studioEntry.options ?? {});
|
|
426
|
+
const [resolved] = await resolvePlugins([{
|
|
427
|
+
name: studioEntry.name,
|
|
428
|
+
options
|
|
429
|
+
}]);
|
|
430
|
+
return resolved ?? diskPlugin;
|
|
431
|
+
}));
|
|
432
|
+
const studioOnly = activeStudioPlugins.filter((entry) => !diskNames.has(toPluginName(entry.name)));
|
|
433
|
+
return [...merged, ...await resolvePlugins(studioOnly)];
|
|
187
434
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
435
|
+
/**
|
|
436
|
+
* Merges Studio-provided adapter option overrides into the disk config's adapter.
|
|
437
|
+
*
|
|
438
|
+
* Adapter instances carry live functions (`parse`, `getImports`, ...) that can't survive
|
|
439
|
+
* JSON serialization over the WebSocket, so `studioOptions` is treated as an options patch
|
|
440
|
+
* rather than a replacement adapter. Re-invokes the same `@kubb/adapter-<name>` factory the
|
|
441
|
+
* disk config used, with the merged options, so the resulting instance has fresh closures
|
|
442
|
+
* over the merged values instead of a plain object missing `parse`.
|
|
443
|
+
*/
|
|
444
|
+
async function mergeAdapter(diskAdapter, studioOptions) {
|
|
445
|
+
if (!studioOptions || !diskAdapter) return diskAdapter;
|
|
446
|
+
const packageName = `@kubb/adapter-${diskAdapter.name}`;
|
|
447
|
+
const factory = (await importFromProject(packageName))[toExportName(packageName)];
|
|
448
|
+
if (typeof factory !== "function") return diskAdapter;
|
|
449
|
+
return factory(mergeOptions(diskAdapter.options ?? {}, studioOptions));
|
|
191
450
|
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region src/configFile.ts
|
|
453
|
+
/**
|
|
454
|
+
* A valid JavaScript identifier, so an import name can only ever print as `import { name } from`,
|
|
455
|
+
* never as source that breaks out of the import statement.
|
|
456
|
+
*/
|
|
457
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
192
458
|
/**
|
|
193
|
-
*
|
|
194
|
-
* identity to the token, reports what the process can take on, and gets back the URL of the one
|
|
195
|
-
* socket it keeps open.
|
|
459
|
+
* `key: value` as an object literal property, in the file's quote and key style.
|
|
196
460
|
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
* {@link IncompatibleAgentError}, since retrying either cannot help.
|
|
461
|
+
* Uses magicast's literal builder for the key/value nodes, then wraps them as a Babel
|
|
462
|
+
* `ObjectProperty`, the type the rest of this file reads.
|
|
200
463
|
*/
|
|
201
|
-
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
464
|
+
function literalProperty({ key, value }) {
|
|
465
|
+
const built = magicast.builders.literal({ [key]: value }).properties[0];
|
|
466
|
+
return {
|
|
467
|
+
type: "ObjectProperty",
|
|
468
|
+
key: built.key,
|
|
469
|
+
value: built.value,
|
|
470
|
+
computed: false,
|
|
471
|
+
shorthand: false
|
|
206
472
|
};
|
|
207
|
-
try {
|
|
208
|
-
return await (0, ofetch.ofetch)(`${studioUrl}/api/agent/connect`, {
|
|
209
|
-
method: "POST",
|
|
210
|
-
headers: { Authorization: `Bearer ${token}` },
|
|
211
|
-
body,
|
|
212
|
-
retry: 0,
|
|
213
|
-
timeout: 1e4,
|
|
214
|
-
signal
|
|
215
|
-
});
|
|
216
|
-
} catch (error) {
|
|
217
|
-
if (rejectedWith(error, 401)) throw new InvalidAgentTokenError(studioUrl, { cause: error });
|
|
218
|
-
if (rejectedWith(error, 426)) throw new IncompatibleAgentError(studioUrl, error instanceof ofetch.FetchError ? responseMessage(error.data) : void 0, { cause: error });
|
|
219
|
-
throw registrationError(error);
|
|
220
|
-
}
|
|
221
473
|
}
|
|
222
474
|
/**
|
|
223
|
-
*
|
|
475
|
+
* Marks the comment block a `disable-plugin` leaves behind, so `enable-plugin` can find its way
|
|
476
|
+
* back to the exact lines it commented out. Carries the block's line count, so `enable-plugin`
|
|
477
|
+
* restores exactly those lines instead of scanning forward through whatever comments follow.
|
|
478
|
+
*/
|
|
479
|
+
const DISABLED_MARKER = "kubb:disabled";
|
|
480
|
+
/**
|
|
481
|
+
* The plugin and comment-block length a marker line names, when `line` is one.
|
|
482
|
+
*/
|
|
483
|
+
function parseMarker(line) {
|
|
484
|
+
const trimmed = line.trim();
|
|
485
|
+
if (!trimmed.startsWith(`// ${DISABLED_MARKER} `)) return;
|
|
486
|
+
const match = trimmed.slice(`// ${DISABLED_MARKER} `.length).match(/^(.+)\s+(\d+)$/);
|
|
487
|
+
return match ? {
|
|
488
|
+
plugin: match[1],
|
|
489
|
+
lineCount: Number(match[2])
|
|
490
|
+
} : void 0;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Steps through a config's wrappers to the object literal underneath: a `satisfies`/`as`
|
|
494
|
+
* assertion, a `() => ...` factory, or a factory whose block body returns the config.
|
|
495
|
+
*/
|
|
496
|
+
function unwrap(node) {
|
|
497
|
+
if (!node) return;
|
|
498
|
+
if (node.type === "TSAsExpression" || node.type === "TSSatisfiesExpression") return unwrap(node.expression);
|
|
499
|
+
if (node.type !== "ArrowFunctionExpression" && node.type !== "FunctionExpression") return node;
|
|
500
|
+
if (node.body.type !== "BlockStatement") return unwrap(node.body);
|
|
501
|
+
return unwrap(node.body.body.find((statement) => statement.type === "ReturnStatement")?.argument);
|
|
502
|
+
}
|
|
503
|
+
/**
|
|
504
|
+
* Every config object in `export default defineConfig(...)`, or why the file is unmanaged.
|
|
505
|
+
*
|
|
506
|
+
* An array export gets one entry per element, matching {@link ConfigRef}'s numeric index.
|
|
507
|
+
*
|
|
508
|
+
* Walks the parsed AST rather than magicast's proxies, which throw on node types they cannot
|
|
509
|
+
* cast, most of what an unmanaged config file is made of.
|
|
224
510
|
*/
|
|
225
|
-
|
|
511
|
+
function findConfigs(mod) {
|
|
512
|
+
const exported = unwrap((mod.$ast.type === "Program" ? mod.$ast.body : []).find((node) => node.type === "ExportDefaultDeclaration")?.declaration);
|
|
513
|
+
if (!exported) return { reason: "no default export found" };
|
|
514
|
+
if (exported.type !== "CallExpression" || exported.callee.type !== "Identifier" || exported.callee.name !== "defineConfig") return { reason: "default export is not a defineConfig(...) call" };
|
|
515
|
+
const argument = unwrap(exported.arguments[0]);
|
|
516
|
+
if (!argument) return { reason: "defineConfig(...) was called without a config" };
|
|
517
|
+
if (argument.type === "ArrayExpression") {
|
|
518
|
+
const configs = [];
|
|
519
|
+
for (const element of argument.elements) {
|
|
520
|
+
const entry = unwrap(element);
|
|
521
|
+
if (entry?.type !== "ObjectExpression") return { reason: "config is not an object literal" };
|
|
522
|
+
configs.push(entry);
|
|
523
|
+
}
|
|
524
|
+
return { configs };
|
|
525
|
+
}
|
|
526
|
+
if (argument.type !== "ObjectExpression") return { reason: "config is not an object literal" };
|
|
527
|
+
return { configs: [argument] };
|
|
528
|
+
}
|
|
226
529
|
/**
|
|
227
|
-
*
|
|
530
|
+
* The config entry an edit names, defaulting to the first when it names none.
|
|
228
531
|
*/
|
|
229
|
-
|
|
532
|
+
function selectConfig(configs, ref) {
|
|
533
|
+
if (ref === void 0) return configs[0];
|
|
534
|
+
if (typeof ref === "number") return configs[ref];
|
|
535
|
+
return configs.find((config) => configName(config) === ref);
|
|
536
|
+
}
|
|
537
|
+
function configName(config) {
|
|
538
|
+
const name = property(config, "name");
|
|
539
|
+
return name?.type === "StringLiteral" ? name.value : void 0;
|
|
540
|
+
}
|
|
230
541
|
/**
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
* (503, an agent process that is mid-reconnect). Anything else (404 agent not found, 401/403 auth)
|
|
234
|
-
* is thrown straight away, since retrying cannot change the outcome.
|
|
542
|
+
* The name of an object literal property, for the two key shapes a config uses: `key: value` and
|
|
543
|
+
* `'key': value`. `undefined` for a computed key, which the patcher never touches.
|
|
235
544
|
*/
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
]);
|
|
545
|
+
function propertyKey(entry) {
|
|
546
|
+
if (entry.key.type === "Identifier") return entry.key.name;
|
|
547
|
+
if (entry.key.type === "StringLiteral") return entry.key.value;
|
|
548
|
+
}
|
|
241
549
|
/**
|
|
242
|
-
*
|
|
550
|
+
* The index of an object literal's own property named `key`, `-1` when it has none.
|
|
243
551
|
*/
|
|
244
|
-
function
|
|
245
|
-
|
|
246
|
-
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1e3 : void 0;
|
|
552
|
+
function entryIndex({ node, key }) {
|
|
553
|
+
return node.properties.findIndex((entry) => entry.type === "ObjectProperty" && propertyKey(entry) === key);
|
|
247
554
|
}
|
|
248
555
|
/**
|
|
249
|
-
*
|
|
250
|
-
* lockstep.
|
|
556
|
+
* The value node of an object literal's own property.
|
|
251
557
|
*/
|
|
252
|
-
function
|
|
253
|
-
|
|
558
|
+
function property(node, key) {
|
|
559
|
+
const index = entryIndex({
|
|
560
|
+
node,
|
|
561
|
+
key
|
|
562
|
+
});
|
|
563
|
+
return index === -1 ? void 0 : node.properties[index].value;
|
|
254
564
|
}
|
|
255
565
|
/**
|
|
256
|
-
*
|
|
566
|
+
* Writes `key: value` on an object literal, replacing the value when the property is already there.
|
|
257
567
|
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
568
|
+
* An existing property has its value swapped in place rather than being replaced whole, so recast
|
|
569
|
+
* reprints only that value and leaves the object's own layout alone.
|
|
570
|
+
*/
|
|
571
|
+
function setProperty({ node, key, value }) {
|
|
572
|
+
const entry = literalProperty({
|
|
573
|
+
key,
|
|
574
|
+
value
|
|
575
|
+
});
|
|
576
|
+
const index = entryIndex({
|
|
577
|
+
node,
|
|
578
|
+
key
|
|
579
|
+
});
|
|
580
|
+
if (index === -1) {
|
|
581
|
+
node.properties.push(entry);
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
node.properties[index].value = entry.value;
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Drops `key` from an object literal.
|
|
588
|
+
*/
|
|
589
|
+
function removeProperty({ node, key }) {
|
|
590
|
+
const index = entryIndex({
|
|
591
|
+
node,
|
|
592
|
+
key
|
|
593
|
+
});
|
|
594
|
+
if (index !== -1) node.properties.splice(index, 1);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Reads a literal node's value: a primitive, or an object/array built only from primitives.
|
|
598
|
+
* `undefined` for anything else, so a caller can use this both to read a value and to check
|
|
599
|
+
* whether a node is a literal at all.
|
|
600
|
+
*/
|
|
601
|
+
function readLiteral(node) {
|
|
602
|
+
if (!node) return;
|
|
603
|
+
if (node.type === "StringLiteral" || node.type === "NumericLiteral" || node.type === "BooleanLiteral") return node.value;
|
|
604
|
+
if (node.type === "NullLiteral") return null;
|
|
605
|
+
if (node.type === "TemplateLiteral") return node.expressions.length === 0 ? node.quasis[0]?.value.cooked ?? "" : void 0;
|
|
606
|
+
if (node.type === "UnaryExpression") {
|
|
607
|
+
const value = readLiteral(node.argument);
|
|
608
|
+
if (typeof value !== "number") return;
|
|
609
|
+
if (node.operator === "-") return -value;
|
|
610
|
+
if (node.operator === "+") return value;
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (node.type === "ArrayExpression") {
|
|
614
|
+
const values = node.elements.map((element) => element ? readLiteral(element) : void 0);
|
|
615
|
+
return values.every((value) => value !== void 0) ? values : void 0;
|
|
616
|
+
}
|
|
617
|
+
if (node.type === "ObjectExpression") {
|
|
618
|
+
const entries = {};
|
|
619
|
+
for (const entry of node.properties) {
|
|
620
|
+
if (entry.type !== "ObjectProperty") return;
|
|
621
|
+
const key = propertyKey(entry);
|
|
622
|
+
const value = readLiteral(entry.value);
|
|
623
|
+
if (key === void 0 || value === void 0) return;
|
|
624
|
+
entries[key] = value;
|
|
625
|
+
}
|
|
626
|
+
return entries;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Maps a factory identifier in the file back to the module it was imported from.
|
|
631
|
+
*/
|
|
632
|
+
function importedFrom(mod) {
|
|
633
|
+
return new Map(mod.imports.$items.map((item) => [item.local, item.from]));
|
|
634
|
+
}
|
|
635
|
+
/**
|
|
636
|
+
* Every `pluginX(...)` element of a config's plugins array that resolves to an import.
|
|
637
|
+
*/
|
|
638
|
+
function pluginCalls(mod, config) {
|
|
639
|
+
const plugins = property(config, "plugins");
|
|
640
|
+
if (plugins?.type !== "ArrayExpression") return [];
|
|
641
|
+
const imports = importedFrom(mod);
|
|
642
|
+
return plugins.elements.flatMap((element) => {
|
|
643
|
+
if (element?.type !== "CallExpression" || element.callee.type !== "Identifier") return [];
|
|
644
|
+
const packageName = imports.get(element.callee.name);
|
|
645
|
+
return packageName ? [{
|
|
646
|
+
importName: element.callee.name,
|
|
647
|
+
packageName,
|
|
648
|
+
call: element
|
|
649
|
+
}] : [];
|
|
650
|
+
});
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Plugins a previous `disable-plugin` commented out of this config, keyed by package name.
|
|
260
654
|
*
|
|
261
|
-
*
|
|
262
|
-
|
|
263
|
-
|
|
655
|
+
* Read from the marker lines rather than the AST, since a commented-out call is no longer a node.
|
|
656
|
+
*/
|
|
657
|
+
function disabledMarkers(source) {
|
|
658
|
+
return source.split("\n").flatMap((line, index) => {
|
|
659
|
+
const marker = parseMarker(line);
|
|
660
|
+
return marker ? [{
|
|
661
|
+
packageName: marker.plugin,
|
|
662
|
+
line: index + 1
|
|
663
|
+
}] : [];
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Reads which plugins the file declares and which of their options Studio may write.
|
|
264
668
|
*
|
|
265
|
-
* @example
|
|
669
|
+
* @example
|
|
266
670
|
* ```ts
|
|
267
|
-
* const
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* agentId: agent.id,
|
|
272
|
-
* name: '@kubb/demo',
|
|
273
|
-
* version: '1.0.0',
|
|
274
|
-
* })
|
|
275
|
-
* const finished = await waitForJob({ studioUrl, token, id: job.id })
|
|
671
|
+
* const view = readConfig(await readFile('kubb.config.ts', 'utf8'))
|
|
672
|
+
* if (view.managed) {
|
|
673
|
+
* view.configs.forEach((config) => console.log(config.name, config.plugins.length))
|
|
674
|
+
* }
|
|
276
675
|
* ```
|
|
277
676
|
*/
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
677
|
+
function readConfig(source) {
|
|
678
|
+
const parsed = parseConfigs(source);
|
|
679
|
+
if ("reason" in parsed) return {
|
|
680
|
+
managed: false,
|
|
681
|
+
reason: parsed.reason
|
|
682
|
+
};
|
|
683
|
+
const { mod, configs } = parsed;
|
|
684
|
+
const importNames = new Map([...importedFrom(mod)].map(([local, from]) => [from, local]));
|
|
685
|
+
const disabled = disabledMarkers(source);
|
|
686
|
+
return {
|
|
687
|
+
managed: true,
|
|
688
|
+
configs: configs.map((config) => {
|
|
689
|
+
const plugins = pluginCalls(mod, config).map(({ importName, packageName, call }) => {
|
|
690
|
+
const entries = {};
|
|
691
|
+
const options = call.arguments[0];
|
|
692
|
+
if (options?.type === "ObjectExpression") for (const entry of options.properties) {
|
|
693
|
+
if (entry.type !== "ObjectProperty") continue;
|
|
694
|
+
const key = propertyKey(entry);
|
|
695
|
+
if (key === void 0) continue;
|
|
696
|
+
const value = readLiteral(entry.value);
|
|
697
|
+
entries[key] = value === void 0 ? { literal: false } : {
|
|
698
|
+
literal: true,
|
|
699
|
+
value
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
return {
|
|
703
|
+
importName,
|
|
704
|
+
packageName,
|
|
705
|
+
options: entries
|
|
706
|
+
};
|
|
300
707
|
});
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
708
|
+
const start = config.loc?.start.line ?? 0;
|
|
709
|
+
const end = config.loc?.end.line ?? Number.POSITIVE_INFINITY;
|
|
710
|
+
for (const { packageName } of disabled.filter((entry) => entry.line >= start && entry.line <= end)) plugins.push({
|
|
711
|
+
importName: importNames.get(packageName) ?? toExportName(packageName),
|
|
712
|
+
packageName,
|
|
713
|
+
options: {},
|
|
714
|
+
disabled: true
|
|
715
|
+
});
|
|
716
|
+
return {
|
|
717
|
+
name: configName(config),
|
|
718
|
+
plugins
|
|
719
|
+
};
|
|
720
|
+
})
|
|
721
|
+
};
|
|
313
722
|
}
|
|
314
723
|
/**
|
|
315
|
-
*
|
|
724
|
+
* The options object of a plugin call, when it was called with one.
|
|
316
725
|
*/
|
|
317
|
-
|
|
726
|
+
function getOptions(call) {
|
|
727
|
+
const options = call.arguments[0];
|
|
728
|
+
return options?.type === "ObjectExpression" ? options : void 0;
|
|
729
|
+
}
|
|
318
730
|
/**
|
|
319
|
-
*
|
|
320
|
-
* every concurrent run on the same organization key draws on one budget.
|
|
731
|
+
* The options object of a plugin call, creating an empty one when the plugin was called bare.
|
|
321
732
|
*/
|
|
322
|
-
|
|
733
|
+
function ensureOptions(call) {
|
|
734
|
+
if (call.arguments.length === 0) call.arguments.push({
|
|
735
|
+
type: "ObjectExpression",
|
|
736
|
+
properties: []
|
|
737
|
+
});
|
|
738
|
+
return getOptions(call);
|
|
739
|
+
}
|
|
323
740
|
/**
|
|
324
|
-
*
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (
|
|
340
|
-
|
|
341
|
-
try {
|
|
342
|
-
const { job } = await (0, ofetch.ofetch)(`${studioUrl}/api/jobs/${id}`, {
|
|
343
|
-
headers: { "x-api-key": token },
|
|
344
|
-
retry: false,
|
|
345
|
-
timeout: Math.max(deadline - Date.now(), 1),
|
|
346
|
-
signal
|
|
347
|
-
});
|
|
348
|
-
if (job.status === "success" || job.status === "failed" || job.status === "canceled") return job;
|
|
349
|
-
} catch (error) {
|
|
350
|
-
signal?.throwIfAborted();
|
|
351
|
-
const response = error.response;
|
|
352
|
-
if (response?.status !== 429) throw error;
|
|
353
|
-
const retryAfter = response._data?.data?.tryAgainIn;
|
|
354
|
-
interval = Math.max(interval, typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : MAX_POLL_INTERVAL_MS);
|
|
355
|
-
}
|
|
741
|
+
* Walks `path` down to the object holding its last key, descending only through object literals.
|
|
742
|
+
*/
|
|
743
|
+
function optionParent(options, path) {
|
|
744
|
+
let object = options;
|
|
745
|
+
for (const [index, key] of path.entries()) {
|
|
746
|
+
if (index === path.length - 1) return {
|
|
747
|
+
object,
|
|
748
|
+
key
|
|
749
|
+
};
|
|
750
|
+
if (property(object, key) === void 0) setProperty({
|
|
751
|
+
node: object,
|
|
752
|
+
key,
|
|
753
|
+
value: {}
|
|
754
|
+
});
|
|
755
|
+
const next = property(object, key);
|
|
756
|
+
if (next?.type !== "ObjectExpression") return { reason: `${key} is not an object, so ${path.join(".")} cannot be reached` };
|
|
757
|
+
object = next;
|
|
356
758
|
}
|
|
759
|
+
return { reason: "no option path given" };
|
|
357
760
|
}
|
|
358
761
|
/**
|
|
359
|
-
*
|
|
360
|
-
*
|
|
361
|
-
*
|
|
762
|
+
* Writes `value` at `path` inside a plugin call's options, creating the options object and any
|
|
763
|
+
* intermediate object along the path as needed. Refuses when the current value at `path` is
|
|
764
|
+
* something other than a literal, so an option customized in code is never overwritten.
|
|
362
765
|
*/
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
766
|
+
function applySet(call, path, value) {
|
|
767
|
+
if (!isOptionValue(value)) return "the value is not a literal that can be written to a config file";
|
|
768
|
+
const options = ensureOptions(call);
|
|
769
|
+
if (!options) return "the plugin was not called with an object literal";
|
|
770
|
+
const target = optionParent(options, path);
|
|
771
|
+
if ("reason" in target) return target.reason;
|
|
772
|
+
const current = property(target.object, target.key);
|
|
773
|
+
if (current !== void 0 && readLiteral(current) === void 0) return `${path.join(".")} is customized in code`;
|
|
774
|
+
setProperty({
|
|
775
|
+
node: target.object,
|
|
776
|
+
key: target.key,
|
|
777
|
+
value
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Deletes the property at `path` inside a plugin call's options, falling the plugin back to its
|
|
782
|
+
* default for that option. Refuses when the value at `path` is not a literal, for the same reason
|
|
783
|
+
* `applySet` does.
|
|
784
|
+
*/
|
|
785
|
+
function applyRemove(call, path) {
|
|
786
|
+
const options = getOptions(call);
|
|
787
|
+
if (!options) return "the plugin has no options to remove";
|
|
788
|
+
const target = optionParent(options, path);
|
|
789
|
+
if ("reason" in target) return target.reason;
|
|
790
|
+
const current = property(target.object, target.key);
|
|
791
|
+
if (current === void 0) return `${path.join(".")} is not set`;
|
|
792
|
+
if (readLiteral(current) === void 0) return `${path.join(".")} is customized in code`;
|
|
793
|
+
removeProperty({
|
|
794
|
+
node: target.object,
|
|
795
|
+
key: target.key
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* Adds a `pluginX(...)` call to a config's plugins array. Replaying the same add is a no-op, while
|
|
800
|
+
* an import name collision with an unrelated package remains an error.
|
|
801
|
+
*/
|
|
802
|
+
function applyAddPlugin(mod, config, edit) {
|
|
803
|
+
if (!isKubbPluginSpecifier(edit.plugin)) return { reason: `"${edit.plugin}" is not a @kubb/plugin-* package` };
|
|
804
|
+
const importName = edit.importName ?? toExportName(edit.plugin);
|
|
805
|
+
if (!IDENTIFIER.test(importName)) return { reason: `"${importName}" is not a valid import name` };
|
|
806
|
+
if (pluginCalls(mod, config).some((plugin) => plugin.packageName === edit.plugin)) return { noop: true };
|
|
807
|
+
const taken = importedFrom(mod).get(importName);
|
|
808
|
+
if (taken && taken !== edit.plugin) return { reason: `${importName} is already imported from ${taken}` };
|
|
809
|
+
const options = edit.options ?? {};
|
|
810
|
+
if (!isOptionValue(options)) return { reason: "the options are not literals that can be written to a config file" };
|
|
811
|
+
const plugins = property(config, "plugins");
|
|
812
|
+
if (plugins?.type !== "ArrayExpression") return { reason: "plugins is not an array literal" };
|
|
813
|
+
const call = Object.keys(options).length ? magicast.builders.functionCall(importName, options) : magicast.builders.functionCall(importName);
|
|
814
|
+
plugins.elements.push(call.$ast);
|
|
815
|
+
return taken ? {} : { addImport: {
|
|
816
|
+
importName,
|
|
817
|
+
moduleSpecifier: edit.plugin
|
|
818
|
+
} };
|
|
819
|
+
}
|
|
820
|
+
/**
|
|
821
|
+
* Comments out a plugin call in place, keeping its options on disk so `enable-plugin` can restore
|
|
822
|
+
* them exactly. Operates on `source` text rather than the AST: a commented-out call is no longer a
|
|
823
|
+
* node magicast can address, and the surrounding array must not reflow when its element count
|
|
824
|
+
* never actually changes.
|
|
825
|
+
*/
|
|
826
|
+
function disablePlugin(source, mod, config, plugin) {
|
|
827
|
+
const target = pluginCalls(mod, config).find((entry) => entry.packageName === plugin);
|
|
828
|
+
if (!target) return { reason: `${plugin} is not in the plugins array` };
|
|
829
|
+
const loc = target.call.loc;
|
|
830
|
+
if (!loc?.start || !loc.end) return { reason: `${plugin} has no source location to comment out` };
|
|
831
|
+
const lines = source.split("\n");
|
|
832
|
+
const from = loc.start.line - 1;
|
|
833
|
+
const to = loc.end.line - 1;
|
|
834
|
+
const firstLine = lines[from] ?? "";
|
|
835
|
+
const lastLine = lines[to] ?? "";
|
|
836
|
+
if (firstLine.slice(0, loc.start.column).trim() !== "" || !/^,?\s*$/.test(lastLine.slice(loc.end.column))) return { reason: `${plugin} shares a line with other code, so it cannot be commented out safely` };
|
|
837
|
+
const indent = firstLine.match(/^\s*/)?.[0] ?? "";
|
|
838
|
+
const commented = lines.slice(from, to + 1).map((line) => line.trim() ? `${indent}// ${line.slice(indent.length)}` : indent ? `${indent}//` : "//");
|
|
839
|
+
lines.splice(from, to - from + 1, `${indent}// ${DISABLED_MARKER} ${plugin} ${commented.length}`, ...commented);
|
|
840
|
+
return { source: lines.join("\n") };
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Uncomments the block a previous `disable-plugin` left behind for `plugin`.
|
|
844
|
+
*/
|
|
845
|
+
function enablePlugin(source, plugin) {
|
|
846
|
+
const lines = source.split("\n");
|
|
847
|
+
for (const [index, line] of lines.entries()) {
|
|
848
|
+
const marker = parseMarker(line);
|
|
849
|
+
if (marker?.plugin !== plugin) continue;
|
|
850
|
+
const end = index + 1 + marker.lineCount;
|
|
851
|
+
const restored = lines.slice(index + 1, end).map((commented) => commented.replace(/^(\s*)\/\/ ?/, "$1"));
|
|
852
|
+
lines.splice(index, end - index, ...restored);
|
|
853
|
+
return { source: lines.join("\n") };
|
|
381
854
|
}
|
|
855
|
+
return { reason: `${plugin} is not disabled` };
|
|
382
856
|
}
|
|
383
|
-
//#endregion
|
|
384
|
-
//#region src/operations/constants.ts
|
|
385
857
|
/**
|
|
386
|
-
*
|
|
387
|
-
* not whatever default the client would pick on its own.
|
|
858
|
+
* Parses `source` and finds the config entries it exports.
|
|
388
859
|
*/
|
|
389
|
-
|
|
860
|
+
function parseConfigs(source) {
|
|
861
|
+
let mod;
|
|
862
|
+
try {
|
|
863
|
+
mod = (0, magicast.parseModule)(source);
|
|
864
|
+
} catch {
|
|
865
|
+
return { reason: "the config file could not be parsed" };
|
|
866
|
+
}
|
|
867
|
+
const found = findConfigs(mod);
|
|
868
|
+
return "reason" in found ? found : {
|
|
869
|
+
mod,
|
|
870
|
+
configs: found.configs
|
|
871
|
+
};
|
|
872
|
+
}
|
|
390
873
|
/**
|
|
391
|
-
*
|
|
392
|
-
*
|
|
874
|
+
* Re-parses `source` and resolves the config entry an edit targets. Every edit re-parses rather
|
|
875
|
+
* than sharing one module across the batch, since the disable/enable edits rewrite `source` as
|
|
876
|
+
* text and would otherwise leave the others working from a stale tree.
|
|
393
877
|
*/
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
maxHeartbeatIntervalMs: 6e4,
|
|
404
|
-
/** How long a heartbeat ping may take before the session is treated as dead. */
|
|
405
|
-
heartbeatTimeoutMs: 1e4,
|
|
406
|
-
maxConcurrent: 1,
|
|
407
|
-
maxGenerations: 8,
|
|
408
|
-
maxGenerationsMb: 100,
|
|
409
|
-
maxSnapshotMb: 50
|
|
410
|
-
};
|
|
411
|
-
function positiveNumber(value) {
|
|
412
|
-
const parsed = Number(value);
|
|
413
|
-
return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
878
|
+
function parseTarget(source, ref) {
|
|
879
|
+
const parsed = parseConfigs(source);
|
|
880
|
+
if ("reason" in parsed) return parsed;
|
|
881
|
+
const config = selectConfig(parsed.configs, ref);
|
|
882
|
+
if (!config) return { reason: `no config entry found for ${JSON.stringify(ref)}` };
|
|
883
|
+
return {
|
|
884
|
+
mod: parsed.mod,
|
|
885
|
+
config
|
|
886
|
+
};
|
|
414
887
|
}
|
|
415
888
|
/**
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
889
|
+
* Applies edits to a `kubb.config.ts` in place. Every node the edits do not touch keeps its
|
|
890
|
+
* original text, so comments, formatting, and hand-written code around the config survive.
|
|
891
|
+
*
|
|
892
|
+
* Edits are independent: one that cannot be applied is reported in `outcomes` and the rest still run.
|
|
893
|
+
*
|
|
894
|
+
* @example
|
|
895
|
+
* ```ts
|
|
896
|
+
* const { source, outcomes } = applyConfigEdits(current, [
|
|
897
|
+
* { operation: 'set', plugin: '@kubb/plugin-ts', path: ['enum', 'type'], value: 'enum' },
|
|
898
|
+
* ])
|
|
899
|
+
* ```
|
|
900
|
+
*
|
|
901
|
+
* @note recast always reprints a semicolon on a reprinted statement, so editing a block-body
|
|
902
|
+
* `defineConfig` in a semicolon-free file adds one to the `return` line. This is a known gap.
|
|
903
|
+
* Strip it when `detectCodeFormat` reports `useSemi: false`, if it turns out to matter in practice.
|
|
419
904
|
*/
|
|
420
|
-
function
|
|
905
|
+
function applyConfigEdits(source, edits) {
|
|
906
|
+
let current = source;
|
|
907
|
+
const format = (0, magicast.detectCodeFormat)(source);
|
|
908
|
+
const endsWithNewline = source.endsWith("\n");
|
|
909
|
+
const outcomes = edits.map((edit) => {
|
|
910
|
+
const target = parseTarget(current, edit.config);
|
|
911
|
+
if ("reason" in target) return {
|
|
912
|
+
edit,
|
|
913
|
+
applied: false,
|
|
914
|
+
reason: target.reason
|
|
915
|
+
};
|
|
916
|
+
const { mod, config } = target;
|
|
917
|
+
if (edit.operation === "disable-plugin" || edit.operation === "enable-plugin") {
|
|
918
|
+
const result = edit.operation === "disable-plugin" ? disablePlugin(current, mod, config, edit.plugin) : enablePlugin(current, edit.plugin);
|
|
919
|
+
if ("reason" in result) return {
|
|
920
|
+
edit,
|
|
921
|
+
applied: false,
|
|
922
|
+
reason: result.reason
|
|
923
|
+
};
|
|
924
|
+
current = result.source;
|
|
925
|
+
return {
|
|
926
|
+
edit,
|
|
927
|
+
applied: true
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
if (edit.operation === "add-plugin") {
|
|
931
|
+
const result = applyAddPlugin(mod, config, edit);
|
|
932
|
+
if ("reason" in result) return {
|
|
933
|
+
edit,
|
|
934
|
+
applied: false,
|
|
935
|
+
reason: result.reason
|
|
936
|
+
};
|
|
937
|
+
if ("noop" in result) return {
|
|
938
|
+
edit,
|
|
939
|
+
applied: true
|
|
940
|
+
};
|
|
941
|
+
const afterLine = lastImportEndLine(mod);
|
|
942
|
+
let next = (0, magicast.generateCode)(mod, { format }).code;
|
|
943
|
+
if (result.addImport) next = insertImportLine({
|
|
944
|
+
source: next,
|
|
945
|
+
afterLine,
|
|
946
|
+
...result.addImport
|
|
947
|
+
});
|
|
948
|
+
current = withTrailingNewline(next, endsWithNewline);
|
|
949
|
+
return {
|
|
950
|
+
edit,
|
|
951
|
+
applied: true
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
const pluginCall = pluginCalls(mod, config).find((plugin) => plugin.packageName === edit.plugin);
|
|
955
|
+
if (!pluginCall) return {
|
|
956
|
+
edit,
|
|
957
|
+
applied: false,
|
|
958
|
+
reason: `${edit.plugin} is not in the plugins array`
|
|
959
|
+
};
|
|
960
|
+
const reason = edit.operation === "set" ? applySet(pluginCall.call, edit.path, edit.value) : applyRemove(pluginCall.call, edit.path);
|
|
961
|
+
if (!reason) current = withTrailingNewline((0, magicast.generateCode)(mod, { format }).code, endsWithNewline);
|
|
962
|
+
return {
|
|
963
|
+
edit,
|
|
964
|
+
applied: !reason,
|
|
965
|
+
reason
|
|
966
|
+
};
|
|
967
|
+
});
|
|
421
968
|
return {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
969
|
+
source: current,
|
|
970
|
+
outcomes,
|
|
971
|
+
changed: current !== source
|
|
425
972
|
};
|
|
426
973
|
}
|
|
974
|
+
async function writeConfigEdits({ filePath, edits }) {
|
|
975
|
+
const result = applyConfigEdits(await read(filePath), edits);
|
|
976
|
+
if (result.changed) await (0, node_fs_promises.writeFile)(filePath, result.source, "utf-8");
|
|
977
|
+
return result;
|
|
978
|
+
}
|
|
427
979
|
/**
|
|
428
|
-
*
|
|
429
|
-
*
|
|
980
|
+
* The 1-based line where the file's last import declaration ends, or `0` when it has none. Read
|
|
981
|
+
* off the parsed module, so a multi-line `import {\n x,\n} from '...'` reports its closing line
|
|
982
|
+
* rather than the `import` keyword.
|
|
430
983
|
*/
|
|
431
|
-
function
|
|
432
|
-
return
|
|
984
|
+
function lastImportEndLine(mod) {
|
|
985
|
+
return (mod.$ast.type === "Program" ? mod.$ast.body : []).filter((node) => node.type === "ImportDeclaration").at(-1)?.loc?.end.line ?? 0;
|
|
986
|
+
}
|
|
987
|
+
/**
|
|
988
|
+
* Writes an import after the last one already in the file, matching its quote style and whether it
|
|
989
|
+
* ends in a semicolon. `afterLine` is where that last import ends, `0` for a file with none.
|
|
990
|
+
*
|
|
991
|
+
* Written as plain text rather than through magicast's import builder, which prints a brand-new
|
|
992
|
+
* import declaration with its own default spacing and a semicolon regardless of `format`, since
|
|
993
|
+
* that formatting only governs nodes recast can diff against the original source.
|
|
994
|
+
*/
|
|
995
|
+
function insertImportLine({ source, importName, moduleSpecifier, afterLine }) {
|
|
996
|
+
const lines = source.split("\n");
|
|
997
|
+
const lastImportLine = afterLine > 0 ? lines[afterLine - 1] : void 0;
|
|
998
|
+
const quote = lastImportLine?.includes(`"`) ? `"` : `'`;
|
|
999
|
+
const line = `import { ${importName} } from ${quote}${moduleSpecifier}${quote}${lastImportLine?.trimEnd().endsWith(";") ? ";" : ""}`;
|
|
1000
|
+
lines.splice(afterLine, 0, ...afterLine > 0 ? [line] : [line, ""]);
|
|
1001
|
+
return lines.join("\n");
|
|
1002
|
+
}
|
|
1003
|
+
/**
|
|
1004
|
+
* `generateCode` always drops the file's trailing newline. Restore it when the input had one.
|
|
1005
|
+
*/
|
|
1006
|
+
function withTrailingNewline(code, hadTrailingNewline) {
|
|
1007
|
+
if (!hadTrailingNewline || code.endsWith("\n")) return code;
|
|
1008
|
+
return `${code}\n`;
|
|
433
1009
|
}
|
|
434
1010
|
//#endregion
|
|
435
|
-
//#region
|
|
436
|
-
var version = "5.6.0";
|
|
437
|
-
//#endregion
|
|
438
|
-
//#region src/operations/generations.ts
|
|
1011
|
+
//#region src/generations.ts
|
|
439
1012
|
const READ_CONCURRENCY = 50;
|
|
440
1013
|
const MB = 1048576;
|
|
441
1014
|
const INDEX_KEY = "studio/generations.json";
|
|
@@ -445,8 +1018,63 @@ const INDEX_KEY = "studio/generations.json";
|
|
|
445
1018
|
function relativeStoragePath({ root, filePath }) {
|
|
446
1019
|
return ((0, node_path.isAbsolute)(filePath) ? (0, node_path.relative)((0, node_path.resolve)(root), filePath) : filePath).replaceAll("\\", "/");
|
|
447
1020
|
}
|
|
1021
|
+
/**
|
|
1022
|
+
* The content fingerprint Studio compares snapshots by. Agent runs and CI uploads must agree on it.
|
|
1023
|
+
*/
|
|
448
1024
|
const hashOf = (content) => (0, node_crypto.createHash)("sha1").update(content).digest("hex").slice(0, 16);
|
|
449
1025
|
const totalBytes = (entries) => entries.reduce((sum, { output, disk }) => sum + output.bytes + (disk?.bytes ?? 0), 0);
|
|
1026
|
+
function positiveNumber(value) {
|
|
1027
|
+
const parsed = Number(value);
|
|
1028
|
+
return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
1029
|
+
}
|
|
1030
|
+
/**
|
|
1031
|
+
* How many generations an agent keeps and how large they may get, read from
|
|
1032
|
+
* `KUBB_AGENT_MAX_GENERATIONS`, `KUBB_AGENT_MAX_GENERATIONS_MB` and `KUBB_AGENT_MAX_SNAPSHOT_MB`.
|
|
1033
|
+
* An unset or invalid value keeps the default.
|
|
1034
|
+
*/
|
|
1035
|
+
function resolveGenerationLimits() {
|
|
1036
|
+
return {
|
|
1037
|
+
maxCount: Math.max(1, Math.floor(positiveNumber(node_process.default.env.KUBB_AGENT_MAX_GENERATIONS) ?? 8)),
|
|
1038
|
+
maxMb: positiveNumber(node_process.default.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100,
|
|
1039
|
+
maxSnapshotMb: positiveNumber(node_process.default.env.KUBB_AGENT_MAX_SNAPSHOT_MB) ?? 50
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Folds error-severity diagnostics into one thrown error so logs name the failing plugin or pass.
|
|
1044
|
+
*/
|
|
1045
|
+
function generationFailure(diagnostics) {
|
|
1046
|
+
const reasons = diagnostics.filter(_kubb_core.Diagnostics.isProblem).filter((diagnostic) => diagnostic.severity === "error").map((diagnostic) => diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message);
|
|
1047
|
+
if (!reasons.length) return /* @__PURE__ */ new Error("Generation failed");
|
|
1048
|
+
return /* @__PURE__ */ new Error(`Generation failed: ${reasons.length} error${reasons.length === 1 ? "" : "s"}: ${reasons.join("; ")}`);
|
|
1049
|
+
}
|
|
1050
|
+
/**
|
|
1051
|
+
* Runs one generation and returns the files it produced. The formatter, the linter, and
|
|
1052
|
+
* `output.postGenerate` only run with `allowExec`. Throws when any plugin reports an error.
|
|
1053
|
+
*/
|
|
1054
|
+
async function generate({ config, hooks, signal, allowExec }) {
|
|
1055
|
+
const output = allowExec ? config.output : {
|
|
1056
|
+
...config.output,
|
|
1057
|
+
format: false,
|
|
1058
|
+
lint: false,
|
|
1059
|
+
postGenerate: []
|
|
1060
|
+
};
|
|
1061
|
+
const result = await (0, _kubb_core.createKubb)({
|
|
1062
|
+
...config,
|
|
1063
|
+
output
|
|
1064
|
+
}, {
|
|
1065
|
+
hooks,
|
|
1066
|
+
signal
|
|
1067
|
+
}).generate();
|
|
1068
|
+
if (!result.success) throw generationFailure(result.diagnostics);
|
|
1069
|
+
return {
|
|
1070
|
+
storage: config.storage,
|
|
1071
|
+
root: config.root,
|
|
1072
|
+
paths: new Set(result.files.map((file) => relativeStoragePath({
|
|
1073
|
+
root: config.root,
|
|
1074
|
+
filePath: file.path
|
|
1075
|
+
})))
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
450
1078
|
/**
|
|
451
1079
|
* What the output directory holds on disk before a run. `undefined` when `outputPath` is not a real
|
|
452
1080
|
* subdirectory of `root` (listing the root would take in every source file) or holds more than `maxFiles`.
|
|
@@ -525,9 +1153,6 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
525
1153
|
keep,
|
|
526
1154
|
drop,
|
|
527
1155
|
get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId && isLive(generation)),
|
|
528
|
-
latest: async () => (await load()).filter(isLive).at(-1),
|
|
529
|
-
/** Total bytes of every set the store holds, expired ones too until the next add drops them. */
|
|
530
|
-
bytes: async () => totalBytes(await load()),
|
|
531
1156
|
async add(generation) {
|
|
532
1157
|
const current = await load();
|
|
533
1158
|
for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
|
|
@@ -574,7 +1199,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
574
1199
|
};
|
|
575
1200
|
}
|
|
576
1201
|
//#endregion
|
|
577
|
-
//#region src/
|
|
1202
|
+
//#region src/generationEvents.ts
|
|
578
1203
|
const MAX_QUEUED_EVENTS = 1024;
|
|
579
1204
|
const RESERVED_EVENTS = 64;
|
|
580
1205
|
const isDiscardable = (event) => event.type === "kubb:files:processing:update" || event.type === "kubb:info" || event.type === "kubb:success";
|
|
@@ -755,423 +1380,151 @@ function createGenerationStream(hooks, jobId) {
|
|
|
755
1380
|
};
|
|
756
1381
|
}
|
|
757
1382
|
//#endregion
|
|
758
|
-
//#region src/
|
|
759
|
-
const CONNECT_TIMEOUT_MS = 5e3;
|
|
760
|
-
/**
|
|
761
|
-
* The only methods Studio may call on an agent. A `StudioSession` carries far more than
|
|
762
|
-
* {@link AgentApi}, so it is wrapped rather than exposed: what Cap'n Web can reach is exactly what
|
|
763
|
-
* this class re-declares.
|
|
764
|
-
*/
|
|
765
|
-
var AgentRpcTarget = class extends capnweb.RpcTarget {
|
|
766
|
-
api;
|
|
767
|
-
constructor(api) {
|
|
768
|
-
super();
|
|
769
|
-
this.api = api;
|
|
770
|
-
}
|
|
771
|
-
connect() {
|
|
772
|
-
return this.api.connect();
|
|
773
|
-
}
|
|
774
|
-
startGeneration(input) {
|
|
775
|
-
return this.api.startGeneration(input);
|
|
776
|
-
}
|
|
777
|
-
saveConfig(input) {
|
|
778
|
-
return this.api.saveConfig(input);
|
|
779
|
-
}
|
|
780
|
-
publishSnapshot(input) {
|
|
781
|
-
return this.api.publishSnapshot(input);
|
|
782
|
-
}
|
|
783
|
-
readFiles(input) {
|
|
784
|
-
return this.api.readFiles(input);
|
|
785
|
-
}
|
|
786
|
-
cancel(jobId) {
|
|
787
|
-
return this.api.cancel(jobId);
|
|
788
|
-
}
|
|
789
|
-
};
|
|
1383
|
+
//#region src/url.ts
|
|
790
1384
|
/**
|
|
791
|
-
*
|
|
792
|
-
* fires no `close` event when the handshake itself fails, only `error`, so `closed` settles as 1006 there.
|
|
1385
|
+
* Hosted Kubb Studio URL, used when a host passes none.
|
|
793
1386
|
*/
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
return
|
|
797
|
-
socket,
|
|
798
|
-
closed: new Promise((resolve) => {
|
|
799
|
-
let opened = false;
|
|
800
|
-
const timer = setTimeout(() => {
|
|
801
|
-
if (socket.readyState === WebSocket.CONNECTING) socket.close(3008, "Connection timeout");
|
|
802
|
-
}, CONNECT_TIMEOUT_MS);
|
|
803
|
-
socket.addEventListener("open", () => {
|
|
804
|
-
opened = true;
|
|
805
|
-
clearTimeout(timer);
|
|
806
|
-
}, { once: true });
|
|
807
|
-
socket.addEventListener("close", (event) => {
|
|
808
|
-
clearTimeout(timer);
|
|
809
|
-
resolve({
|
|
810
|
-
code: event.code,
|
|
811
|
-
reason: event.reason
|
|
812
|
-
});
|
|
813
|
-
}, { once: true });
|
|
814
|
-
socket.addEventListener("error", () => {
|
|
815
|
-
if (opened) return;
|
|
816
|
-
clearTimeout(timer);
|
|
817
|
-
resolve({
|
|
818
|
-
code: 1006,
|
|
819
|
-
reason: ""
|
|
820
|
-
});
|
|
821
|
-
});
|
|
822
|
-
})
|
|
823
|
-
};
|
|
1387
|
+
const defaultStudioUrl = "https://kubb.studio";
|
|
1388
|
+
function isLoopbackHost(hostname) {
|
|
1389
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
824
1390
|
}
|
|
825
1391
|
/**
|
|
826
|
-
*
|
|
827
|
-
*
|
|
1392
|
+
* Appends `path` to the Studio URL and refuses plaintext to anything but this machine, so a token
|
|
1393
|
+
* or API key never crosses the network unencrypted. `socket` switches the scheme to `ws:`/`wss:`.
|
|
828
1394
|
*
|
|
829
1395
|
* @example
|
|
830
1396
|
* ```ts
|
|
831
|
-
*
|
|
832
|
-
* await rpc.studio.ping()
|
|
1397
|
+
* secureUrl({ studioUrl: 'https://kubb.studio', path: '/api/agent/socket', socket: true }).href // 'wss://kubb.studio/api/agent/socket'
|
|
833
1398
|
* ```
|
|
834
1399
|
*/
|
|
835
|
-
|
|
836
|
-
const
|
|
837
|
-
if (protocol !== "
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
Authorization: `Bearer ${token}`,
|
|
842
|
-
[require_protocol.AGENT_INSTANCE_HEADER]: instanceId
|
|
843
|
-
}
|
|
844
|
-
});
|
|
845
|
-
const studio = (0, capnweb.newWebSocketRpcSession)(socket, new AgentRpcTarget(local));
|
|
846
|
-
studio.onRpcBroken(() => socket.close());
|
|
847
|
-
return {
|
|
848
|
-
studio,
|
|
849
|
-
closed,
|
|
850
|
-
close: () => studio[Symbol.dispose]()
|
|
851
|
-
};
|
|
852
|
-
};
|
|
1400
|
+
function secureUrl({ studioUrl, path, socket = false }) {
|
|
1401
|
+
const url = new URL(`${studioUrl.replace(/\/+$/, "")}${path}`);
|
|
1402
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) throw new Error(`Refusing to reach Kubb Studio at ${url.origin}: use an https:// URL, or a loopback host for local development`);
|
|
1403
|
+
if (socket) url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1404
|
+
return url;
|
|
1405
|
+
}
|
|
853
1406
|
//#endregion
|
|
854
|
-
//#region src/
|
|
1407
|
+
//#region src/connect.ts
|
|
1408
|
+
const HEARTBEAT_INTERVAL_MS = 3e4;
|
|
1409
|
+
const MAX_HEARTBEAT_INTERVAL_MS = 6e4;
|
|
1410
|
+
const PING_TIMEOUT_MS = 1e4;
|
|
1411
|
+
const OPEN_TIMEOUT_MS = 5e3;
|
|
1412
|
+
const MAX_RETRY_DELAY_MS = 3e4;
|
|
855
1413
|
const DISK_SNAPSHOT_MAX_FILES = 1e4;
|
|
1414
|
+
const SANDBOX_GENERATION_TTL_MS = 9e5;
|
|
856
1415
|
/**
|
|
857
|
-
*
|
|
1416
|
+
* Studio closed with {@link AgentCloseCode.UNAUTHORIZED}. Hosts forget the token and pair again.
|
|
858
1417
|
*/
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
const SANDBOX_GENERATION_TTL_MS = 9e5;
|
|
1418
|
+
var InvalidAgentTokenError = class extends Error {
|
|
1419
|
+
constructor(studioUrl) {
|
|
1420
|
+
super(`Kubb Studio rejected this agent's token. It expired or was revoked, or the agent was deleted in ${studioUrl}.`);
|
|
1421
|
+
this.name = "InvalidAgentTokenError";
|
|
1422
|
+
}
|
|
1423
|
+
};
|
|
866
1424
|
/**
|
|
867
|
-
*
|
|
868
|
-
* shared the agent's own root would read each other's manifest.
|
|
1425
|
+
* Studio closed with {@link AgentCloseCode.INCOMPATIBLE}. Only an upgrade helps.
|
|
869
1426
|
*/
|
|
870
|
-
|
|
871
|
-
|
|
1427
|
+
var IncompatibleAgentError = class extends Error {
|
|
1428
|
+
constructor({ studioUrl, detail }) {
|
|
1429
|
+
super(`Kubb Studio at ${studioUrl} requires a newer agent${detail ? `: ${detail}` : ""}. Upgrade kubb or the Kubb agent image.`);
|
|
1430
|
+
this.name = "IncompatibleAgentError";
|
|
1431
|
+
}
|
|
1432
|
+
};
|
|
1433
|
+
function emit(hooks, name, ...args) {
|
|
1434
|
+
return Promise.resolve(hooks.callHook(name, ...args)).catch(() => {});
|
|
872
1435
|
}
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
async function removeJobRoot(jobRoot) {
|
|
878
|
-
await Promise.all([(0, node_fs_promises.rm)(jobRoot, {
|
|
879
|
-
recursive: true,
|
|
880
|
-
force: true
|
|
881
|
-
}), (0, node_fs_promises.rm)((0, _kubb_core.resolveCacheDir)(jobRoot), {
|
|
882
|
-
recursive: true,
|
|
883
|
-
force: true
|
|
884
|
-
})]).catch(() => {});
|
|
1436
|
+
function isValidPage({ cursor, limit }) {
|
|
1437
|
+
if (limit === void 0) return cursor === void 0;
|
|
1438
|
+
if (cursor !== void 0 && (!Number.isSafeInteger(cursor) || cursor < 0)) return false;
|
|
1439
|
+
return Number.isSafeInteger(limit) && limit >= 1 && limit <= 2e3;
|
|
885
1440
|
}
|
|
886
1441
|
var GenerationRunTarget = class extends capnweb.RpcTarget {
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
cancelGeneration;
|
|
890
|
-
stopGeneration;
|
|
891
|
-
constructor(generationStream, generationResult, cancelGeneration, stopGeneration) {
|
|
1442
|
+
#run;
|
|
1443
|
+
constructor(run) {
|
|
892
1444
|
super();
|
|
893
|
-
this
|
|
894
|
-
this.generationResult = generationResult;
|
|
895
|
-
this.cancelGeneration = cancelGeneration;
|
|
896
|
-
this.stopGeneration = stopGeneration;
|
|
1445
|
+
this.#run = run;
|
|
897
1446
|
}
|
|
898
1447
|
async events() {
|
|
899
|
-
return this.
|
|
1448
|
+
return this.#run.stream;
|
|
900
1449
|
}
|
|
901
1450
|
result() {
|
|
902
|
-
return this.
|
|
1451
|
+
return this.#run.result;
|
|
903
1452
|
}
|
|
904
|
-
cancel() {
|
|
905
|
-
|
|
1453
|
+
async cancel() {
|
|
1454
|
+
this.#run.stop();
|
|
906
1455
|
}
|
|
907
1456
|
[Symbol.dispose]() {
|
|
908
|
-
this.
|
|
1457
|
+
this.#run.stop();
|
|
909
1458
|
}
|
|
910
1459
|
};
|
|
911
|
-
|
|
912
|
-
* Fills in a host's options: the hosted Studio URL, the current working directory, and every
|
|
913
|
-
* permission off unless granted. Idempotent, so a reconnect can pass an already-resolved bag
|
|
914
|
-
* back in.
|
|
915
|
-
*/
|
|
916
|
-
function applyStudioDefaults(options) {
|
|
917
|
-
const root = options.root ?? node_process.default.cwd();
|
|
918
|
-
return {
|
|
919
|
-
...options,
|
|
920
|
-
studioUrl: options.studioUrl ?? agentDefaults.studioUrl,
|
|
921
|
-
root,
|
|
922
|
-
configFile: node_path.default.resolve(root, options.configPath),
|
|
923
|
-
permissions: {
|
|
924
|
-
allowWrite: false,
|
|
925
|
-
allowConfigEdit: false,
|
|
926
|
-
allowExec: false,
|
|
927
|
-
allowRead: false,
|
|
928
|
-
...options.permissions
|
|
929
|
-
},
|
|
930
|
-
heartbeatInterval: Math.min(options.heartbeatInterval ?? agentDefaults.heartbeatIntervalMs, agentDefaults.maxHeartbeatIntervalMs),
|
|
931
|
-
capacity: {
|
|
932
|
-
...resolveAgentCapacity(),
|
|
933
|
-
...options.capacity
|
|
934
|
-
},
|
|
935
|
-
instanceId: options.instanceId ?? (0, node_crypto.randomUUID)()
|
|
936
|
-
};
|
|
937
|
-
}
|
|
938
|
-
/**
|
|
939
|
-
* Jobs one agent process can run at once today. Two runs would share this session's hook emitter,
|
|
940
|
-
* and with it each other's events, so the connection keeps this limit.
|
|
941
|
-
*/
|
|
942
|
-
const RUNTIME_MAX_CONCURRENT = 1;
|
|
943
|
-
/**
|
|
944
|
-
* Reads what Studio meant by closing the connection. A code Studio did not send on purpose is an
|
|
945
|
-
* ordinary drop, and the agent reconnects as it always has.
|
|
946
|
-
*/
|
|
947
|
-
function planEnd(close) {
|
|
948
|
-
const code = close?.code;
|
|
949
|
-
if (code === require_protocol.AgentCloseCode.REAUTHENTICATE) return {
|
|
950
|
-
reason: "Kubb Studio asked the agent to register again",
|
|
951
|
-
retry: true
|
|
952
|
-
};
|
|
953
|
-
if (code === require_protocol.AgentCloseCode.SUPERSEDED) return {
|
|
954
|
-
reason: "another instance of this agent took over",
|
|
955
|
-
retry: false
|
|
956
|
-
};
|
|
957
|
-
if (code === require_protocol.AgentCloseCode.INCOMPATIBLE) return {
|
|
958
|
-
reason: "this agent is too old for Kubb Studio, or was deleted",
|
|
959
|
-
retry: false,
|
|
960
|
-
error: /* @__PURE__ */ new Error("Kubb Studio closed the connection: this agent is too old for it, or was deleted. Upgrade the agent, or pair it again.")
|
|
961
|
-
};
|
|
962
|
-
return {
|
|
963
|
-
reason: "connection closed",
|
|
964
|
-
retry: true
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
function isValidPage({ cursor, limit }) {
|
|
968
|
-
if (limit === void 0) return cursor === void 0;
|
|
969
|
-
if (cursor !== void 0 && (!Number.isSafeInteger(cursor) || cursor < 0)) return false;
|
|
970
|
-
return Number.isSafeInteger(limit) && limit >= 1 && limit <= 2e3;
|
|
971
|
-
}
|
|
972
|
-
/**
|
|
973
|
-
* One agent-to-Studio RPC transport: opening it, keeping it alive, and serving remote methods.
|
|
974
|
-
* `runConnection` owns the lifetime and retry of each attempt.
|
|
975
|
-
*/
|
|
976
|
-
var StudioSession = class {
|
|
1460
|
+
var Agent = class extends capnweb.RpcTarget {
|
|
977
1461
|
#options;
|
|
978
|
-
#
|
|
979
|
-
/**
|
|
980
|
-
* Removers for every listener this session added (socket, shutdown signal, hooks) so `dispose`
|
|
981
|
-
* detaches them in one pass. Listeners a host attached itself through `installLogger` survive.
|
|
982
|
-
*/
|
|
983
|
-
#unhooks = [];
|
|
984
|
-
/**
|
|
985
|
-
* What registration handed back, and the marker for whether the agent registered at all.
|
|
986
|
-
* Before it resolves there is no sandbox flag to read.
|
|
987
|
-
*/
|
|
988
|
-
#registration;
|
|
989
|
-
#rpc;
|
|
990
|
-
#disposed = false;
|
|
991
|
-
#isGenerating = false;
|
|
992
|
-
#activeJob;
|
|
993
|
-
#heartbeatTimer;
|
|
994
|
-
#store;
|
|
1462
|
+
#session;
|
|
995
1463
|
#limits = resolveGenerationLimits();
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
#ended = Promise.withResolvers();
|
|
1002
|
-
closed = this.#ended.promise;
|
|
1003
|
-
#generationResult;
|
|
1004
|
-
constructor(options) {
|
|
1005
|
-
this.#options = applyStudioDefaults(options);
|
|
1006
|
-
this.#connectAck.promise.catch(() => {});
|
|
1464
|
+
#store;
|
|
1465
|
+
constructor({ options, session }) {
|
|
1466
|
+
super();
|
|
1467
|
+
this.#options = options;
|
|
1468
|
+
this.#session = session;
|
|
1007
1469
|
}
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
get #
|
|
1012
|
-
|
|
1470
|
+
get #sandbox() {
|
|
1471
|
+
return this.#session.info?.sandbox === true;
|
|
1472
|
+
}
|
|
1473
|
+
get #permissions() {
|
|
1474
|
+
const { permissions } = this.#options;
|
|
1475
|
+
return this.#sandbox ? {
|
|
1476
|
+
...permissions,
|
|
1477
|
+
allowWrite: false,
|
|
1478
|
+
allowConfigEdit: false,
|
|
1479
|
+
allowRead: true
|
|
1480
|
+
} : permissions;
|
|
1013
1481
|
}
|
|
1014
|
-
/**
|
|
1015
|
-
* Kept in the project's cache directory, so it survives a restart, except on a sandbox: its pool
|
|
1016
|
-
* sessions run every tenant's jobs, so it keeps them in memory. Looked up by job id only.
|
|
1017
|
-
*/
|
|
1018
1482
|
get #generations() {
|
|
1019
1483
|
this.#store ??= createGenerationStore({
|
|
1020
|
-
storage: this.#
|
|
1484
|
+
storage: this.#sandbox ? (0, _kubb_core.memoryStorage)() : (0, _kubb_core.cacheStorage)({ root: this.#options.root }),
|
|
1021
1485
|
maxCount: this.#limits.maxCount,
|
|
1022
1486
|
maxMb: this.#limits.maxMb,
|
|
1023
|
-
ttlMs: this.#
|
|
1487
|
+
ttlMs: this.#sandbox ? SANDBOX_GENERATION_TTL_MS : void 0
|
|
1024
1488
|
});
|
|
1025
1489
|
return this.#store;
|
|
1026
1490
|
}
|
|
1027
|
-
|
|
1028
|
-
return
|
|
1029
|
-
}
|
|
1030
|
-
get #canEditConfig() {
|
|
1031
|
-
return !this.#isSandbox && this.#options.permissions.allowConfigEdit;
|
|
1032
|
-
}
|
|
1033
|
-
/**
|
|
1034
|
-
* A sandbox agent always allows reading its output back; a local agent only when opted in.
|
|
1035
|
-
*/
|
|
1036
|
-
get #canRead() {
|
|
1037
|
-
return this.#isSandbox || this.#options.permissions.allowRead;
|
|
1038
|
-
}
|
|
1039
|
-
async start() {
|
|
1040
|
-
const { token, studioUrl, signal, heartbeatInterval, installLogger, instanceId, capacity } = this.#options;
|
|
1041
|
-
await installLogger?.(this.#hooks);
|
|
1042
|
-
signal?.addEventListener("abort", this.#onAbort, { once: true });
|
|
1043
|
-
this.#unhooks.push(() => signal?.removeEventListener("abort", this.#onAbort));
|
|
1044
|
-
try {
|
|
1045
|
-
signal?.throwIfAborted();
|
|
1046
|
-
await this.#hooks.callHook("studio:connecting", { url: studioUrl });
|
|
1047
|
-
if (capacity.maxConcurrent > RUNTIME_MAX_CONCURRENT) await this.#warn(`Running ${RUNTIME_MAX_CONCURRENT} job at a time: KUBB_AGENT_MAX_CONCURRENT=${capacity.maxConcurrent} needs per-job workers, which this agent does not have yet`);
|
|
1048
|
-
const registration = await registerAgent({
|
|
1049
|
-
signal,
|
|
1050
|
-
token,
|
|
1051
|
-
studioUrl,
|
|
1052
|
-
instanceId,
|
|
1053
|
-
capacity: {
|
|
1054
|
-
...capacity,
|
|
1055
|
-
maxConcurrent: Math.min(capacity.maxConcurrent, RUNTIME_MAX_CONCURRENT)
|
|
1056
|
-
}
|
|
1057
|
-
});
|
|
1058
|
-
signal?.throwIfAborted();
|
|
1059
|
-
this.#registration = registration;
|
|
1060
|
-
const rpc = await (this.#options.connector ?? connectWebSocketRpc)({
|
|
1061
|
-
url: registration.socketUrl,
|
|
1062
|
-
token,
|
|
1063
|
-
instanceId,
|
|
1064
|
-
local: this
|
|
1065
|
-
});
|
|
1066
|
-
this.#rpc = rpc;
|
|
1067
|
-
rpc.closed.then(this.#onClose);
|
|
1068
|
-
if (signal?.aborted || this.#disposed) {
|
|
1069
|
-
rpc.close();
|
|
1070
|
-
throw new Error("Connection stopped during startup");
|
|
1071
|
-
}
|
|
1072
|
-
this.#scheduleHeartbeat(heartbeatInterval);
|
|
1073
|
-
await this.#hooks.callHook("studio:connected", {
|
|
1074
|
-
url: studioUrl,
|
|
1075
|
-
versions: {
|
|
1076
|
-
studio: registration.version,
|
|
1077
|
-
kubb: version,
|
|
1078
|
-
agent: this.#options.version
|
|
1079
|
-
},
|
|
1080
|
-
agentSlug: registration.agentSlug,
|
|
1081
|
-
organizationSlug: registration.organizationSlug
|
|
1082
|
-
});
|
|
1083
|
-
await this.#connectAck.promise;
|
|
1084
|
-
await this.#hooks.callHook("studio:ready", {});
|
|
1085
|
-
} catch (error) {
|
|
1086
|
-
await this.dispose();
|
|
1087
|
-
if (!signal?.aborted) await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
1088
|
-
throw error;
|
|
1089
|
-
}
|
|
1090
|
-
}
|
|
1091
|
-
reportRetry(delayMs) {
|
|
1092
|
-
return this.#hooks.callHook("studio:reconnecting", { delayMs });
|
|
1093
|
-
}
|
|
1094
|
-
#warn(message, permission) {
|
|
1095
|
-
return this.#hooks.callHook("studio:warn", {
|
|
1491
|
+
#warn({ message, permission }) {
|
|
1492
|
+
return emit(this.#options.hooks, "studio:warn", {
|
|
1096
1493
|
message,
|
|
1097
1494
|
permission
|
|
1098
1495
|
});
|
|
1099
1496
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
await this.#warn(reason, permission);
|
|
1497
|
+
async #refuse({ message, permission }) {
|
|
1498
|
+
await this.#warn({
|
|
1499
|
+
message,
|
|
1500
|
+
permission
|
|
1501
|
+
});
|
|
1106
1502
|
throw new Error(message);
|
|
1107
1503
|
}
|
|
1108
|
-
/**
|
|
1109
|
-
* Brackets one Studio command with `studio:command:start` and `studio:command:end`. A command
|
|
1110
|
-
* that declined its request reports no `info`, and only the start is heard.
|
|
1111
|
-
*/
|
|
1112
1504
|
async #command({ command, run }) {
|
|
1113
|
-
|
|
1505
|
+
if (!this.#session.info) throw new Error("Studio must call connect() before any other command");
|
|
1114
1506
|
const { value, info } = await run();
|
|
1115
|
-
if (info !== void 0) await this.#hooks
|
|
1507
|
+
if (info !== void 0) await emit(this.#options.hooks, "studio:command", {
|
|
1116
1508
|
command,
|
|
1117
1509
|
info
|
|
1118
1510
|
});
|
|
1119
1511
|
return value;
|
|
1120
1512
|
}
|
|
1121
|
-
#scheduleHeartbeat(interval) {
|
|
1122
|
-
const rpc = this.#rpc;
|
|
1123
|
-
if (!rpc) return;
|
|
1124
|
-
this.#heartbeatTimer = setTimeout(async () => {
|
|
1125
|
-
try {
|
|
1126
|
-
await this.#ping(rpc);
|
|
1127
|
-
} catch {
|
|
1128
|
-
if (this.#rpc === rpc) rpc.close();
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
if (this.#rpc === rpc) this.#scheduleHeartbeat(interval);
|
|
1132
|
-
}, interval);
|
|
1133
|
-
}
|
|
1134
|
-
/**
|
|
1135
|
-
* Races `studio.ping()` against a deadline, so a half-open socket can't hang it forever.
|
|
1136
|
-
* */
|
|
1137
|
-
async #ping(rpc) {
|
|
1138
|
-
const { promise: timedOut, reject: onTimeout } = Promise.withResolvers();
|
|
1139
|
-
const timer = setTimeout(() => onTimeout(/* @__PURE__ */ new Error("Heartbeat ping timed out")), agentDefaults.heartbeatTimeoutMs);
|
|
1140
|
-
try {
|
|
1141
|
-
const load = await Promise.race([this.#load().catch(() => void 0), timedOut]);
|
|
1142
|
-
await Promise.race([rpc.studio.ping(load), timedOut]);
|
|
1143
|
-
} finally {
|
|
1144
|
-
clearTimeout(timer);
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
async #load() {
|
|
1148
|
-
return {
|
|
1149
|
-
running: this.#isGenerating ? 1 : 0,
|
|
1150
|
-
rssMb: Math.round(node_process.default.memoryUsage().rss / 1048576),
|
|
1151
|
-
storeBytes: await this.#generations.bytes(),
|
|
1152
|
-
accepting: true
|
|
1153
|
-
};
|
|
1154
|
-
}
|
|
1155
|
-
/**
|
|
1156
|
-
* Reads `kubb.config.ts` and reports which plugin options Studio may edit.
|
|
1157
|
-
*
|
|
1158
|
-
* Skipped when the host did not grant `allowConfigEdit`. Not cached: the user can edit the file
|
|
1159
|
-
* between two Studio actions.
|
|
1160
|
-
*/
|
|
1161
1513
|
async #readConfigFileView(source) {
|
|
1162
|
-
if (!this.#
|
|
1514
|
+
if (!this.#permissions.allowConfigEdit) return void 0;
|
|
1163
1515
|
try {
|
|
1164
|
-
|
|
1165
|
-
return readConfig(source ?? await require_resolveConfig.read(this.#options.configFile));
|
|
1516
|
+
return readConfig(source ?? await read(this.#options.configFile));
|
|
1166
1517
|
} catch (error) {
|
|
1167
|
-
await this.#warn(`Could not read ${this.#options.configFile}: ${getErrorMessage(error)}`);
|
|
1518
|
+
await this.#warn({ message: `Could not read ${this.#options.configFile}: ${getErrorMessage(error)}` });
|
|
1168
1519
|
return;
|
|
1169
1520
|
}
|
|
1170
1521
|
}
|
|
1171
|
-
async connect() {
|
|
1172
|
-
|
|
1522
|
+
async connect(info) {
|
|
1523
|
+
this.#session.info = info;
|
|
1524
|
+
const { configPath, root, version: version$1, loadConfig } = this.#options;
|
|
1173
1525
|
const [config, file] = await Promise.all([loadConfig(), this.#readConfigFileView()]);
|
|
1174
|
-
|
|
1526
|
+
this.#session.connected.resolve(info);
|
|
1527
|
+
return {
|
|
1175
1528
|
versions: {
|
|
1176
1529
|
kubb: version,
|
|
1177
1530
|
agent: version$1
|
|
@@ -1181,201 +1534,135 @@ var StudioSession = class {
|
|
|
1181
1534
|
path: configPath,
|
|
1182
1535
|
file,
|
|
1183
1536
|
plugins: config.plugins.map((plugin) => ({
|
|
1184
|
-
name:
|
|
1537
|
+
name: toPackageName(plugin.name),
|
|
1185
1538
|
options: plugin.options ?? {}
|
|
1186
1539
|
}))
|
|
1187
1540
|
},
|
|
1188
|
-
permissions:
|
|
1189
|
-
...permissions,
|
|
1190
|
-
allowWrite: this.#canWrite,
|
|
1191
|
-
allowConfigEdit: this.#canEditConfig,
|
|
1192
|
-
allowRead: this.#canRead
|
|
1193
|
-
}
|
|
1541
|
+
permissions: this.#permissions
|
|
1194
1542
|
};
|
|
1195
|
-
this.#connectAck.resolve();
|
|
1196
|
-
return payload;
|
|
1197
|
-
}
|
|
1198
|
-
#onAbort = () => void this.#end({
|
|
1199
|
-
reason: "shutdown",
|
|
1200
|
-
retry: false
|
|
1201
|
-
}).catch(() => {});
|
|
1202
|
-
#onClose = (close) => void this.#end(planEnd(close)).catch(() => {});
|
|
1203
|
-
/** Closes this attempt and waits for its generation to stop before another can start. */
|
|
1204
|
-
dispose() {
|
|
1205
|
-
return this.#end({
|
|
1206
|
-
reason: "shutdown",
|
|
1207
|
-
retry: false
|
|
1208
|
-
});
|
|
1209
|
-
}
|
|
1210
|
-
async #end(plan) {
|
|
1211
|
-
if (this.#disposed) {
|
|
1212
|
-
await this.closed;
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
this.#disposed = true;
|
|
1216
|
-
clearTimeout(this.#heartbeatTimer);
|
|
1217
|
-
this.#rpc?.close();
|
|
1218
|
-
this.#rpc = void 0;
|
|
1219
|
-
this.#connectAck.reject(/* @__PURE__ */ new Error("Session ended before Studio called connect()"));
|
|
1220
|
-
for (const unhook of this.#unhooks) unhook();
|
|
1221
|
-
this.#unhooks.length = 0;
|
|
1222
|
-
try {
|
|
1223
|
-
await this.#activeJob?.cancel();
|
|
1224
|
-
await this.#generationResult?.catch(() => {});
|
|
1225
|
-
await this.#hooks.callHook("studio:disconnected", { reason: plan.reason });
|
|
1226
|
-
if (plan.error) await this.#hooks.callHook("studio:error", { error: plan.error });
|
|
1227
|
-
} finally {
|
|
1228
|
-
this.#ended.resolve(plan);
|
|
1229
|
-
}
|
|
1230
1543
|
}
|
|
1231
|
-
startGeneration(
|
|
1232
|
-
const
|
|
1544
|
+
startGeneration(input) {
|
|
1545
|
+
const stream = createGenerationStream(this.#options.hooks, input.jobId);
|
|
1233
1546
|
const controller = new AbortController();
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
};
|
|
1237
|
-
const result = this.#runGeneration(data, controller, cancelRun).then(async (value) => {
|
|
1238
|
-
await generationStream.close();
|
|
1239
|
-
return value;
|
|
1240
|
-
}).catch((error) => {
|
|
1241
|
-
generationStream.fail(error);
|
|
1547
|
+
const result = this.#runGeneration(input, controller).then((value) => stream.close().then(() => value), (error) => {
|
|
1548
|
+
stream.fail(error);
|
|
1242
1549
|
throw error;
|
|
1243
|
-
}).finally(() => {
|
|
1244
|
-
if (this.#activeJob?.cancel === cancelRun) this.#activeJob = void 0;
|
|
1245
1550
|
});
|
|
1246
|
-
if (this.#activeJob?.cancel === cancelRun) this.#generationResult = result;
|
|
1247
1551
|
result.catch(() => {});
|
|
1248
|
-
return new GenerationRunTarget(
|
|
1249
|
-
|
|
1250
|
-
|
|
1552
|
+
return new GenerationRunTarget({
|
|
1553
|
+
stream: stream.stream,
|
|
1554
|
+
result,
|
|
1555
|
+
stop: () => {
|
|
1556
|
+
controller.abort(/* @__PURE__ */ new Error("Generation canceled"));
|
|
1557
|
+
stream.dispose();
|
|
1558
|
+
}
|
|
1251
1559
|
});
|
|
1252
1560
|
}
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
if (this.#isGenerating) return this.#refuse("Ignored generate: a generation is already in progress", "A generation is already in progress, please wait for it to finish");
|
|
1262
|
-
this.#isGenerating = true;
|
|
1263
|
-
this.#activeJob = {
|
|
1264
|
-
jobId: data.jobId,
|
|
1265
|
-
cancel: cancelRun
|
|
1561
|
+
async #runGeneration(input, controller) {
|
|
1562
|
+
if (this.#session.closed) throw new Error("Connection is closed");
|
|
1563
|
+
if (this.#session.active) return this.#refuse({ message: "A generation is already in progress, please wait for it to finish" });
|
|
1564
|
+
const settled = Promise.withResolvers();
|
|
1565
|
+
this.#session.active = {
|
|
1566
|
+
jobId: input.jobId,
|
|
1567
|
+
abort: () => controller.abort(/* @__PURE__ */ new Error("Generation canceled")),
|
|
1568
|
+
settled: settled.promise
|
|
1266
1569
|
};
|
|
1267
1570
|
let root = this.#options.root;
|
|
1268
1571
|
try {
|
|
1269
|
-
if (this.#
|
|
1572
|
+
if (this.#sandbox) root = await (0, node_fs_promises.mkdtemp)(node_path.default.join((0, node_os.tmpdir)(), "kubb-job-"));
|
|
1270
1573
|
return await this.#command({
|
|
1271
1574
|
command: "generate",
|
|
1272
1575
|
run: () => this.#generate({
|
|
1273
|
-
|
|
1576
|
+
input,
|
|
1274
1577
|
root,
|
|
1275
1578
|
signal: controller.signal
|
|
1276
1579
|
})
|
|
1277
1580
|
});
|
|
1278
1581
|
} finally {
|
|
1279
|
-
if (root !== this.#options.root) await
|
|
1280
|
-
|
|
1582
|
+
if (root !== this.#options.root) await Promise.all([(0, node_fs_promises.rm)(root, {
|
|
1583
|
+
recursive: true,
|
|
1584
|
+
force: true
|
|
1585
|
+
}), (0, node_fs_promises.rm)((0, _kubb_core.resolveCacheDir)(root), {
|
|
1586
|
+
recursive: true,
|
|
1587
|
+
force: true
|
|
1588
|
+
})]).catch(() => {});
|
|
1589
|
+
this.#session.active = void 0;
|
|
1590
|
+
settled.resolve();
|
|
1281
1591
|
}
|
|
1282
1592
|
}
|
|
1283
|
-
async #generate({
|
|
1284
|
-
const {
|
|
1285
|
-
const config = await loadConfig();
|
|
1286
|
-
const
|
|
1287
|
-
const
|
|
1288
|
-
const
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
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");
|
|
1292
|
-
const resolvedPlugins = plugins ?? config.plugins;
|
|
1293
|
-
const storage = this.#canWrite ? (0, _kubb_core.fsStorage)() : (0, _kubb_core.memoryStorage)();
|
|
1294
|
-
const diskFiles = this.#hasProjectOnDisk ? await listDisk({
|
|
1593
|
+
async #generate({ input, root, signal }) {
|
|
1594
|
+
const { jobId, config: patch } = input;
|
|
1595
|
+
const config = await this.#options.loadConfig();
|
|
1596
|
+
const plugins = await mergePlugins(config.plugins, patch?.plugins) ?? config.plugins;
|
|
1597
|
+
const adapter = await mergeAdapter(config.adapter, patch?.adapter);
|
|
1598
|
+
const studioInput = this.#sandbox ? patch?.input ?? "" : void 0;
|
|
1599
|
+
if (patch?.input && !this.#sandbox) await this.#warn({ message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox agent" });
|
|
1600
|
+
const diskFiles = !this.#sandbox && this.#permissions.allowRead ? await listDisk({
|
|
1295
1601
|
root,
|
|
1296
1602
|
outputPath: config.output.path,
|
|
1297
1603
|
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
1298
1604
|
}) : void 0;
|
|
1299
1605
|
const disk = diskFiles ? await this.#generations.keep({
|
|
1300
|
-
jobId
|
|
1606
|
+
jobId,
|
|
1301
1607
|
source: "disk",
|
|
1302
1608
|
files: diskFiles,
|
|
1303
1609
|
maxSetMb: this.#limits.maxSnapshotMb
|
|
1304
1610
|
}) : void 0;
|
|
1305
|
-
const
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
lint: false,
|
|
1314
|
-
postGenerate: []
|
|
1611
|
+
const files = await generate({
|
|
1612
|
+
config: {
|
|
1613
|
+
...config,
|
|
1614
|
+
root,
|
|
1615
|
+
input: studioInput ?? config.input,
|
|
1616
|
+
storage: this.#permissions.allowWrite ? (0, _kubb_core.fsStorage)() : (0, _kubb_core.memoryStorage)(),
|
|
1617
|
+
plugins,
|
|
1618
|
+
adapter
|
|
1315
1619
|
},
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
}).generate().catch(async (error) => {
|
|
1322
|
-
await this.#generations.drop(data.jobId);
|
|
1620
|
+
hooks: this.#options.hooks,
|
|
1621
|
+
signal,
|
|
1622
|
+
allowExec: this.#permissions.allowExec
|
|
1623
|
+
}).catch(async (error) => {
|
|
1624
|
+
await this.#generations.drop(jobId);
|
|
1323
1625
|
throw error;
|
|
1324
1626
|
});
|
|
1325
|
-
if (!result.success) {
|
|
1326
|
-
await this.#generations.drop(data.jobId);
|
|
1327
|
-
throw generationFailure(result.diagnostics);
|
|
1328
|
-
}
|
|
1329
|
-
const paths = new Set(result.files.map((file) => relativeStoragePath({
|
|
1330
|
-
root,
|
|
1331
|
-
filePath: file.path
|
|
1332
|
-
})));
|
|
1333
1627
|
const output = await this.#generations.keep({
|
|
1334
|
-
jobId
|
|
1628
|
+
jobId,
|
|
1335
1629
|
source: "output",
|
|
1336
|
-
files
|
|
1337
|
-
storage,
|
|
1338
|
-
root,
|
|
1339
|
-
paths
|
|
1340
|
-
},
|
|
1630
|
+
files,
|
|
1341
1631
|
maxSetMb: this.#limits.maxMb
|
|
1342
1632
|
});
|
|
1343
|
-
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");
|
|
1344
|
-
const { peerDependencies, missingDependencies } = await require_resolveConfig.resolvePeerDependencies(resolvedPlugins.map(({ name }) => name));
|
|
1633
|
+
if (!output.paths.length && Object.keys(output.hashes).length) await this.#warn({ message: "Kept only the hashes of this generation: its output is too large to keep" });
|
|
1345
1634
|
await this.#generations.add({
|
|
1346
|
-
jobId
|
|
1635
|
+
jobId,
|
|
1347
1636
|
output,
|
|
1348
|
-
disk
|
|
1349
|
-
peerDependencies,
|
|
1350
|
-
missingDependencies
|
|
1637
|
+
disk
|
|
1351
1638
|
});
|
|
1352
1639
|
return {
|
|
1353
1640
|
value: {
|
|
1354
1641
|
status: "success",
|
|
1355
|
-
files: [...paths],
|
|
1356
|
-
fileCount: paths.size,
|
|
1642
|
+
files: [...files.paths],
|
|
1643
|
+
fileCount: files.paths.size,
|
|
1357
1644
|
hashes: output.hashes,
|
|
1358
1645
|
disk: disk ? { hashes: disk.hashes } : void 0
|
|
1359
1646
|
},
|
|
1360
|
-
info: `${
|
|
1647
|
+
info: `${plugins.length} plugin${plugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "in memory"}${studioInput !== void 0 ? ", from a Studio spec" : ""}`
|
|
1361
1648
|
};
|
|
1362
1649
|
}
|
|
1363
|
-
saveConfig(
|
|
1650
|
+
saveConfig(input) {
|
|
1364
1651
|
return this.#command({
|
|
1365
1652
|
command: "saveConfig",
|
|
1366
|
-
run: () => this.#saveConfig(
|
|
1653
|
+
run: () => this.#saveConfig(input)
|
|
1367
1654
|
});
|
|
1368
1655
|
}
|
|
1369
|
-
async #saveConfig(
|
|
1656
|
+
async #saveConfig(input) {
|
|
1370
1657
|
const { configPath, configFile } = this.#options;
|
|
1371
|
-
if (!Array.isArray(
|
|
1372
|
-
await this.#warn("Ignored save: the message carried no edits");
|
|
1658
|
+
if (!Array.isArray(input.edits)) {
|
|
1659
|
+
await this.#warn({ message: "Ignored save: the message carried no edits" });
|
|
1373
1660
|
return { value: {
|
|
1374
1661
|
outcomes: [],
|
|
1375
1662
|
changed: false
|
|
1376
1663
|
} };
|
|
1377
1664
|
}
|
|
1378
|
-
const edits =
|
|
1665
|
+
const edits = input.edits;
|
|
1379
1666
|
const refuse = (reason) => ({
|
|
1380
1667
|
outcomes: edits.map((edit) => ({
|
|
1381
1668
|
edit,
|
|
@@ -1384,14 +1671,13 @@ var StudioSession = class {
|
|
|
1384
1671
|
})),
|
|
1385
1672
|
changed: false
|
|
1386
1673
|
});
|
|
1387
|
-
if (!this.#
|
|
1388
|
-
await this.#warn("Ignored save: editing kubb.config.ts was not granted");
|
|
1674
|
+
if (!this.#permissions.allowConfigEdit) {
|
|
1675
|
+
await this.#warn({ message: "Ignored save: editing kubb.config.ts was not granted" });
|
|
1389
1676
|
return { value: refuse("the agent was not granted permission to edit kubb.config.ts") };
|
|
1390
1677
|
}
|
|
1391
|
-
if (this.#
|
|
1678
|
+
if (this.#session.active) return { value: refuse("a generation is in progress") };
|
|
1392
1679
|
try {
|
|
1393
|
-
const {
|
|
1394
|
-
const { source: patched, outcomes, changed } = await writeConfigEdits({
|
|
1680
|
+
const { source, outcomes, changed } = await writeConfigEdits({
|
|
1395
1681
|
filePath: configFile,
|
|
1396
1682
|
edits
|
|
1397
1683
|
});
|
|
@@ -1400,89 +1686,40 @@ var StudioSession = class {
|
|
|
1400
1686
|
value: {
|
|
1401
1687
|
outcomes,
|
|
1402
1688
|
changed,
|
|
1403
|
-
file: changed ? await this.#readConfigFileView(
|
|
1689
|
+
file: changed ? await this.#readConfigFileView(source) : void 0
|
|
1404
1690
|
},
|
|
1405
1691
|
info: `applied ${applied}/${outcomes.length} edits to ${configPath}`
|
|
1406
1692
|
};
|
|
1407
1693
|
} catch (error) {
|
|
1408
|
-
await this.#hooks
|
|
1694
|
+
await emit(this.#options.hooks, "studio:error", { error: toError(error) });
|
|
1409
1695
|
return { value: refuse(getErrorMessage(error)) };
|
|
1410
1696
|
}
|
|
1411
1697
|
}
|
|
1412
|
-
|
|
1413
|
-
return this.#command({
|
|
1414
|
-
command: "snapshot",
|
|
1415
|
-
run: () => this.#publishSnapshot(data)
|
|
1416
|
-
});
|
|
1417
|
-
}
|
|
1418
|
-
async #publishSnapshot(data) {
|
|
1419
|
-
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");
|
|
1420
|
-
const { name, version, bundledDependencies, uploadPath } = data;
|
|
1421
|
-
if (!name || !version || !uploadPath) return this.#refuse("Ignored snapshot: the message was missing required fields", "The request was missing required fields");
|
|
1422
|
-
const generation = await this.#generations.latest();
|
|
1423
|
-
if (!generation) return this.#refuse("Ignored snapshot: no prior generation to pack", "No prior generation exists to pack, run a generation first");
|
|
1424
|
-
const bundled = new Set(bundledDependencies ?? []);
|
|
1425
|
-
const missing = generation.missingDependencies.filter((dependency) => !bundled.has(dependency));
|
|
1426
|
-
if (missing.length) return this.#refuse(`Ignored snapshot: missing dependencies: ${missing.join(", ")}`, `Missing dependencies: ${missing.join(", ")}`);
|
|
1427
|
-
try {
|
|
1428
|
-
const { createSnapshotPackage, uploadSnapshot } = await Promise.resolve().then(() => require("./snapshotPackage-BFjiRhGd.cjs"));
|
|
1429
|
-
const { files } = await this.#generations.read({
|
|
1430
|
-
generation,
|
|
1431
|
-
source: "output",
|
|
1432
|
-
paths: generation.output.paths
|
|
1433
|
-
});
|
|
1434
|
-
const { bytes, integrity } = await createSnapshotPackage(files, {
|
|
1435
|
-
name,
|
|
1436
|
-
version,
|
|
1437
|
-
peerDependencies: generation.peerDependencies
|
|
1438
|
-
});
|
|
1439
|
-
const { token, studioUrl, signal } = this.#options;
|
|
1440
|
-
await uploadSnapshot({
|
|
1441
|
-
bytes,
|
|
1442
|
-
uploadPath,
|
|
1443
|
-
studioUrl,
|
|
1444
|
-
token,
|
|
1445
|
-
shutdown: signal
|
|
1446
|
-
});
|
|
1447
|
-
return {
|
|
1448
|
-
value: {
|
|
1449
|
-
integrity,
|
|
1450
|
-
peerDependencies: generation.peerDependencies
|
|
1451
|
-
},
|
|
1452
|
-
info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
|
|
1453
|
-
};
|
|
1454
|
-
} catch (error) {
|
|
1455
|
-
await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
1456
|
-
throw error;
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
/** A sandbox has no project directory to snapshot. */
|
|
1460
|
-
get #hasProjectOnDisk() {
|
|
1461
|
-
return !this.#isSandbox && this.#canRead;
|
|
1462
|
-
}
|
|
1463
|
-
readFiles(data) {
|
|
1698
|
+
readFiles(input) {
|
|
1464
1699
|
return this.#command({
|
|
1465
1700
|
command: "readFiles",
|
|
1466
|
-
run: () => this.#readFiles(
|
|
1701
|
+
run: () => this.#readFiles(input)
|
|
1467
1702
|
});
|
|
1468
1703
|
}
|
|
1469
|
-
async #readFiles(
|
|
1470
|
-
if (!this.#
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
if (typeof
|
|
1475
|
-
const
|
|
1476
|
-
if (
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (!
|
|
1704
|
+
async #readFiles(input) {
|
|
1705
|
+
if (!this.#permissions.allowRead) return this.#refuse({
|
|
1706
|
+
message: "The agent was not granted permission to read generated files",
|
|
1707
|
+
permission: "allowRead"
|
|
1708
|
+
});
|
|
1709
|
+
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job and carry paths" });
|
|
1710
|
+
const { paths } = input;
|
|
1711
|
+
if (paths.length > 50) return this.#refuse({ message: `At most 50 paths may be requested at once` });
|
|
1712
|
+
const generation = await this.#generations.get(input.jobId);
|
|
1713
|
+
if (!generation) return this.#refuse({ message: require_protocol.GENERATION_GONE_MESSAGE });
|
|
1714
|
+
if (!isValidPage(input)) return this.#refuse({ message: `The cursor must be a whole line from 0 and come with a limit of 1 to ${require_protocol.MAX_PAGE_LINES} lines` });
|
|
1715
|
+
const source = input.source === "disk" ? "disk" : "output";
|
|
1716
|
+
if (!generation[source]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
|
|
1480
1717
|
const result = await this.#generations.read({
|
|
1481
1718
|
generation,
|
|
1482
1719
|
source,
|
|
1483
1720
|
paths,
|
|
1484
|
-
cursor:
|
|
1485
|
-
limit:
|
|
1721
|
+
cursor: input.cursor,
|
|
1722
|
+
limit: input.limit
|
|
1486
1723
|
});
|
|
1487
1724
|
return {
|
|
1488
1725
|
value: result,
|
|
@@ -1490,96 +1727,159 @@ var StudioSession = class {
|
|
|
1490
1727
|
};
|
|
1491
1728
|
}
|
|
1492
1729
|
};
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
const
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1730
|
+
const openWebSocketRpc = ({ url, token, instanceId, agent }) => {
|
|
1731
|
+
const socket = new WebSocket(url, { headers: {
|
|
1732
|
+
Authorization: `Bearer ${token}`,
|
|
1733
|
+
[require_protocol.AGENT_INSTANCE_HEADER]: instanceId
|
|
1734
|
+
} });
|
|
1735
|
+
const timer = setTimeout(() => socket.close(3008, "Connection timeout"), OPEN_TIMEOUT_MS);
|
|
1736
|
+
socket.addEventListener("open", () => clearTimeout(timer), { once: true });
|
|
1737
|
+
const closed = new Promise((resolve) => {
|
|
1738
|
+
socket.addEventListener("close", ({ code, reason }) => resolve({
|
|
1739
|
+
code,
|
|
1740
|
+
reason
|
|
1741
|
+
}), { once: true });
|
|
1742
|
+
socket.addEventListener("error", () => {
|
|
1743
|
+
if (socket.readyState !== WebSocket.OPEN) resolve({
|
|
1744
|
+
code: 1006,
|
|
1745
|
+
reason: ""
|
|
1746
|
+
});
|
|
1509
1747
|
});
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
await session.dispose();
|
|
1520
|
-
const end = await session.closed;
|
|
1521
|
-
if (!end.retry && end.reason !== "shutdown") return "stopped";
|
|
1522
|
-
if (error instanceof InvalidAgentTokenError) {
|
|
1523
|
-
const next = await onTokenRejected({
|
|
1524
|
-
error,
|
|
1525
|
-
credentials: current,
|
|
1526
|
-
live
|
|
1527
|
-
});
|
|
1528
|
-
if (!next) return "stopped";
|
|
1529
|
-
current = next;
|
|
1530
|
-
options = clientOptions(current);
|
|
1531
|
-
live = false;
|
|
1532
|
-
attempt = 0;
|
|
1533
|
-
continue;
|
|
1534
|
-
}
|
|
1535
|
-
} finally {
|
|
1536
|
-
await session.dispose();
|
|
1537
|
-
}
|
|
1538
|
-
onStarted?.();
|
|
1539
|
-
const cap = Math.min(1e3 * 2 ** Math.min(attempt++, 16), options.retryInterval ?? agentDefaults.retryIntervalMs);
|
|
1540
|
-
const delayMs = Math.random() * cap;
|
|
1541
|
-
await session.reportRetry(delayMs);
|
|
1542
|
-
try {
|
|
1543
|
-
await (0, node_timers_promises.setTimeout)(delayMs, void 0, { signal });
|
|
1544
|
-
} catch (error) {
|
|
1545
|
-
if (!signal?.aborted) throw error;
|
|
1748
|
+
}).finally(() => clearTimeout(timer));
|
|
1749
|
+
const studio = (0, capnweb.newWebSocketRpcSession)(socket, agent);
|
|
1750
|
+
studio.onRpcBroken(() => socket.close());
|
|
1751
|
+
return {
|
|
1752
|
+
studio,
|
|
1753
|
+
closed,
|
|
1754
|
+
close: () => {
|
|
1755
|
+
studio[Symbol.dispose]();
|
|
1756
|
+
socket.close();
|
|
1546
1757
|
}
|
|
1547
|
-
}
|
|
1548
|
-
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
const
|
|
1556
|
-
|
|
1557
|
-
|
|
1758
|
+
};
|
|
1759
|
+
};
|
|
1760
|
+
async function runSession({ options, openRpc, url, instanceId }) {
|
|
1761
|
+
const { hooks, signal, heartbeatInterval, token, studioUrl, version: version$2 } = options;
|
|
1762
|
+
const session = {
|
|
1763
|
+
connected: Promise.withResolvers(),
|
|
1764
|
+
closed: false
|
|
1765
|
+
};
|
|
1766
|
+
const rpc = openRpc({
|
|
1767
|
+
url,
|
|
1768
|
+
token,
|
|
1769
|
+
instanceId,
|
|
1770
|
+
agent: new Agent({
|
|
1771
|
+
options,
|
|
1772
|
+
session
|
|
1773
|
+
})
|
|
1774
|
+
});
|
|
1775
|
+
signal?.addEventListener("abort", rpc.close, { once: true });
|
|
1776
|
+
let timer;
|
|
1777
|
+
const heartbeat = async () => {
|
|
1778
|
+
let deadline;
|
|
1779
|
+
const timedOut = new Promise((resolve) => deadline = setTimeout(resolve, PING_TIMEOUT_MS, true));
|
|
1780
|
+
const failed = await Promise.race([Promise.resolve(rpc.studio.ping()).then(() => false, () => true), timedOut]);
|
|
1781
|
+
clearTimeout(deadline);
|
|
1782
|
+
if (failed) return rpc.close();
|
|
1783
|
+
if (!session.closed) timer = setTimeout(heartbeat, heartbeatInterval);
|
|
1784
|
+
};
|
|
1785
|
+
session.connected.promise.then(async (info) => {
|
|
1786
|
+
await emit(hooks, "studio:connected", {
|
|
1787
|
+
...info,
|
|
1788
|
+
url: studioUrl,
|
|
1789
|
+
versions: {
|
|
1790
|
+
studio: info.studioVersion,
|
|
1791
|
+
kubb: version,
|
|
1792
|
+
agent: version$2
|
|
1793
|
+
}
|
|
1794
|
+
});
|
|
1795
|
+
if (!session.closed) timer = setTimeout(heartbeat, heartbeatInterval);
|
|
1796
|
+
}, () => {});
|
|
1797
|
+
const close = await rpc.closed;
|
|
1798
|
+
session.closed = true;
|
|
1799
|
+
clearTimeout(timer);
|
|
1800
|
+
signal?.removeEventListener("abort", rpc.close);
|
|
1801
|
+
session.connected.reject(/* @__PURE__ */ new Error("The socket closed before Studio called connect()"));
|
|
1802
|
+
session.active?.abort();
|
|
1803
|
+
await session.active?.settled;
|
|
1558
1804
|
return {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1805
|
+
...close,
|
|
1806
|
+
connected: session.info !== void 0
|
|
1807
|
+
};
|
|
1808
|
+
}
|
|
1809
|
+
/**
|
|
1810
|
+
* The connection loop with a swappable transport. {@link connectAgent} passes a real WebSocket.
|
|
1811
|
+
*/
|
|
1812
|
+
async function runAgent({ options, openRpc }) {
|
|
1813
|
+
const root = options.root ?? node_process.default.cwd();
|
|
1814
|
+
const resolved = {
|
|
1815
|
+
...options,
|
|
1816
|
+
studioUrl: options.studioUrl ?? "https://kubb.studio",
|
|
1817
|
+
root,
|
|
1818
|
+
configFile: node_path.default.resolve(root, options.configPath),
|
|
1819
|
+
permissions: {
|
|
1820
|
+
allowWrite: false,
|
|
1821
|
+
allowConfigEdit: false,
|
|
1822
|
+
allowExec: false,
|
|
1823
|
+
allowRead: false,
|
|
1824
|
+
...options.permissions
|
|
1575
1825
|
},
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1826
|
+
hooks: options.hooks ?? new _kubb_core.Hookable(),
|
|
1827
|
+
maxRetryDelay: options.maxRetryDelay ?? MAX_RETRY_DELAY_MS,
|
|
1828
|
+
heartbeatInterval: Math.min(options.heartbeatInterval ?? HEARTBEAT_INTERVAL_MS, MAX_HEARTBEAT_INTERVAL_MS)
|
|
1579
1829
|
};
|
|
1830
|
+
const { studioUrl, hooks, signal, maxRetryDelay } = resolved;
|
|
1831
|
+
const url = secureUrl({
|
|
1832
|
+
studioUrl,
|
|
1833
|
+
path: require_protocol.AGENT_SOCKET_PATH,
|
|
1834
|
+
socket: true
|
|
1835
|
+
});
|
|
1836
|
+
const instanceId = (0, node_crypto.randomUUID)();
|
|
1837
|
+
let attempt = 0;
|
|
1838
|
+
while (!signal?.aborted) {
|
|
1839
|
+
const { code, reason, connected } = await runSession({
|
|
1840
|
+
options: resolved,
|
|
1841
|
+
openRpc,
|
|
1842
|
+
url,
|
|
1843
|
+
instanceId
|
|
1844
|
+
});
|
|
1845
|
+
if (signal?.aborted) return;
|
|
1846
|
+
await emit(hooks, "studio:disconnected", {
|
|
1847
|
+
code,
|
|
1848
|
+
reason
|
|
1849
|
+
});
|
|
1850
|
+
if (code === require_protocol.AgentCloseCode.UNAUTHORIZED) throw new InvalidAgentTokenError(studioUrl);
|
|
1851
|
+
if (code === require_protocol.AgentCloseCode.INCOMPATIBLE) throw new IncompatibleAgentError({
|
|
1852
|
+
studioUrl,
|
|
1853
|
+
detail: reason
|
|
1854
|
+
});
|
|
1855
|
+
if (code === require_protocol.AgentCloseCode.SUPERSEDED) return;
|
|
1856
|
+
if (connected) attempt = 0;
|
|
1857
|
+
const delayMs = Math.random() * Math.min(1e3 * 2 ** Math.min(attempt++, 16), maxRetryDelay);
|
|
1858
|
+
await emit(hooks, "studio:reconnecting", { delayMs });
|
|
1859
|
+
await (0, node_timers_promises.setTimeout)(delayMs, void 0, { signal }).catch(() => {});
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
/**
|
|
1863
|
+
* Connects this project to Kubb Studio and serves its commands until `signal` aborts. Reconnects
|
|
1864
|
+
* with jittered backoff, and resolves when another process of the agent takes over. Rejects with
|
|
1865
|
+
* {@link InvalidAgentTokenError} so the host can pair again, or {@link IncompatibleAgentError}.
|
|
1866
|
+
*
|
|
1867
|
+
* @example
|
|
1868
|
+
* ```ts
|
|
1869
|
+
* const hooks = new Hookable<KubbHooks>()
|
|
1870
|
+
* hooks.hook('studio:connected', ({ agentSlug }) => console.log(`Connected as ${agentSlug}`))
|
|
1871
|
+
*
|
|
1872
|
+
* await connectAgent({ token, configPath: 'kubb.config.ts', loadConfig, version: '1.0.0', hooks, signal })
|
|
1873
|
+
* ```
|
|
1874
|
+
*/
|
|
1875
|
+
function connectAgent(options) {
|
|
1876
|
+
return runAgent({
|
|
1877
|
+
options,
|
|
1878
|
+
openRpc: openWebSocketRpc
|
|
1879
|
+
});
|
|
1580
1880
|
}
|
|
1581
1881
|
//#endregion
|
|
1582
|
-
//#region src/
|
|
1882
|
+
//#region src/pair.ts
|
|
1583
1883
|
/** Labels, not secrets: a person approving the code in the browser is what authorizes a pairing. */
|
|
1584
1884
|
const CLIENT_IDS = {
|
|
1585
1885
|
cli: "kubb-cli",
|
|
@@ -1611,30 +1911,33 @@ var PairingDeniedError = class extends Error {
|
|
|
1611
1911
|
}
|
|
1612
1912
|
};
|
|
1613
1913
|
/**
|
|
1614
|
-
* Asks Studio for a pairing code
|
|
1615
|
-
* the code, so approval knows which machine it is pairing: the same machine pairing twice rotates
|
|
1616
|
-
* one agent's token instead of creating a second agent.
|
|
1914
|
+
* Asks Studio for a pairing code, sending the hashed machine id with it.
|
|
1617
1915
|
*/
|
|
1618
|
-
async function startPairing({ studioUrl =
|
|
1916
|
+
async function startPairing({ studioUrl = defaultStudioUrl, type, name, hostname, machineId, signal }) {
|
|
1619
1917
|
try {
|
|
1620
|
-
|
|
1918
|
+
const pairing = await (0, ofetch.ofetch)(secureUrl({
|
|
1919
|
+
studioUrl,
|
|
1920
|
+
path: "/api/auth/device/code"
|
|
1921
|
+
}).href, {
|
|
1621
1922
|
method: "POST",
|
|
1622
1923
|
body: {
|
|
1623
1924
|
client_id: type === "cli" ? CLIENT_IDS.cli : CLIENT_IDS.agent,
|
|
1624
1925
|
name,
|
|
1625
1926
|
hostname,
|
|
1626
|
-
machine_token:
|
|
1927
|
+
machine_token: (0, node_crypto.hash)("sha256", machineId),
|
|
1627
1928
|
agent_kind: type === "cli" ? void 0 : type
|
|
1628
1929
|
},
|
|
1629
1930
|
signal
|
|
1630
1931
|
});
|
|
1932
|
+
if (typeof pairing?.device_code !== "string") throw new Error("Kubb Studio returned no pairing code");
|
|
1933
|
+
return pairing;
|
|
1631
1934
|
} catch (error) {
|
|
1632
1935
|
if (signal?.aborted) throw new PairingCanceledError();
|
|
1633
1936
|
throw error;
|
|
1634
1937
|
}
|
|
1635
1938
|
}
|
|
1636
1939
|
function isPairingResult(response) {
|
|
1637
|
-
return
|
|
1940
|
+
return "token" in response && typeof response.token === "string";
|
|
1638
1941
|
}
|
|
1639
1942
|
/**
|
|
1640
1943
|
* Polls until the user approves or denies, honoring the server's `slow_down` back-off.
|
|
@@ -1646,7 +1949,7 @@ function isPairingResult(response) {
|
|
|
1646
1949
|
* @throws {PairingDeniedError} when the pairing is denied in the browser.
|
|
1647
1950
|
* @throws {PairingCanceledError} when `signal` aborts.
|
|
1648
1951
|
*/
|
|
1649
|
-
async function pollForPairingToken({ studioUrl =
|
|
1952
|
+
async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, signal, onRetry }) {
|
|
1650
1953
|
const deadline = Date.now() + (session.expires_in > 0 ? session.expires_in : 600) * 1e3;
|
|
1651
1954
|
let intervalMs = (session.interval > 0 ? session.interval : 5) * 1e3;
|
|
1652
1955
|
while (Date.now() < deadline) {
|
|
@@ -1658,19 +1961,24 @@ async function pollForPairingToken({ studioUrl = agentDefaults.studioUrl, sessio
|
|
|
1658
1961
|
}
|
|
1659
1962
|
let response;
|
|
1660
1963
|
try {
|
|
1661
|
-
|
|
1964
|
+
const { status, _data } = await ofetch.ofetch.raw(secureUrl({
|
|
1965
|
+
studioUrl,
|
|
1966
|
+
path: "/api/agent/token"
|
|
1967
|
+
}).href, {
|
|
1662
1968
|
method: "POST",
|
|
1663
1969
|
body: { device_code: session.device_code },
|
|
1664
|
-
|
|
1665
|
-
|
|
1970
|
+
signal,
|
|
1971
|
+
ignoreResponseError: true
|
|
1666
1972
|
});
|
|
1973
|
+
if (status >= 500 || !_data || typeof _data !== "object") throw new Error(`Kubb Studio answered with status ${status}`);
|
|
1974
|
+
response = _data;
|
|
1667
1975
|
} catch (error) {
|
|
1668
1976
|
if (signal?.aborted) throw new PairingCanceledError();
|
|
1669
1977
|
onRetry?.(toError(error));
|
|
1670
1978
|
continue;
|
|
1671
1979
|
}
|
|
1672
1980
|
if (isPairingResult(response)) return response;
|
|
1673
|
-
if (!
|
|
1981
|
+
if (!("error" in response) || typeof response.error !== "string") throw new Error("Kubb Studio returned an empty pairing response, pair again");
|
|
1674
1982
|
if (response.error === "authorization_pending") continue;
|
|
1675
1983
|
if (response.error === "slow_down") {
|
|
1676
1984
|
intervalMs += 5e3;
|
|
@@ -1702,26 +2010,220 @@ async function pairAgent({ onCode, onRetry, maxAttempts = 1, ...options }) {
|
|
|
1702
2010
|
}
|
|
1703
2011
|
}
|
|
1704
2012
|
//#endregion
|
|
1705
|
-
|
|
1706
|
-
|
|
2013
|
+
//#region src/snapshot.ts
|
|
2014
|
+
const gzipAsync = (0, node_util.promisify)(node_zlib.gzip);
|
|
2015
|
+
const UPLOAD_TIMEOUT_MS = 12e4;
|
|
2016
|
+
const BUNDLED_DEPENDENCIES = ["@kubb/plugin-barrel"];
|
|
2017
|
+
/**
|
|
2018
|
+
* Maps a generated file's path to its place inside the tarball, stripping everything before a
|
|
2019
|
+
* `src`/`dist` segment and any `..`/empty path segment so a crafted file name cannot escape the
|
|
2020
|
+
* `package/` root.
|
|
2021
|
+
*/
|
|
2022
|
+
function packagePath(filePath) {
|
|
2023
|
+
const normalized = filePath.replaceAll("\\", "/");
|
|
2024
|
+
return `package/${(normalized.match(/\/(?:src|dist)\/.*$/)?.[0].slice(1) ?? normalized.replace(/^\/+/, "")).split("/").filter((part) => part && part !== "." && part !== "..").join("/")}`;
|
|
2025
|
+
}
|
|
2026
|
+
/**
|
|
2027
|
+
* Splits a tarball entry path into the legacy 100-byte `name` field and, when the path does not
|
|
2028
|
+
* fit, the 155-byte USTAR `prefix` field that extends it. Throws rather than silently truncating
|
|
2029
|
+
* a path the format cannot address (max 256 bytes: 100 name + 1 separator + 155 prefix).
|
|
2030
|
+
*/
|
|
2031
|
+
function splitEntryPath(path) {
|
|
2032
|
+
if (Buffer.byteLength(path, "utf8") <= 100) return {
|
|
2033
|
+
name: path,
|
|
2034
|
+
prefix: ""
|
|
2035
|
+
};
|
|
2036
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
2037
|
+
if (path[i] !== "/") continue;
|
|
2038
|
+
const prefix = path.slice(0, i);
|
|
2039
|
+
const name = path.slice(i + 1);
|
|
2040
|
+
if (Buffer.byteLength(prefix, "utf8") <= 155 && Buffer.byteLength(name, "utf8") <= 100) return {
|
|
2041
|
+
name,
|
|
2042
|
+
prefix
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
throw new Error(`Snapshot path is too long for a tar entry: ${path}`);
|
|
2046
|
+
}
|
|
2047
|
+
function header(path, size) {
|
|
2048
|
+
const { name, prefix } = splitEntryPath(path);
|
|
2049
|
+
const value = Buffer.alloc(512);
|
|
2050
|
+
value.write(name, 0, "utf8");
|
|
2051
|
+
value.write("0000644\0", 100, "ascii");
|
|
2052
|
+
value.write("0000000\0", 108, "ascii");
|
|
2053
|
+
value.write("0000000\0", 116, "ascii");
|
|
2054
|
+
value.write(`${size.toString(8).padStart(11, "0")}\0`, 124, "ascii");
|
|
2055
|
+
value.write("03560116604\0", 136, "ascii");
|
|
2056
|
+
value.fill(32, 148, 156);
|
|
2057
|
+
value.write("0", 156, "ascii");
|
|
2058
|
+
value.write("ustar\0", 257, "ascii");
|
|
2059
|
+
value.write("00", 263, "ascii");
|
|
2060
|
+
value.write("0000000\0", 265, "ascii");
|
|
2061
|
+
value.write("0000000\0", 297, "ascii");
|
|
2062
|
+
value.write(prefix, 345, "utf8");
|
|
2063
|
+
const checksum = [...value].reduce((sum, byte) => sum + byte, 0);
|
|
2064
|
+
value.write(`${checksum.toString(8).padStart(6, "0")}\0 `, 148, "ascii");
|
|
2065
|
+
return value;
|
|
2066
|
+
}
|
|
2067
|
+
/**
|
|
2068
|
+
* Packs a generation's files into a gzipped, npm-installable tarball: a `package/` root with the
|
|
2069
|
+
* generated sources, a `tsdown`-built `dist/` (esm + cjs), and a `package.json` manifest.
|
|
2070
|
+
*/
|
|
2071
|
+
async function createSnapshotPackage({ files, packageInfo }) {
|
|
2072
|
+
const root = await (0, node_fs_promises.mkdtemp)((0, node_path.join)((0, node_os.tmpdir)(), "kubb-snapshot-"));
|
|
2073
|
+
const dist = (0, node_path.join)(root, "dist");
|
|
2074
|
+
const resolvedPaths = Object.entries(files).map(([name, content]) => ({
|
|
2075
|
+
name,
|
|
2076
|
+
content,
|
|
2077
|
+
target: packagePath(name)
|
|
2078
|
+
}));
|
|
2079
|
+
const targetOwners = /* @__PURE__ */ new Map();
|
|
2080
|
+
for (const { name, target } of resolvedPaths) {
|
|
2081
|
+
const owner = targetOwners.get(target);
|
|
2082
|
+
if (owner) throw new Error(`Snapshot has two generated files that sanitize to the same path "${target}": "${owner}" and "${name}"`);
|
|
2083
|
+
targetOwners.set(target, name);
|
|
2084
|
+
}
|
|
2085
|
+
try {
|
|
2086
|
+
await (0, node_fs_promises.mkdir)(dist);
|
|
2087
|
+
await Promise.all(resolvedPaths.map(async ({ content, target }) => {
|
|
2088
|
+
const path = (0, node_path.join)(root, target.slice(8));
|
|
2089
|
+
await (0, node_fs_promises.mkdir)((0, node_path.join)(path, ".."), { recursive: true });
|
|
2090
|
+
await (0, node_fs_promises.writeFile)(path, content);
|
|
2091
|
+
}));
|
|
2092
|
+
const sourceEntries = resolvedPaths.filter(({ name }) => /\.(?:[cm]?[jt]sx?)$/.test(name)).map(({ target }) => (0, node_path.join)(root, target.slice(8)));
|
|
2093
|
+
if (sourceEntries.length) {
|
|
2094
|
+
const { build } = await import("tsdown");
|
|
2095
|
+
await build({
|
|
2096
|
+
entry: sourceEntries,
|
|
2097
|
+
outDir: dist,
|
|
2098
|
+
format: ["esm", "cjs"],
|
|
2099
|
+
dts: false,
|
|
2100
|
+
sourcemap: false,
|
|
2101
|
+
unbundle: true,
|
|
2102
|
+
report: false,
|
|
2103
|
+
logLevel: "silent",
|
|
2104
|
+
fixedExtension: true
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
const builtEntries = await Promise.all((await (0, _kubb_core.fsStorage)().readKeys(dist)).map(async (key) => [`package/dist/${key}`, await (0, node_fs_promises.readFile)((0, node_path.join)(dist, key), "utf8")]));
|
|
2108
|
+
const builtPaths = new Set(builtEntries.map(([path]) => path));
|
|
2109
|
+
const hasBarrel = builtPaths.has("package/dist/index.mjs") && builtPaths.has("package/dist/index.cjs");
|
|
2110
|
+
const barrelFields = hasBarrel ? {
|
|
2111
|
+
main: "./dist/index.cjs",
|
|
2112
|
+
module: "./dist/index.mjs"
|
|
2113
|
+
} : {};
|
|
2114
|
+
const barrelExport = hasBarrel ? { ".": {
|
|
2115
|
+
import: "./dist/index.mjs",
|
|
2116
|
+
require: "./dist/index.cjs"
|
|
2117
|
+
} } : {};
|
|
2118
|
+
const entries = {
|
|
2119
|
+
"package/package.json": JSON.stringify({
|
|
2120
|
+
...packageInfo,
|
|
2121
|
+
type: "module",
|
|
2122
|
+
...barrelFields,
|
|
2123
|
+
exports: {
|
|
2124
|
+
...barrelExport,
|
|
2125
|
+
"./*": {
|
|
2126
|
+
import: "./dist/*.mjs",
|
|
2127
|
+
require: "./dist/*.cjs"
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}, null, 2),
|
|
2131
|
+
...Object.fromEntries(resolvedPaths.map(({ content, target }) => [target, content])),
|
|
2132
|
+
...Object.fromEntries(builtEntries)
|
|
2133
|
+
};
|
|
2134
|
+
const chunks = [];
|
|
2135
|
+
for (const [name, content] of Object.entries(entries)) {
|
|
2136
|
+
const bytes = Buffer.from(content);
|
|
2137
|
+
chunks.push(header(name, bytes.length), bytes, Buffer.alloc((512 - bytes.length % 512) % 512));
|
|
2138
|
+
}
|
|
2139
|
+
return await gzipAsync(Buffer.concat([...chunks, Buffer.alloc(1024)]));
|
|
2140
|
+
} finally {
|
|
2141
|
+
await (0, node_fs_promises.rm)(root, {
|
|
2142
|
+
recursive: true,
|
|
2143
|
+
force: true
|
|
2144
|
+
});
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
function failureMessage({ status, _data }) {
|
|
2148
|
+
const upgrade = _data?.data?.upgradeUrl ? ` Upgrade at ${_data.data.upgradeUrl}.` : "";
|
|
2149
|
+
return `Kubb Studio refused the snapshot (status ${status})${_data?.message ? `: ${_data.message}` : ""}.${upgrade}`;
|
|
2150
|
+
}
|
|
2151
|
+
/**
|
|
2152
|
+
* Generates the project, packs it as an npm tarball, and publishes it to Studio in one
|
|
2153
|
+
* `POST /api/snapshots` request.
|
|
2154
|
+
*
|
|
2155
|
+
* @example
|
|
2156
|
+
* ```ts
|
|
2157
|
+
* const snapshot = await createSnapshot({
|
|
2158
|
+
* apiKey: process.env.KUBB_TOKEN!,
|
|
2159
|
+
* config,
|
|
2160
|
+
* id: 'gh:123:pr-4',
|
|
2161
|
+
* name: 'acme/api#4',
|
|
2162
|
+
* packageName: '@acme/api',
|
|
2163
|
+
* version: '1.0.0',
|
|
2164
|
+
* })
|
|
2165
|
+
* console.log(snapshot.url, snapshot.changes)
|
|
2166
|
+
* ```
|
|
2167
|
+
*/
|
|
2168
|
+
async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, hooks, signal, ...metadata }) {
|
|
2169
|
+
const url = secureUrl({
|
|
2170
|
+
studioUrl,
|
|
2171
|
+
path: "/api/snapshots"
|
|
2172
|
+
});
|
|
2173
|
+
const generated = await generate({
|
|
2174
|
+
config,
|
|
2175
|
+
hooks,
|
|
2176
|
+
signal,
|
|
2177
|
+
allowExec: true
|
|
2178
|
+
});
|
|
2179
|
+
const files = Object.fromEntries(await Promise.all([...generated.paths].map(async (path) => {
|
|
2180
|
+
const content = await generated.storage.readItem((0, node_path.resolve)(generated.root, path));
|
|
2181
|
+
if (content === null) throw new Error(`The generation listed ${path} but did not keep its content`);
|
|
2182
|
+
return [path, content];
|
|
2183
|
+
})));
|
|
2184
|
+
const { peerDependencies, missingDependencies } = await resolvePeerDependencies(config.plugins.map(({ name }) => name));
|
|
2185
|
+
const missing = missingDependencies.filter((dependency) => !BUNDLED_DEPENDENCIES.includes(dependency));
|
|
2186
|
+
if (missing.length) throw new Error(`Missing dependencies: ${missing.join(", ")}`);
|
|
2187
|
+
const bytes = await createSnapshotPackage({
|
|
2188
|
+
files,
|
|
2189
|
+
packageInfo: {
|
|
2190
|
+
name: metadata.packageName,
|
|
2191
|
+
version: metadata.version,
|
|
2192
|
+
peerDependencies
|
|
2193
|
+
}
|
|
2194
|
+
});
|
|
2195
|
+
if (bytes.length > 52428800) throw new Error(`The snapshot is ${Math.ceil(bytes.length / 1024 / 1024)} MB, over the ${require_protocol.MAX_SNAPSHOT_BYTES / 1024 / 1024} MB limit`);
|
|
2196
|
+
const hashes = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, hashOf(content)]));
|
|
2197
|
+
const body = new FormData();
|
|
2198
|
+
body.set("metadata", JSON.stringify({
|
|
2199
|
+
...metadata,
|
|
2200
|
+
hashes,
|
|
2201
|
+
peerDependencies
|
|
2202
|
+
}));
|
|
2203
|
+
body.set("tarball", new Blob([new Uint8Array(bytes)], { type: "application/gzip" }), "snapshot.tgz");
|
|
2204
|
+
const timeout = AbortSignal.timeout(UPLOAD_TIMEOUT_MS);
|
|
2205
|
+
const response = await ofetch.ofetch.raw(url.href, {
|
|
2206
|
+
method: "POST",
|
|
2207
|
+
headers: { "x-api-key": apiKey },
|
|
2208
|
+
body,
|
|
2209
|
+
redirect: "error",
|
|
2210
|
+
signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
|
|
2211
|
+
ignoreResponseError: true
|
|
2212
|
+
});
|
|
2213
|
+
if (!response.ok) throw new Error(failureMessage(response));
|
|
2214
|
+
const snapshot = response._data?.snapshot;
|
|
2215
|
+
if (!snapshot) throw new Error(`Kubb Studio returned no snapshot (status ${response.status})`);
|
|
2216
|
+
return snapshot;
|
|
2217
|
+
}
|
|
2218
|
+
//#endregion
|
|
1707
2219
|
exports.IncompatibleAgentError = IncompatibleAgentError;
|
|
1708
2220
|
exports.InvalidAgentTokenError = InvalidAgentTokenError;
|
|
1709
2221
|
exports.PairingCanceledError = PairingCanceledError;
|
|
1710
2222
|
exports.PairingDeniedError = PairingDeniedError;
|
|
1711
2223
|
exports.PairingExpiredError = PairingExpiredError;
|
|
1712
|
-
exports.
|
|
1713
|
-
exports.
|
|
1714
|
-
exports.createClient = createClient;
|
|
1715
|
-
exports.createFileStorage = createFileStorage;
|
|
1716
|
-
exports.createJob = createJob;
|
|
2224
|
+
exports.connectAgent = connectAgent;
|
|
2225
|
+
exports.createSnapshot = createSnapshot;
|
|
1717
2226
|
exports.defaultStudioUrl = defaultStudioUrl;
|
|
1718
|
-
exports.generationEventTypes = require_protocol.generationEventTypes;
|
|
1719
|
-
exports.machineTokenFrom = machineTokenFrom;
|
|
1720
2227
|
exports.pairAgent = pairAgent;
|
|
1721
|
-
exports.pollForPairingToken = pollForPairingToken;
|
|
1722
|
-
exports.runConnection = runConnection;
|
|
1723
|
-
exports.setStorage = setStorage;
|
|
1724
|
-
exports.startPairing = startPairing;
|
|
1725
|
-
exports.waitForJob = waitForJob;
|
|
1726
2228
|
|
|
1727
2229
|
//# sourceMappingURL=index.cjs.map
|