@kubb/studio 5.5.6 → 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 +1599 -1476
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +119 -421
- package/dist/index.js +1575 -1459
- 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 -15
- package/dist/configFile-BTVeov27.cjs +0 -585
- package/dist/configFile-BTVeov27.cjs.map +0 -1
- package/dist/configFile-BymXUi9P.js +0 -584
- package/dist/configFile-BymXUi9P.js.map +0 -1
- package/dist/resolveConfig-D7I3Nj60.cjs +0 -360
- package/dist/resolveConfig-D7I3Nj60.cjs.map +0 -1
- package/dist/resolveConfig-DMIG_MH-.js +0 -325
- package/dist/resolveConfig-DMIG_MH-.js.map +0 -1
- package/dist/rolldown-runtime-qbf5tadS.cjs +0 -38
- package/dist/snapshotPackage-BwM3LFmZ.cjs +0 -176
- package/dist/snapshotPackage-BwM3LFmZ.cjs.map +0 -1
- package/dist/snapshotPackage-I8bt8KTR.js +0 -175
- package/dist/snapshotPackage-I8bt8KTR.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,27 +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
25
|
let node_crypto = require("node:crypto");
|
|
6
|
-
let node_timers_promises = require("node:timers/promises");
|
|
7
|
-
let node_process = require("node:process");
|
|
8
|
-
node_process = require_rolldown_runtime.__toESM(node_process, 1);
|
|
9
|
-
let node_child_process = require("node:child_process");
|
|
10
26
|
let node_fs_promises = require("node:fs/promises");
|
|
11
|
-
let node_path = require("node:path");
|
|
12
|
-
node_path = require_rolldown_runtime.__toESM(node_path, 1);
|
|
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
|
-
let node_module = require("node:module");
|
|
21
36
|
let capnweb = require("capnweb");
|
|
22
|
-
let
|
|
23
|
-
|
|
24
|
-
let
|
|
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
|
|
25
69
|
//#region ../../internals/utils/src/errors.ts
|
|
26
70
|
/**
|
|
27
71
|
* Coerces an unknown thrown value to an `Error` instance.
|
|
@@ -50,45 +94,92 @@ function getErrorMessage(value) {
|
|
|
50
94
|
return value instanceof Error ? value.message : String(value);
|
|
51
95
|
}
|
|
52
96
|
//#endregion
|
|
53
|
-
//#region ../../internals/utils/src/
|
|
97
|
+
//#region ../../internals/utils/src/runtime.ts
|
|
54
98
|
/**
|
|
55
|
-
*
|
|
99
|
+
* Detects the JavaScript runtime executing the current process and exposes its name and version.
|
|
56
100
|
*
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* ```
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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.
|
|
76
171
|
*
|
|
77
|
-
* @example
|
|
172
|
+
* @example
|
|
78
173
|
* ```ts
|
|
79
|
-
* const
|
|
80
|
-
* const fn = memoize(outer, (params) => memoize(new Map(), (key) => transform(params, key)))
|
|
81
|
-
* fn(params)('camelcase')
|
|
174
|
+
* const source = await read('./src/Pet.ts')
|
|
82
175
|
* ```
|
|
83
176
|
*/
|
|
84
|
-
function
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const value = factory(key);
|
|
88
|
-
store.set(key, value);
|
|
89
|
-
return value;
|
|
90
|
-
};
|
|
177
|
+
async function read(path) {
|
|
178
|
+
if (runtime.isBun) return Bun.file(path).text();
|
|
179
|
+
return (0, node_fs_promises.readFile)(path, { encoding: "utf8" });
|
|
91
180
|
}
|
|
181
|
+
//#endregion
|
|
182
|
+
//#region ../../internals/utils/src/promise.ts
|
|
92
183
|
/**
|
|
93
184
|
* Runs `run` over every item with at most `limit` in flight. Workers share one iterator, so each
|
|
94
185
|
* takes the next item the moment it frees up instead of waiting for a batch to drain.
|
|
@@ -106,458 +197,832 @@ async function inParallel({ items, limit, run }) {
|
|
|
106
197
|
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
107
198
|
}
|
|
108
199
|
//#endregion
|
|
109
|
-
//#region ../../internals/utils/src/
|
|
110
|
-
/**
|
|
111
|
-
* CLI command descriptors for each supported code formatter.
|
|
112
|
-
*/
|
|
113
|
-
const formatters = {
|
|
114
|
-
prettier: {
|
|
115
|
-
command: "prettier",
|
|
116
|
-
args: (outputPath) => [
|
|
117
|
-
"--ignore-unknown",
|
|
118
|
-
"--write",
|
|
119
|
-
outputPath
|
|
120
|
-
],
|
|
121
|
-
errorMessage: "Prettier not found"
|
|
122
|
-
},
|
|
123
|
-
biome: {
|
|
124
|
-
command: "biome",
|
|
125
|
-
args: (outputPath) => [
|
|
126
|
-
"format",
|
|
127
|
-
"--write",
|
|
128
|
-
outputPath
|
|
129
|
-
],
|
|
130
|
-
errorMessage: "Biome not found"
|
|
131
|
-
},
|
|
132
|
-
oxfmt: {
|
|
133
|
-
command: "oxfmt",
|
|
134
|
-
args: (outputPath) => [outputPath],
|
|
135
|
-
errorMessage: "Oxfmt not found"
|
|
136
|
-
}
|
|
137
|
-
};
|
|
200
|
+
//#region ../../internals/utils/src/time.ts
|
|
138
201
|
/**
|
|
139
|
-
*
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
202
|
+
* Calculates elapsed time in milliseconds from a high-resolution `process.hrtime` start time.
|
|
203
|
+
* Rounds to 2 decimal places for sub-millisecond precision without noise.
|
|
204
|
+
*
|
|
205
|
+
* @example
|
|
206
|
+
* ```ts
|
|
207
|
+
* const start = process.hrtime()
|
|
208
|
+
* doWork()
|
|
209
|
+
* getElapsedMs(start) // 42.35
|
|
210
|
+
* ```
|
|
211
|
+
*/
|
|
212
|
+
function getElapsedMs(hrStart) {
|
|
213
|
+
const [seconds, nanoseconds] = process.hrtime(hrStart);
|
|
214
|
+
const ms = seconds * 1e3 + nanoseconds / 1e6;
|
|
215
|
+
return Math.round(ms * 100) / 100;
|
|
216
|
+
}
|
|
217
|
+
//#endregion
|
|
218
|
+
//#region package.json
|
|
219
|
+
var version = "5.6.1";
|
|
220
|
+
//#endregion
|
|
221
|
+
//#region src/resolveConfig.ts
|
|
222
|
+
/**
|
|
223
|
+
* Imports a package, falling back to how the user's project would resolve it.
|
|
224
|
+
*
|
|
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.
|
|
228
|
+
*/
|
|
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);
|
|
164
236
|
}
|
|
165
|
-
}
|
|
237
|
+
}
|
|
166
238
|
/**
|
|
167
|
-
*
|
|
168
|
-
*
|
|
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
|
+
* ```
|
|
169
246
|
*/
|
|
170
|
-
|
|
171
|
-
"
|
|
172
|
-
|
|
173
|
-
"prettier"
|
|
174
|
-
];
|
|
247
|
+
function toPluginName(packageName) {
|
|
248
|
+
return packageName.split("/").pop() ?? packageName;
|
|
249
|
+
}
|
|
175
250
|
/**
|
|
176
|
-
*
|
|
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
|
+
* ```
|
|
177
260
|
*/
|
|
178
|
-
|
|
179
|
-
"
|
|
180
|
-
|
|
181
|
-
"eslint"
|
|
182
|
-
];
|
|
261
|
+
function toPackageName(name) {
|
|
262
|
+
return name.startsWith("plugin-") ? `@kubb/${name}` : name;
|
|
263
|
+
}
|
|
183
264
|
/**
|
|
184
|
-
*
|
|
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
|
+
* ```
|
|
185
272
|
*/
|
|
186
|
-
function
|
|
187
|
-
return
|
|
188
|
-
const child = (0, node_child_process.spawn)(name, ["--version"], { stdio: "ignore" });
|
|
189
|
-
child.on("close", (code) => resolve(code === 0));
|
|
190
|
-
child.on("error", () => resolve(false));
|
|
191
|
-
});
|
|
273
|
+
function toExportName(packageName) {
|
|
274
|
+
return camelCase(toPluginName(packageName));
|
|
192
275
|
}
|
|
193
276
|
/**
|
|
194
|
-
*
|
|
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.
|
|
280
|
+
*/
|
|
281
|
+
const KUBB_PLUGIN_SPECIFIER = /^@kubb\/plugin-[\w.-]+$/;
|
|
282
|
+
/**
|
|
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.
|
|
285
|
+
*/
|
|
286
|
+
function isKubbPluginSpecifier(name) {
|
|
287
|
+
return KUBB_PLUGIN_SPECIFIER.test(name);
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
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.
|
|
195
294
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
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.
|
|
198
299
|
*/
|
|
199
|
-
async function
|
|
200
|
-
|
|
201
|
-
|
|
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".`);
|
|
202
312
|
}
|
|
203
|
-
require_rolldown_runtime.__name(detectTool$1, "detectTool");
|
|
204
313
|
/**
|
|
205
|
-
*
|
|
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.
|
|
206
319
|
*
|
|
207
320
|
* @example
|
|
208
321
|
* ```ts
|
|
209
|
-
*
|
|
210
|
-
*
|
|
322
|
+
* { name: '@kubb/plugin-react-query', options: { output: { path: './hooks' } } }
|
|
323
|
+
* { name: '@kubb/plugin-ts', options: { output: { path: './types' } } }
|
|
211
324
|
* ```
|
|
212
325
|
*/
|
|
213
|
-
function
|
|
214
|
-
return (
|
|
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);
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
peerDependencies,
|
|
355
|
+
missingDependencies
|
|
356
|
+
};
|
|
215
357
|
}
|
|
216
|
-
//#endregion
|
|
217
|
-
//#region ../../internals/utils/src/time.ts
|
|
218
358
|
/**
|
|
219
|
-
*
|
|
220
|
-
|
|
359
|
+
* Whether `value` is an object literal or a null-prototype object, not an array or class instance.
|
|
360
|
+
*/
|
|
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`).
|
|
221
383
|
*
|
|
222
384
|
* @example
|
|
223
385
|
* ```ts
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* getElapsedMs(start) // 42.35
|
|
386
|
+
* mergeOptions({ macros: [camelCaseMacro], syntaxType: 'type' }, { macros: [{ name: 'camelCase' }], syntaxType: 'interface' })
|
|
387
|
+
* // { macros: [camelCaseMacro], syntaxType: 'interface' }
|
|
227
388
|
* ```
|
|
228
389
|
*/
|
|
229
|
-
function
|
|
230
|
-
const
|
|
231
|
-
const
|
|
232
|
-
|
|
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;
|
|
400
|
+
}
|
|
401
|
+
return merged;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
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.
|
|
412
|
+
*/
|
|
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)];
|
|
434
|
+
}
|
|
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));
|
|
233
450
|
}
|
|
234
451
|
//#endregion
|
|
235
|
-
//#region src/
|
|
452
|
+
//#region src/configFile.ts
|
|
236
453
|
/**
|
|
237
|
-
*
|
|
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$]*$/;
|
|
458
|
+
/**
|
|
459
|
+
* `key: value` as an object literal property, in the file's quote and key style.
|
|
238
460
|
*
|
|
239
|
-
*
|
|
240
|
-
*
|
|
241
|
-
|
|
242
|
-
|
|
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.
|
|
463
|
+
*/
|
|
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
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
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.
|
|
243
478
|
*/
|
|
244
|
-
|
|
245
|
-
let hasInstalledStorage = false;
|
|
479
|
+
const DISABLED_MARKER = "kubb:disabled";
|
|
246
480
|
/**
|
|
247
|
-
*
|
|
481
|
+
* The plugin and comment-block length a marker line names, when `line` is one.
|
|
248
482
|
*/
|
|
249
|
-
function
|
|
250
|
-
|
|
251
|
-
|
|
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;
|
|
252
491
|
}
|
|
253
492
|
/**
|
|
254
|
-
*
|
|
255
|
-
*
|
|
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.
|
|
256
495
|
*/
|
|
257
|
-
function
|
|
258
|
-
|
|
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);
|
|
259
502
|
}
|
|
260
|
-
let fallbackSecretPromise = null;
|
|
261
503
|
/**
|
|
262
|
-
*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
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.
|
|
266
510
|
*/
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
if (
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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] };
|
|
276
528
|
}
|
|
277
529
|
/**
|
|
278
|
-
*
|
|
279
|
-
* own identity without duplicating `getMachineToken`'s SHA-256 step.
|
|
530
|
+
* The config entry an edit names, defaulting to the first when it names none.
|
|
280
531
|
*/
|
|
281
|
-
function
|
|
282
|
-
|
|
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;
|
|
283
540
|
}
|
|
284
541
|
/**
|
|
285
|
-
*
|
|
286
|
-
*
|
|
287
|
-
* The token is hashed with SHA-256.
|
|
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.
|
|
288
544
|
*/
|
|
289
|
-
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
return machineTokenFrom(await fallbackSecretPromise);
|
|
545
|
+
function propertyKey(entry) {
|
|
546
|
+
if (entry.key.type === "Identifier") return entry.key.name;
|
|
547
|
+
if (entry.key.type === "StringLiteral") return entry.key.value;
|
|
293
548
|
}
|
|
294
|
-
//#endregion
|
|
295
|
-
//#region src/operations/api.ts
|
|
296
549
|
/**
|
|
297
|
-
*
|
|
298
|
-
* message stops at the status line, so the detail Studio sends with a failure (an agent limit, a
|
|
299
|
-
* revoked token) would otherwise never reach the user.
|
|
550
|
+
* The index of an object literal's own property named `key`, `-1` when it has none.
|
|
300
551
|
*/
|
|
301
|
-
function
|
|
302
|
-
|
|
303
|
-
const body = data;
|
|
304
|
-
for (const value of [
|
|
305
|
-
body.error_description,
|
|
306
|
-
body.message,
|
|
307
|
-
body.error
|
|
308
|
-
]) if (typeof value === "string" && value) return value;
|
|
552
|
+
function entryIndex({ node, key }) {
|
|
553
|
+
return node.properties.findIndex((entry) => entry.type === "ObjectProperty" && propertyKey(entry) === key);
|
|
309
554
|
}
|
|
310
555
|
/**
|
|
311
|
-
*
|
|
312
|
-
* revoked, or the agent it belonged to was deleted in the Studio UI. Hosts catch this to forget
|
|
313
|
-
* the stored credential and pair again.
|
|
556
|
+
* The value node of an object literal's own property.
|
|
314
557
|
*/
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
558
|
+
function property(node, key) {
|
|
559
|
+
const index = entryIndex({
|
|
560
|
+
node,
|
|
561
|
+
key
|
|
562
|
+
});
|
|
563
|
+
return index === -1 ? void 0 : node.properties[index].value;
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Writes `key: value` on an object literal, replacing the value when the property is already there.
|
|
567
|
+
*
|
|
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;
|
|
319
583
|
}
|
|
320
|
-
|
|
584
|
+
node.properties[index].value = entry.value;
|
|
585
|
+
}
|
|
321
586
|
/**
|
|
322
|
-
*
|
|
323
|
-
* agent is upgraded, so hosts stop instead of reconnecting.
|
|
587
|
+
* Drops `key` from an object literal.
|
|
324
588
|
*/
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
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;
|
|
329
612
|
}
|
|
330
|
-
|
|
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
|
+
}
|
|
331
629
|
/**
|
|
332
|
-
*
|
|
333
|
-
* throw its own error shape with the same field.
|
|
630
|
+
* Maps a factory identifier in the file back to the module it was imported from.
|
|
334
631
|
*/
|
|
335
|
-
function
|
|
336
|
-
return
|
|
632
|
+
function importedFrom(mod) {
|
|
633
|
+
return new Map(mod.imports.$items.map((item) => [item.local, item.from]));
|
|
337
634
|
}
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
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
|
+
});
|
|
341
651
|
}
|
|
342
652
|
/**
|
|
343
|
-
*
|
|
344
|
-
* identity to the token, reports what the process can take on, and gets back the URL of the one
|
|
345
|
-
* socket it keeps open.
|
|
653
|
+
* Plugins a previous `disable-plugin` commented out of this config, keyed by package name.
|
|
346
654
|
*
|
|
347
|
-
*
|
|
348
|
-
* {@link InvalidAgentTokenError} and an unsupported agent version (426) throws
|
|
349
|
-
* {@link IncompatibleAgentError}, since retrying either cannot help.
|
|
655
|
+
* Read from the marker lines rather than the AST, since a commented-out call is no longer a node.
|
|
350
656
|
*/
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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.
|
|
668
|
+
*
|
|
669
|
+
* @example
|
|
670
|
+
* ```ts
|
|
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
|
+
* }
|
|
675
|
+
* ```
|
|
676
|
+
*/
|
|
677
|
+
function readConfig(source) {
|
|
678
|
+
const parsed = parseConfigs(source);
|
|
679
|
+
if ("reason" in parsed) return {
|
|
680
|
+
managed: false,
|
|
681
|
+
reason: parsed.reason
|
|
356
682
|
};
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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
|
+
};
|
|
707
|
+
});
|
|
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
|
+
};
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* The options object of a plugin call, when it was called with one.
|
|
725
|
+
*/
|
|
726
|
+
function getOptions(call) {
|
|
727
|
+
const options = call.arguments[0];
|
|
728
|
+
return options?.type === "ObjectExpression" ? options : void 0;
|
|
729
|
+
}
|
|
730
|
+
/**
|
|
731
|
+
* The options object of a plugin call, creating an empty one when the plugin was called bare.
|
|
732
|
+
*/
|
|
733
|
+
function ensureOptions(call) {
|
|
734
|
+
if (call.arguments.length === 0) call.arguments.push({
|
|
735
|
+
type: "ObjectExpression",
|
|
736
|
+
properties: []
|
|
737
|
+
});
|
|
738
|
+
return getOptions(call);
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
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: {}
|
|
365
754
|
});
|
|
366
|
-
|
|
367
|
-
if (
|
|
368
|
-
|
|
369
|
-
throw registrationError(error);
|
|
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;
|
|
370
758
|
}
|
|
759
|
+
return { reason: "no option path given" };
|
|
371
760
|
}
|
|
372
761
|
/**
|
|
373
|
-
*
|
|
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.
|
|
374
765
|
*/
|
|
375
|
-
|
|
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
|
+
}
|
|
376
780
|
/**
|
|
377
|
-
*
|
|
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.
|
|
378
784
|
*/
|
|
379
|
-
|
|
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
|
+
}
|
|
380
798
|
/**
|
|
381
|
-
*
|
|
382
|
-
*
|
|
383
|
-
* (503, an agent process that is mid-reconnect). Anything else (404 agent not found, 401/403 auth)
|
|
384
|
-
* is thrown straight away, since retrying cannot change the outcome.
|
|
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.
|
|
385
801
|
*/
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
}
|
|
391
820
|
/**
|
|
392
|
-
*
|
|
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.
|
|
393
825
|
*/
|
|
394
|
-
function
|
|
395
|
-
const
|
|
396
|
-
|
|
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") };
|
|
397
841
|
}
|
|
398
842
|
/**
|
|
399
|
-
*
|
|
400
|
-
* lockstep.
|
|
843
|
+
* Uncomments the block a previous `disable-plugin` left behind for `plugin`.
|
|
401
844
|
*/
|
|
402
|
-
function
|
|
403
|
-
|
|
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") };
|
|
854
|
+
}
|
|
855
|
+
return { reason: `${plugin} is not disabled` };
|
|
404
856
|
}
|
|
405
857
|
/**
|
|
406
|
-
*
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
858
|
+
* Parses `source` and finds the config entries it exports.
|
|
859
|
+
*/
|
|
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
|
+
}
|
|
873
|
+
/**
|
|
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.
|
|
877
|
+
*/
|
|
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
|
+
};
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
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.
|
|
410
891
|
*
|
|
411
|
-
*
|
|
412
|
-
* exponential backoff and jitter, honoring Studio's `Retry-After` header when it sends one, up to
|
|
413
|
-
* `timeoutMs`. Every other failure, including a missing agent (404), throws immediately.
|
|
892
|
+
* Edits are independent: one that cannot be applied is reported in `outcomes` and the rest still run.
|
|
414
893
|
*
|
|
415
|
-
* @example
|
|
894
|
+
* @example
|
|
416
895
|
* ```ts
|
|
417
|
-
* const
|
|
418
|
-
*
|
|
419
|
-
*
|
|
420
|
-
* type: 'snapshot',
|
|
421
|
-
* agentId: agent.id,
|
|
422
|
-
* name: '@kubb/demo',
|
|
423
|
-
* version: '1.0.0',
|
|
424
|
-
* })
|
|
425
|
-
* const finished = await waitForJob({ studioUrl, token, id: job.id })
|
|
896
|
+
* const { source, outcomes } = applyConfigEdits(current, [
|
|
897
|
+
* { operation: 'set', plugin: '@kubb/plugin-ts', path: ['enum', 'type'], value: 'enum' },
|
|
898
|
+
* ])
|
|
426
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.
|
|
427
904
|
*/
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
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
|
|
450
947
|
});
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const wait = Math.min(retryAfterMs(error) ?? withJitter(interval), Math.max(deadline - Date.now(), 0));
|
|
457
|
-
if (signal) await (0, node_timers_promises.setTimeout)(wait, void 0, { signal });
|
|
458
|
-
else await new Promise((resolve) => setTimeout(resolve, wait));
|
|
459
|
-
if (Date.now() >= deadline) throw error;
|
|
460
|
-
interval = Math.min(interval * 2, CREATE_JOB_MAX_INTERVAL_MS);
|
|
948
|
+
current = withTrailingNewline(next, endsWithNewline);
|
|
949
|
+
return {
|
|
950
|
+
edit,
|
|
951
|
+
applied: true
|
|
952
|
+
};
|
|
461
953
|
}
|
|
462
|
-
|
|
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
|
+
});
|
|
968
|
+
return {
|
|
969
|
+
source: current,
|
|
970
|
+
outcomes,
|
|
971
|
+
changed: current !== source
|
|
972
|
+
};
|
|
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;
|
|
463
978
|
}
|
|
464
979
|
/**
|
|
465
|
-
*
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
/**
|
|
469
|
-
* Slowest the poll backs off to. Requests per run are roughly `timeoutMs` divided by this, and
|
|
470
|
-
* every concurrent run on the same organization key draws on one budget.
|
|
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.
|
|
471
983
|
*/
|
|
472
|
-
|
|
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
|
+
}
|
|
473
987
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
* job stays inside the API key's rate limit.
|
|
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.
|
|
477
990
|
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (Date.now() >= deadline) throw new Error("Timed out waiting for the Studio job");
|
|
490
|
-
interval = Math.min(interval * 2, MAX_POLL_INTERVAL_MS);
|
|
491
|
-
try {
|
|
492
|
-
const { job } = await (0, ofetch.ofetch)(`${studioUrl}/api/jobs/${id}`, {
|
|
493
|
-
headers: { "x-api-key": token },
|
|
494
|
-
retry: false,
|
|
495
|
-
timeout: Math.max(deadline - Date.now(), 1),
|
|
496
|
-
signal
|
|
497
|
-
});
|
|
498
|
-
if (job.status === "success" || job.status === "failed" || job.status === "canceled") return job;
|
|
499
|
-
} catch (error) {
|
|
500
|
-
signal?.throwIfAborted();
|
|
501
|
-
const response = error.response;
|
|
502
|
-
if (response?.status !== 429) throw error;
|
|
503
|
-
const retryAfter = response._data?.data?.tryAgainIn;
|
|
504
|
-
interval = Math.max(interval, typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter : MAX_POLL_INTERVAL_MS);
|
|
505
|
-
}
|
|
506
|
-
}
|
|
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");
|
|
507
1002
|
}
|
|
508
1003
|
/**
|
|
509
|
-
*
|
|
510
|
-
* Authenticates via `x-api-key`. Reusing the same `machineToken` reuses the same agent instead of
|
|
511
|
-
* consuming a new one from the organization's agent limit.
|
|
1004
|
+
* `generateCode` always drops the file's trailing newline. Restore it when the input had one.
|
|
512
1005
|
*/
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
method: "POST",
|
|
517
|
-
headers: { "x-api-key": token },
|
|
518
|
-
body: {
|
|
519
|
-
name,
|
|
520
|
-
machineToken
|
|
521
|
-
}
|
|
522
|
-
});
|
|
523
|
-
} catch (error) {
|
|
524
|
-
if (error instanceof ofetch.FetchError) {
|
|
525
|
-
const upgradeUrl = error.data?.data?.upgradeUrl;
|
|
526
|
-
const detail = responseMessage(error.data) ?? getErrorMessage(error);
|
|
527
|
-
const hint = upgradeUrl ? ` Agent limit reached; upgrade at ${upgradeUrl}.` : "";
|
|
528
|
-
throw new Error(`Failed to create a Kubb Studio agent: ${detail}${hint}`, { cause: error });
|
|
529
|
-
}
|
|
530
|
-
throw error;
|
|
531
|
-
}
|
|
1006
|
+
function withTrailingNewline(code, hadTrailingNewline) {
|
|
1007
|
+
if (!hadTrailingNewline || code.endsWith("\n")) return code;
|
|
1008
|
+
return `${code}\n`;
|
|
532
1009
|
}
|
|
533
1010
|
//#endregion
|
|
534
|
-
//#region src/
|
|
1011
|
+
//#region src/generations.ts
|
|
1012
|
+
const READ_CONCURRENCY = 50;
|
|
1013
|
+
const MB = 1048576;
|
|
1014
|
+
const INDEX_KEY = "studio/generations.json";
|
|
535
1015
|
/**
|
|
536
|
-
*
|
|
537
|
-
* not whatever default the client would pick on its own.
|
|
1016
|
+
* The path form {@link SourceFiles} keys on: relative to `root`, with forward slashes.
|
|
538
1017
|
*/
|
|
539
|
-
|
|
1018
|
+
function relativeStoragePath({ root, filePath }) {
|
|
1019
|
+
return ((0, node_path.isAbsolute)(filePath) ? (0, node_path.relative)((0, node_path.resolve)(root), filePath) : filePath).replaceAll("\\", "/");
|
|
1020
|
+
}
|
|
540
1021
|
/**
|
|
541
|
-
*
|
|
542
|
-
* Config path is left out on purpose: each host discovers that itself.
|
|
1022
|
+
* The content fingerprint Studio compares snapshots by. Agent runs and CI uploads must agree on it.
|
|
543
1023
|
*/
|
|
544
|
-
const
|
|
545
|
-
|
|
546
|
-
retryIntervalMs: 3e4,
|
|
547
|
-
heartbeatIntervalMs: 3e4,
|
|
548
|
-
/**
|
|
549
|
-
* Slowest heartbeat a host may ask for. Studio drops an agent from the active list once its
|
|
550
|
-
* stored ping is older than its liveness window, and it stores a ping at most once a minute, so
|
|
551
|
-
* a slower cadence would make a healthy agent look dead after a single missed ping.
|
|
552
|
-
*/
|
|
553
|
-
maxHeartbeatIntervalMs: 6e4,
|
|
554
|
-
/** How long a heartbeat ping may take before the session is treated as dead. */
|
|
555
|
-
heartbeatTimeoutMs: 1e4,
|
|
556
|
-
maxConcurrent: 1,
|
|
557
|
-
maxGenerations: 8,
|
|
558
|
-
maxGenerationsMb: 100,
|
|
559
|
-
maxSnapshotMb: 50
|
|
560
|
-
};
|
|
1024
|
+
const hashOf = (content) => (0, node_crypto.createHash)("sha1").update(content).digest("hex").slice(0, 16);
|
|
1025
|
+
const totalBytes = (entries) => entries.reduce((sum, { output, disk }) => sum + output.bytes + (disk?.bytes ?? 0), 0);
|
|
561
1026
|
function positiveNumber(value) {
|
|
562
1027
|
const parsed = Number(value);
|
|
563
1028
|
return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
@@ -567,29 +1032,49 @@ function positiveNumber(value) {
|
|
|
567
1032
|
* `KUBB_AGENT_MAX_GENERATIONS`, `KUBB_AGENT_MAX_GENERATIONS_MB` and `KUBB_AGENT_MAX_SNAPSHOT_MB`.
|
|
568
1033
|
* An unset or invalid value keeps the default.
|
|
569
1034
|
*/
|
|
570
|
-
function resolveGenerationLimits(
|
|
1035
|
+
function resolveGenerationLimits() {
|
|
571
1036
|
return {
|
|
572
|
-
maxCount: Math.max(1, Math.floor(positiveNumber(env.KUBB_AGENT_MAX_GENERATIONS) ??
|
|
573
|
-
maxMb: positiveNumber(env.KUBB_AGENT_MAX_GENERATIONS_MB) ??
|
|
574
|
-
maxSnapshotMb: positiveNumber(env.KUBB_AGENT_MAX_SNAPSHOT_MB) ??
|
|
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
|
|
575
1040
|
};
|
|
576
1041
|
}
|
|
577
1042
|
/**
|
|
578
|
-
*
|
|
579
|
-
* default: one job at a time.
|
|
1043
|
+
* Folds error-severity diagnostics into one thrown error so logs name the failing plugin or pass.
|
|
580
1044
|
*/
|
|
581
|
-
function
|
|
582
|
-
|
|
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
|
+
};
|
|
583
1077
|
}
|
|
584
|
-
//#endregion
|
|
585
|
-
//#region package.json
|
|
586
|
-
var version = "5.5.6";
|
|
587
|
-
//#endregion
|
|
588
|
-
//#region src/operations/generations.ts
|
|
589
|
-
const READ_CONCURRENCY = 50;
|
|
590
|
-
const MB$1 = 1048576;
|
|
591
|
-
const INDEX_KEY = "studio/generations.json";
|
|
592
|
-
const hashOf = (content) => (0, node_crypto.createHash)("sha1").update(content).digest("hex").slice(0, 16);
|
|
593
1078
|
/**
|
|
594
1079
|
* What the output directory holds on disk before a run. `undefined` when `outputPath` is not a real
|
|
595
1080
|
* subdirectory of `root` (listing the root would take in every source file) or holds more than `maxFiles`.
|
|
@@ -632,7 +1117,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
632
1117
|
* Copies `files` into the store as one set of `jobId`. Above `maxSetMb` only the hashes are kept.
|
|
633
1118
|
*/
|
|
634
1119
|
async function keep({ jobId, source, files, maxSetMb }) {
|
|
635
|
-
const maxSetBytes = maxSetMb * MB
|
|
1120
|
+
const maxSetBytes = maxSetMb * MB;
|
|
636
1121
|
const hashes = {};
|
|
637
1122
|
let bytes = 0;
|
|
638
1123
|
await inParallel({
|
|
@@ -668,9 +1153,6 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
668
1153
|
keep,
|
|
669
1154
|
drop,
|
|
670
1155
|
get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId && isLive(generation)),
|
|
671
|
-
latest: async () => (await load()).filter(isLive).at(-1),
|
|
672
|
-
/** Total bytes of every set the store holds, expired ones too until the next add drops them. */
|
|
673
|
-
bytes: async () => (await load()).reduce((sum, { output, disk }) => sum + output.bytes + (disk?.bytes ?? 0), 0),
|
|
674
1156
|
async add(generation) {
|
|
675
1157
|
const current = await load();
|
|
676
1158
|
for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
|
|
@@ -679,8 +1161,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
679
1161
|
...generation,
|
|
680
1162
|
keptAt: now()
|
|
681
1163
|
});
|
|
682
|
-
|
|
683
|
-
while (entries.length > 1 && (entries.length > maxCount || weight() > maxMb * MB$1)) await drop(entries.shift().jobId);
|
|
1164
|
+
while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
|
|
684
1165
|
index = entries;
|
|
685
1166
|
await storage.writeItem(INDEX_KEY, JSON.stringify(entries));
|
|
686
1167
|
},
|
|
@@ -718,41 +1199,12 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
718
1199
|
};
|
|
719
1200
|
}
|
|
720
1201
|
//#endregion
|
|
721
|
-
//#region src/
|
|
722
|
-
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
723
|
-
function relativeStoragePath(root, filePath) {
|
|
724
|
-
return ((0, node_path.isAbsolute)(filePath) ? (0, node_path.relative)((0, node_path.resolve)(root), filePath) : filePath).replaceAll("\\", "/");
|
|
725
|
-
}
|
|
726
|
-
async function resolvePeerDependencies(names) {
|
|
727
|
-
const uniqueNames = [...new Set(names.map(require_resolveConfig.toPackageName))];
|
|
728
|
-
const peerDependencies = {};
|
|
729
|
-
const missingDependencies = [];
|
|
730
|
-
const versions = await Promise.all(uniqueNames.map(async (name) => {
|
|
731
|
-
try {
|
|
732
|
-
const path = require$1.resolve(`${name}/package.json`);
|
|
733
|
-
return JSON.parse(await (0, node_fs_promises.readFile)(path, "utf8")).version;
|
|
734
|
-
} catch {
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
737
|
-
}));
|
|
738
|
-
for (const [index, name] of uniqueNames.entries()) {
|
|
739
|
-
const version = versions[index];
|
|
740
|
-
if (version) {
|
|
741
|
-
peerDependencies[name] = version;
|
|
742
|
-
continue;
|
|
743
|
-
}
|
|
744
|
-
missingDependencies.push(name);
|
|
745
|
-
}
|
|
746
|
-
return {
|
|
747
|
-
peerDependencies,
|
|
748
|
-
missingDependencies
|
|
749
|
-
};
|
|
750
|
-
}
|
|
1202
|
+
//#region src/generationEvents.ts
|
|
751
1203
|
const MAX_QUEUED_EVENTS = 1024;
|
|
752
1204
|
const RESERVED_EVENTS = 64;
|
|
753
1205
|
const isDiscardable = (event) => event.type === "kubb:files:processing:update" || event.type === "kubb:info" || event.type === "kubb:success";
|
|
754
1206
|
/** Forwards selected Kubb lifecycle events to a native Cap'n Web stream. */
|
|
755
|
-
function createGenerationStream(hooks, jobId
|
|
1207
|
+
function createGenerationStream(hooks, jobId) {
|
|
756
1208
|
const unhooks = [];
|
|
757
1209
|
let root = "";
|
|
758
1210
|
let controller;
|
|
@@ -789,129 +1241,92 @@ function createGenerationStream(hooks, jobId, options = {}) {
|
|
|
789
1241
|
}
|
|
790
1242
|
controller.enqueue(event);
|
|
791
1243
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
}]);
|
|
801
|
-
});
|
|
802
|
-
on("kubb:build:start", ({ config, adapter }) => {
|
|
803
|
-
root = config.root;
|
|
804
|
-
emitEvent("kubb:build:start", [{
|
|
1244
|
+
const projections = {
|
|
1245
|
+
"kubb:plugin:start": ({ plugin }) => [{ plugin: { name: plugin.name } }],
|
|
1246
|
+
"kubb:plugin:end": ({ plugin, duration, success }) => [{
|
|
1247
|
+
plugin: { name: plugin.name },
|
|
1248
|
+
duration,
|
|
1249
|
+
success
|
|
1250
|
+
}],
|
|
1251
|
+
"kubb:build:start": ({ config, adapter }) => [{
|
|
805
1252
|
config: { name: config.name },
|
|
806
1253
|
adapter: { name: adapter.name }
|
|
807
|
-
}]
|
|
808
|
-
|
|
809
|
-
on("kubb:build:end", ({ files, config, outputDir }) => {
|
|
810
|
-
emitEvent("kubb:build:end", [{
|
|
1254
|
+
}],
|
|
1255
|
+
"kubb:build:end": ({ files, config, outputDir }) => [{
|
|
811
1256
|
files: files.map((file) => ({
|
|
812
|
-
path: relativeStoragePath(
|
|
1257
|
+
path: relativeStoragePath({
|
|
1258
|
+
root: config.root,
|
|
1259
|
+
filePath: file.path
|
|
1260
|
+
}),
|
|
813
1261
|
name: file.name
|
|
814
1262
|
})),
|
|
815
1263
|
outputDir
|
|
816
|
-
}]
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
file: relativeStoragePath(root, file.path),
|
|
1264
|
+
}],
|
|
1265
|
+
"kubb:files:processing:start": ({ files }) => [{ total: files.length }],
|
|
1266
|
+
"kubb:files:processing:update": ({ files }) => [{ files: files.map(({ file, processed, total, percentage }) => ({
|
|
1267
|
+
file: relativeStoragePath({
|
|
1268
|
+
root,
|
|
1269
|
+
filePath: file.path
|
|
1270
|
+
}),
|
|
824
1271
|
processed,
|
|
825
1272
|
total,
|
|
826
1273
|
percentage
|
|
827
|
-
})) }]
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
});
|
|
832
|
-
for (const type of [
|
|
833
|
-
"kubb:info",
|
|
834
|
-
"kubb:success",
|
|
835
|
-
"kubb:warn"
|
|
836
|
-
]) on(type, ({ message, info }) => {
|
|
837
|
-
emitEvent(type, [{
|
|
838
|
-
message,
|
|
1274
|
+
})) }],
|
|
1275
|
+
"kubb:files:processing:end": ({ files }) => [{ total: files.length }],
|
|
1276
|
+
"kubb:info": ({ message, info }) => [{
|
|
1277
|
+
message: (0, node_util.stripVTControlCharacters)(message),
|
|
839
1278
|
info
|
|
840
|
-
}]
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
const keys = await storage.readKeys();
|
|
851
|
-
const paths = new Set(keys.map((key) => relativeStoragePath(config.root, key)));
|
|
852
|
-
options.onGenerationEnd?.({
|
|
853
|
-
output: {
|
|
854
|
-
storage,
|
|
855
|
-
root: config.root,
|
|
856
|
-
paths
|
|
857
|
-
},
|
|
858
|
-
peerDependencies,
|
|
859
|
-
missingDependencies
|
|
860
|
-
});
|
|
861
|
-
emitEvent("kubb:generation:end", []);
|
|
862
|
-
if (!hrStart) return;
|
|
863
|
-
emitEvent("kubb:generation:summary", [{
|
|
864
|
-
duration: Math.round(getElapsedMs(hrStart)),
|
|
865
|
-
fileCount: filesCreated ?? 0,
|
|
866
|
-
failedPlugins: _kubb_core.Diagnostics.failedPlugins(diagnostics).length,
|
|
867
|
-
status: status ?? "success"
|
|
868
|
-
}]);
|
|
869
|
-
});
|
|
870
|
-
on("kubb:error", ({ error }) => {
|
|
871
|
-
emitEvent("kubb:error", [{
|
|
1279
|
+
}],
|
|
1280
|
+
"kubb:success": ({ message, info }) => [{
|
|
1281
|
+
message: (0, node_util.stripVTControlCharacters)(message),
|
|
1282
|
+
info
|
|
1283
|
+
}],
|
|
1284
|
+
"kubb:warn": ({ message, info }) => [{
|
|
1285
|
+
message: (0, node_util.stripVTControlCharacters)(message),
|
|
1286
|
+
info
|
|
1287
|
+
}],
|
|
1288
|
+
"kubb:error": ({ error }) => [{
|
|
872
1289
|
message: error.message,
|
|
873
1290
|
stack: error.stack
|
|
874
|
-
}]
|
|
875
|
-
|
|
876
|
-
on("kubb:diagnostic", ({ diagnostic }) => {
|
|
877
|
-
const cause = "cause" in diagnostic ? diagnostic.cause : void 0;
|
|
878
|
-
emitEvent("kubb:diagnostic", [{
|
|
1291
|
+
}],
|
|
1292
|
+
"kubb:diagnostic": ({ diagnostic }) => [{
|
|
879
1293
|
code: diagnostic.code,
|
|
880
1294
|
message: diagnostic.message,
|
|
881
1295
|
severity: diagnostic.severity,
|
|
882
1296
|
location: "location" in diagnostic ? diagnostic.location : void 0,
|
|
883
1297
|
help: "help" in diagnostic ? diagnostic.help : void 0,
|
|
884
1298
|
plugin: "plugin" in diagnostic ? diagnostic.plugin : void 0,
|
|
885
|
-
stack: cause?.stack
|
|
886
|
-
}]
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
"kubb:
|
|
892
|
-
"kubb:
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
1299
|
+
stack: "cause" in diagnostic ? diagnostic.cause?.stack : void 0
|
|
1300
|
+
}],
|
|
1301
|
+
"kubb:generation:start": ({ config }) => [{
|
|
1302
|
+
name: config.name,
|
|
1303
|
+
plugins: config.plugins.length
|
|
1304
|
+
}],
|
|
1305
|
+
"kubb:generation:end": () => [],
|
|
1306
|
+
"kubb:generation:summary": ({ duration, fileCount, failedPlugins, status }) => [{
|
|
1307
|
+
duration,
|
|
1308
|
+
fileCount,
|
|
1309
|
+
failedPlugins,
|
|
1310
|
+
status
|
|
1311
|
+
}],
|
|
1312
|
+
"kubb:lifecycle:start": () => [],
|
|
1313
|
+
"kubb:lifecycle:end": () => [],
|
|
1314
|
+
"kubb:format:start": () => [],
|
|
1315
|
+
"kubb:format:end": () => [],
|
|
1316
|
+
"kubb:lint:start": () => [],
|
|
1317
|
+
"kubb:lint:end": () => [],
|
|
1318
|
+
"kubb:hooks:start": () => [],
|
|
1319
|
+
"kubb:hooks:end": () => [],
|
|
1320
|
+
"kubb:hook:start": ({ id, command, args }) => [{
|
|
902
1321
|
id,
|
|
903
1322
|
command,
|
|
904
1323
|
args: args ? [...args] : void 0
|
|
905
|
-
}]
|
|
906
|
-
|
|
907
|
-
on("kubb:hook:line", ({ id, line }) => {
|
|
908
|
-
emitEvent("kubb:hook:line", [{
|
|
1324
|
+
}],
|
|
1325
|
+
"kubb:hook:line": ({ id, line }) => [{
|
|
909
1326
|
id,
|
|
910
1327
|
line
|
|
911
|
-
}]
|
|
912
|
-
|
|
913
|
-
on("kubb:hook:end", ({ id, command, args, success, error }) => {
|
|
914
|
-
emitEvent("kubb:hook:end", [{
|
|
1328
|
+
}],
|
|
1329
|
+
"kubb:hook:end": ({ id, command, args, success, error }) => [{
|
|
915
1330
|
id,
|
|
916
1331
|
command,
|
|
917
1332
|
args: args ? [...args] : void 0,
|
|
@@ -920,6 +1335,22 @@ function createGenerationStream(hooks, jobId, options = {}) {
|
|
|
920
1335
|
message: error.message,
|
|
921
1336
|
stack: error.stack
|
|
922
1337
|
} : void 0
|
|
1338
|
+
}]
|
|
1339
|
+
};
|
|
1340
|
+
function forward(type) {
|
|
1341
|
+
on(type, (...args) => emitEvent(type, projections[type](...args)));
|
|
1342
|
+
}
|
|
1343
|
+
on("kubb:build:start", ({ config }) => {
|
|
1344
|
+
root = config.root;
|
|
1345
|
+
});
|
|
1346
|
+
for (const type of require_protocol.generationEventTypes) forward(type);
|
|
1347
|
+
on("kubb:generation:end", ({ diagnostics = [], status, hrStart, filesCreated }) => {
|
|
1348
|
+
if (!hrStart) return;
|
|
1349
|
+
emitEvent("kubb:generation:summary", [{
|
|
1350
|
+
duration: Math.round(getElapsedMs(hrStart)),
|
|
1351
|
+
fileCount: filesCreated ?? 0,
|
|
1352
|
+
failedPlugins: _kubb_core.Diagnostics.failedPlugins(diagnostics).length,
|
|
1353
|
+
status: status ?? "success"
|
|
923
1354
|
}]);
|
|
924
1355
|
});
|
|
925
1356
|
/**
|
|
@@ -949,638 +1380,151 @@ function createGenerationStream(hooks, jobId, options = {}) {
|
|
|
949
1380
|
};
|
|
950
1381
|
}
|
|
951
1382
|
//#endregion
|
|
952
|
-
//#region src/
|
|
953
|
-
const CONNECT_TIMEOUT_MS = 5e3;
|
|
1383
|
+
//#region src/url.ts
|
|
954
1384
|
/**
|
|
955
|
-
*
|
|
1385
|
+
* Hosted Kubb Studio URL, used when a host passes none.
|
|
956
1386
|
*/
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
if (ws$1.readyState === ws.default.CONNECTING) ws$1.close(3008, "Connection timeout");
|
|
961
|
-
}, CONNECT_TIMEOUT_MS);
|
|
962
|
-
ws$1.once("open", () => clearTimeout(timer));
|
|
963
|
-
ws$1.once("close", () => clearTimeout(timer));
|
|
964
|
-
return ws$1;
|
|
1387
|
+
const defaultStudioUrl = "https://kubb.studio";
|
|
1388
|
+
function isLoopbackHost(hostname) {
|
|
1389
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]";
|
|
965
1390
|
}
|
|
966
|
-
//#endregion
|
|
967
|
-
//#region src/operations/rpc.ts
|
|
968
|
-
/**
|
|
969
|
-
* The only methods Studio may call on an agent. A `StudioSession` carries far more than
|
|
970
|
-
* {@link AgentApi}, so it is wrapped rather than exposed: what Cap'n Web can reach is exactly what
|
|
971
|
-
* this class re-declares.
|
|
972
|
-
*/
|
|
973
|
-
var AgentRpcTarget = class extends capnweb.RpcTarget {
|
|
974
|
-
api;
|
|
975
|
-
constructor(api) {
|
|
976
|
-
super();
|
|
977
|
-
this.api = api;
|
|
978
|
-
}
|
|
979
|
-
connect() {
|
|
980
|
-
return this.api.connect();
|
|
981
|
-
}
|
|
982
|
-
startGeneration(input) {
|
|
983
|
-
return this.api.startGeneration(input);
|
|
984
|
-
}
|
|
985
|
-
saveConfig(input) {
|
|
986
|
-
return this.api.saveConfig(input);
|
|
987
|
-
}
|
|
988
|
-
publishSnapshot(input) {
|
|
989
|
-
return this.api.publishSnapshot(input);
|
|
990
|
-
}
|
|
991
|
-
readFiles(input) {
|
|
992
|
-
return this.api.readFiles(input);
|
|
993
|
-
}
|
|
994
|
-
cancel(jobId) {
|
|
995
|
-
return this.api.cancel(jobId);
|
|
996
|
-
}
|
|
997
|
-
};
|
|
998
1391
|
/**
|
|
999
|
-
*
|
|
1000
|
-
*
|
|
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:`.
|
|
1001
1394
|
*
|
|
1002
1395
|
* @example
|
|
1003
1396
|
* ```ts
|
|
1004
|
-
*
|
|
1005
|
-
* await rpc.studio.ping()
|
|
1397
|
+
* secureUrl({ studioUrl: 'https://kubb.studio', path: '/api/agent/socket', socket: true }).href // 'wss://kubb.studio/api/agent/socket'
|
|
1006
1398
|
* ```
|
|
1007
1399
|
*/
|
|
1008
|
-
|
|
1009
|
-
const
|
|
1010
|
-
if (protocol !== "
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
[require_protocol.AGENT_INSTANCE_HEADER]: instanceId
|
|
1014
|
-
} });
|
|
1015
|
-
const closed = new Promise((resolve) => socket.once("close", (code, reason) => resolve({
|
|
1016
|
-
code,
|
|
1017
|
-
reason: reason.toString()
|
|
1018
|
-
})));
|
|
1019
|
-
const studio = (0, capnweb.newWebSocketRpcSession)(socket, new AgentRpcTarget(local));
|
|
1020
|
-
studio.onRpcBroken(() => socket.close());
|
|
1021
|
-
return {
|
|
1022
|
-
studio,
|
|
1023
|
-
closed,
|
|
1024
|
-
close: () => studio[Symbol.dispose]()
|
|
1025
|
-
};
|
|
1026
|
-
};
|
|
1027
|
-
//#endregion
|
|
1028
|
-
//#region src/operations/hooks.ts
|
|
1029
|
-
/**
|
|
1030
|
-
* Register a `kubb:hook:start` listener that spawns the requested command via tinyexec,
|
|
1031
|
-
* streams each stdout line as a `kubb:hook:line` event, and calls `kubb:hook:end` with the result.
|
|
1032
|
-
* A failure travels on `kubb:hook:end` only: the caller waiting on it reports `kubb:error` once.
|
|
1033
|
-
* Streaming the output lets Kubb Studio render live hook progress over the WebSocket connection.
|
|
1034
|
-
*
|
|
1035
|
-
* Returns a remover, so a session that runs one generation after another on the same emitter does
|
|
1036
|
-
* not stack a listener per run.
|
|
1037
|
-
*/
|
|
1038
|
-
function setupHookListener(hooks, root, signal) {
|
|
1039
|
-
return hooks.hook("kubb:hook:start", async (ctx) => {
|
|
1040
|
-
const { id, command, args } = ctx;
|
|
1041
|
-
if (!id) return;
|
|
1042
|
-
const commandWithArgs = args?.length ? `${command} ${args.join(" ")}` : command;
|
|
1043
|
-
try {
|
|
1044
|
-
const proc = (0, tinyexec.x)(command, [...args ?? []], {
|
|
1045
|
-
signal,
|
|
1046
|
-
nodeOptions: {
|
|
1047
|
-
cwd: root,
|
|
1048
|
-
detached: true
|
|
1049
|
-
}
|
|
1050
|
-
});
|
|
1051
|
-
for await (const line of proc) await hooks.callHook("kubb:hook:line", {
|
|
1052
|
-
id,
|
|
1053
|
-
line
|
|
1054
|
-
});
|
|
1055
|
-
const { exitCode } = await proc;
|
|
1056
|
-
if (exitCode !== 0) {
|
|
1057
|
-
const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
|
|
1058
|
-
await hooks.callHook("kubb:hook:end", {
|
|
1059
|
-
id,
|
|
1060
|
-
command,
|
|
1061
|
-
args,
|
|
1062
|
-
success: false,
|
|
1063
|
-
error
|
|
1064
|
-
});
|
|
1065
|
-
return;
|
|
1066
|
-
}
|
|
1067
|
-
await hooks.callHook("kubb:hook:end", {
|
|
1068
|
-
id,
|
|
1069
|
-
command,
|
|
1070
|
-
args,
|
|
1071
|
-
success: true,
|
|
1072
|
-
error: null
|
|
1073
|
-
});
|
|
1074
|
-
} catch (caughtError) {
|
|
1075
|
-
const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
|
|
1076
|
-
error.cause = caughtError;
|
|
1077
|
-
await hooks.callHook("kubb:hook:end", {
|
|
1078
|
-
id,
|
|
1079
|
-
command,
|
|
1080
|
-
args,
|
|
1081
|
-
success: false,
|
|
1082
|
-
error
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
});
|
|
1086
|
-
}
|
|
1087
|
-
/**
|
|
1088
|
-
* Waits for the `kubb:hook:end` matching `hookId`. Register this before calling `kubb:hook:start`:
|
|
1089
|
-
* `callHook` awaits its listeners, and {@link setupHookListener} calls `kubb:hook:end` from inside
|
|
1090
|
-
* that same listener, so a handler added afterward would already have missed it.
|
|
1091
|
-
*/
|
|
1092
|
-
function waitForHookEnd(hooks, hookId) {
|
|
1093
|
-
return new Promise((resolve, reject) => {
|
|
1094
|
-
const handleHookEnd = (ctx) => {
|
|
1095
|
-
if (ctx.id !== hookId) return;
|
|
1096
|
-
hooks.removeHook("kubb:hook:end", handleHookEnd);
|
|
1097
|
-
if (ctx.success) {
|
|
1098
|
-
resolve();
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
reject(ctx.error);
|
|
1102
|
-
};
|
|
1103
|
-
hooks.hook("kubb:hook:end", handleHookEnd);
|
|
1104
|
-
});
|
|
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;
|
|
1105
1405
|
}
|
|
1106
1406
|
//#endregion
|
|
1107
|
-
//#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;
|
|
1108
1413
|
const DISK_SNAPSHOT_MAX_FILES = 1e4;
|
|
1109
|
-
/**
|
|
1110
|
-
* `isToolAvailable` spawns a process, and a long-lived connection generates repeatedly, so each
|
|
1111
|
-
* executable is probed once per process. The CLI deliberately does not memoize: a `--watch` build
|
|
1112
|
-
* should keep noticing a tool installed mid-session.
|
|
1113
|
-
*/
|
|
1114
|
-
const detectTool = memoize(/* @__PURE__ */ new Map(), detectTool$1);
|
|
1115
|
-
/**
|
|
1116
|
-
* The two post-build tool steps. Formatting and linting differ only in which tools they look for,
|
|
1117
|
-
* so they run through one loop rather than two near-identical blocks.
|
|
1118
|
-
*
|
|
1119
|
-
* `noun` and `verbing` are spelled out instead of built from `kind`. Concatenating `` `${kind}ter` ``
|
|
1120
|
-
* and `` `${kind}ting` `` works for `format`, but doubles the `t` in `lint`, giving "lintter" and
|
|
1121
|
-
* "lintting" instead of "linter" and "linting".
|
|
1122
|
-
*/
|
|
1123
|
-
const TOOL_STEPS = [{
|
|
1124
|
-
kind: "format",
|
|
1125
|
-
noun: "formatter",
|
|
1126
|
-
verbing: "Formatting",
|
|
1127
|
-
tools: formatters,
|
|
1128
|
-
detect: FORMATTER_PREFERENCE
|
|
1129
|
-
}, {
|
|
1130
|
-
kind: "lint",
|
|
1131
|
-
noun: "linter",
|
|
1132
|
-
verbing: "Linting",
|
|
1133
|
-
tools: linters,
|
|
1134
|
-
detect: LINTER_PREFERENCE
|
|
1135
|
-
}];
|
|
1136
|
-
/**
|
|
1137
|
-
* Absolute path of the directory the formatter and linter are pointed at.
|
|
1138
|
-
*/
|
|
1139
|
-
function outputPath(config) {
|
|
1140
|
-
return node_path.default.isAbsolute(config.output.path) ? config.output.path : node_path.default.resolve(node_process.default.cwd(), config.root, config.output.path);
|
|
1141
|
-
}
|
|
1142
|
-
/**
|
|
1143
|
-
* Emits `kubb:hook:start` and waits for the matching `kubb:hook:end`. The host spawns the process:
|
|
1144
|
-
* this only describes what to run and when it finished.
|
|
1145
|
-
*
|
|
1146
|
-
* @throws whatever the command failed with, so callers can report it their own way.
|
|
1147
|
-
*/
|
|
1148
|
-
async function runHook({ hooks, id, command, args }) {
|
|
1149
|
-
const hookId = (0, node_crypto.hash)("sha256", id);
|
|
1150
|
-
const hookEnd = waitForHookEnd(hooks, hookId);
|
|
1151
|
-
await hooks.callHook("kubb:hook:start", {
|
|
1152
|
-
id: hookId,
|
|
1153
|
-
command,
|
|
1154
|
-
args: [...args]
|
|
1155
|
-
});
|
|
1156
|
-
await hookEnd;
|
|
1157
|
-
}
|
|
1158
|
-
function isProblemErrorDiagnostic(diagnostic) {
|
|
1159
|
-
return (diagnostic.kind ?? "problem") === "problem" && diagnostic.severity === "error";
|
|
1160
|
-
}
|
|
1161
|
-
/**
|
|
1162
|
-
* Folds error-severity diagnostics into one thrown error so logs name the failing plugin.
|
|
1163
|
-
*/
|
|
1164
|
-
function formatGenerationFailure(diagnostics) {
|
|
1165
|
-
const reasons = diagnostics.filter(isProblemErrorDiagnostic).map((diagnostic) => diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message);
|
|
1166
|
-
if (!reasons.length) return /* @__PURE__ */ new Error("Generation failed");
|
|
1167
|
-
return /* @__PURE__ */ new Error(`Generation failed: ${reasons.length} error${reasons.length === 1 ? "" : "s"}: ${reasons.join("; ")}`);
|
|
1168
|
-
}
|
|
1169
|
-
/**
|
|
1170
|
-
* Runs a full Kubb code-generation cycle for the given config.
|
|
1171
|
-
*
|
|
1172
|
-
* Emits lifecycle events on the provided `hooks` emitter so callers (e.g. the WebSocket stream)
|
|
1173
|
-
* can forward progress to connected clients. After a successful build, auto-formatting and
|
|
1174
|
-
* linting are applied when configured, followed by any user-defined `hooks.done` commands.
|
|
1175
|
-
*/
|
|
1176
|
-
async function generate({ config, hooks, signal }) {
|
|
1177
|
-
signal?.throwIfAborted();
|
|
1178
|
-
const hrStart = node_process.default.hrtime();
|
|
1179
|
-
await hooks.callHook("kubb:generation:start", { config });
|
|
1180
|
-
await hooks.callHook("kubb:info", { message: config.name ? `Setup generation ${config.name}` : "Setup generation" });
|
|
1181
|
-
const kubb = (0, _kubb_core.createKubb)(config, {
|
|
1182
|
-
hooks,
|
|
1183
|
-
signal
|
|
1184
|
-
});
|
|
1185
|
-
await kubb.setup();
|
|
1186
|
-
await hooks.callHook("kubb:info", { message: config.name ? `Build generation ${config.name}` : "Build generation" });
|
|
1187
|
-
const { files, diagnostics, storage } = await kubb.safeBuild();
|
|
1188
|
-
signal?.throwIfAborted();
|
|
1189
|
-
await hooks.callHook("kubb:info", { message: "Load summary" });
|
|
1190
|
-
for (const diagnostic of diagnostics.filter(isProblemErrorDiagnostic)) await hooks.callHook("kubb:error", { error: new Error(diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message) });
|
|
1191
|
-
const status = _kubb_core.Diagnostics.hasError(diagnostics) ? "failed" : "success";
|
|
1192
|
-
await hooks.callHook("kubb:generation:end", {
|
|
1193
|
-
config,
|
|
1194
|
-
storage: {
|
|
1195
|
-
...storage,
|
|
1196
|
-
readKeys: async () => [...new Set(files.map((file) => file.path))]
|
|
1197
|
-
},
|
|
1198
|
-
diagnostics,
|
|
1199
|
-
status,
|
|
1200
|
-
hrStart,
|
|
1201
|
-
filesCreated: files.length
|
|
1202
|
-
});
|
|
1203
|
-
if (status === "failed") throw formatGenerationFailure(diagnostics);
|
|
1204
|
-
await hooks.callHook("kubb:success", { message: "Generation successfully" });
|
|
1205
|
-
for (const step of TOOL_STEPS) {
|
|
1206
|
-
const setting = config.output[step.kind];
|
|
1207
|
-
if (!setting) continue;
|
|
1208
|
-
await hooks.callHook(`kubb:${step.kind}:start`);
|
|
1209
|
-
const tool = setting === "auto" ? await detectTool(step.detect) : setting;
|
|
1210
|
-
if (!tool) await hooks.callHook("kubb:warn", { message: `No ${step.noun} found (${step.detect.join(", ")}). Skipping ${step.verbing.toLowerCase()}.` });
|
|
1211
|
-
if (tool && setting === "auto") await hooks.callHook("kubb:info", { message: `Auto-detected ${step.noun}: ${(0, node_util.styleText)("dim", tool)}` });
|
|
1212
|
-
const command = tool ? step.tools[tool] : void 0;
|
|
1213
|
-
if (command) try {
|
|
1214
|
-
await runHook({
|
|
1215
|
-
hooks,
|
|
1216
|
-
id: [config.name, tool].filter(Boolean).join("-"),
|
|
1217
|
-
command: command.command,
|
|
1218
|
-
args: command.args(outputPath(config))
|
|
1219
|
-
});
|
|
1220
|
-
await hooks.callHook("kubb:success", { message: `${step.verbing} with ${tool} successfully` });
|
|
1221
|
-
} catch (caughtError) {
|
|
1222
|
-
await hooks.callHook("kubb:error", { error: new Error(command.errorMessage, { cause: caughtError }) });
|
|
1223
|
-
signal?.throwIfAborted();
|
|
1224
|
-
}
|
|
1225
|
-
await hooks.callHook(`kubb:${step.kind}:end`);
|
|
1226
|
-
}
|
|
1227
|
-
if (config.output.postGenerate?.length) {
|
|
1228
|
-
await hooks.callHook("kubb:hooks:start");
|
|
1229
|
-
for (const entry of config.output.postGenerate) {
|
|
1230
|
-
const line = typeof entry === "string" ? entry : entry.command;
|
|
1231
|
-
const [cmd, ...args] = tokenize(line);
|
|
1232
|
-
if (!cmd) continue;
|
|
1233
|
-
try {
|
|
1234
|
-
await runHook({
|
|
1235
|
-
hooks,
|
|
1236
|
-
id: line,
|
|
1237
|
-
command: cmd,
|
|
1238
|
-
args
|
|
1239
|
-
});
|
|
1240
|
-
} catch (error) {
|
|
1241
|
-
await hooks.callHook("kubb:error", { error: toError(error) });
|
|
1242
|
-
throw error;
|
|
1243
|
-
}
|
|
1244
|
-
await hooks.callHook("kubb:success", { message: `${line} successfully executed` });
|
|
1245
|
-
}
|
|
1246
|
-
await hooks.callHook("kubb:hooks:end");
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
async function runGenerationOperation({ config, hooks, signal, jobId, store, snapshotRoot, maxSnapshotMb }) {
|
|
1250
|
-
const diskFiles = snapshotRoot ? await listDisk({
|
|
1251
|
-
root: snapshotRoot,
|
|
1252
|
-
outputPath: config.output.path,
|
|
1253
|
-
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
1254
|
-
}) : void 0;
|
|
1255
|
-
const disk = diskFiles ? await store.keep({
|
|
1256
|
-
jobId,
|
|
1257
|
-
source: "disk",
|
|
1258
|
-
files: diskFiles,
|
|
1259
|
-
maxSetMb: maxSnapshotMb
|
|
1260
|
-
}) : void 0;
|
|
1261
|
-
const removeHookListener = setupHookListener(hooks, config.root, signal);
|
|
1262
|
-
try {
|
|
1263
|
-
await generate({
|
|
1264
|
-
config,
|
|
1265
|
-
hooks,
|
|
1266
|
-
signal
|
|
1267
|
-
});
|
|
1268
|
-
} catch (error) {
|
|
1269
|
-
await store.drop(jobId);
|
|
1270
|
-
throw error;
|
|
1271
|
-
} finally {
|
|
1272
|
-
removeHookListener();
|
|
1273
|
-
}
|
|
1274
|
-
return disk;
|
|
1275
|
-
}
|
|
1276
|
-
//#endregion
|
|
1277
|
-
//#region src/runtime/StudioSession.ts
|
|
1278
|
-
/** A sandbox shares one in-memory generation store across tenants, so old generations expire. */
|
|
1279
1414
|
const SANDBOX_GENERATION_TTL_MS = 9e5;
|
|
1280
1415
|
/**
|
|
1281
|
-
*
|
|
1282
|
-
* shared the agent's own root would read each other's manifest.
|
|
1283
|
-
*/
|
|
1284
|
-
function createJobRoot() {
|
|
1285
|
-
return (0, node_fs_promises.mkdtemp)(node_path.default.join((0, node_os.tmpdir)(), "kubb-job-"));
|
|
1286
|
-
}
|
|
1287
|
-
/**
|
|
1288
|
-
* Removes a job root and the manifest cache Kubb derived from it. Best effort: a leftover temp
|
|
1289
|
-
* directory must not fail a job that already finished.
|
|
1416
|
+
* Studio closed with {@link AgentCloseCode.UNAUTHORIZED}. Hosts forget the token and pair again.
|
|
1290
1417
|
*/
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
}), (0, node_fs_promises.rm)((0, _kubb_core.resolveCacheDir)(jobRoot), {
|
|
1296
|
-
recursive: true,
|
|
1297
|
-
force: true
|
|
1298
|
-
})]).catch(() => {});
|
|
1299
|
-
}
|
|
1300
|
-
var GenerationRunTarget = class extends capnweb.RpcTarget {
|
|
1301
|
-
generationStream;
|
|
1302
|
-
generationResult;
|
|
1303
|
-
cancelGeneration;
|
|
1304
|
-
stopGeneration;
|
|
1305
|
-
constructor(generationStream, generationResult, cancelGeneration, stopGeneration) {
|
|
1306
|
-
super();
|
|
1307
|
-
this.generationStream = generationStream;
|
|
1308
|
-
this.generationResult = generationResult;
|
|
1309
|
-
this.cancelGeneration = cancelGeneration;
|
|
1310
|
-
this.stopGeneration = stopGeneration;
|
|
1311
|
-
}
|
|
1312
|
-
async events() {
|
|
1313
|
-
return this.generationStream;
|
|
1314
|
-
}
|
|
1315
|
-
result() {
|
|
1316
|
-
return this.generationResult;
|
|
1317
|
-
}
|
|
1318
|
-
cancel() {
|
|
1319
|
-
return this.cancelGeneration();
|
|
1320
|
-
}
|
|
1321
|
-
[Symbol.dispose]() {
|
|
1322
|
-
this.stopGeneration();
|
|
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";
|
|
1323
1422
|
}
|
|
1324
1423
|
};
|
|
1325
|
-
/**
|
|
1326
|
-
*
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
configFile: node_path.default.resolve(root, options.configPath),
|
|
1337
|
-
permissions: {
|
|
1338
|
-
allowWrite: false,
|
|
1339
|
-
allowConfigEdit: false,
|
|
1340
|
-
allowExec: false,
|
|
1341
|
-
allowRead: false,
|
|
1342
|
-
...options.permissions
|
|
1343
|
-
},
|
|
1344
|
-
retryInterval: options.retryInterval ?? agentDefaults.retryIntervalMs,
|
|
1345
|
-
heartbeatInterval: Math.min(options.heartbeatInterval ?? agentDefaults.heartbeatIntervalMs, agentDefaults.maxHeartbeatIntervalMs),
|
|
1346
|
-
capacity: {
|
|
1347
|
-
...resolveAgentCapacity(),
|
|
1348
|
-
...options.capacity
|
|
1349
|
-
},
|
|
1350
|
-
instanceId: options.instanceId ?? (0, node_crypto.randomUUID)()
|
|
1351
|
-
};
|
|
1352
|
-
}
|
|
1353
|
-
/**
|
|
1354
|
-
* Jobs one agent process can run at once today. Two runs would share this session's hook emitter,
|
|
1355
|
-
* and with it each other's events, so the connection keeps this limit.
|
|
1356
|
-
*/
|
|
1357
|
-
const RUNTIME_MAX_CONCURRENT = 1;
|
|
1358
|
-
const MB = 1048576;
|
|
1359
|
-
function rssMb() {
|
|
1360
|
-
return node_process.default.memoryUsage().rss / MB;
|
|
1361
|
-
}
|
|
1362
|
-
/**
|
|
1363
|
-
* Reads what Studio meant by closing the connection. A code Studio did not send on purpose is an
|
|
1364
|
-
* ordinary drop, and the agent reconnects as it always has.
|
|
1365
|
-
*/
|
|
1366
|
-
function planEnd(close) {
|
|
1367
|
-
const code = close?.code;
|
|
1368
|
-
if (code === require_protocol.AgentCloseCode.REAUTHENTICATE) return {
|
|
1369
|
-
reason: "Kubb Studio asked the agent to register again",
|
|
1370
|
-
retry: true
|
|
1371
|
-
};
|
|
1372
|
-
if (code === require_protocol.AgentCloseCode.SUPERSEDED) return {
|
|
1373
|
-
reason: "another instance of this agent took over",
|
|
1374
|
-
retry: false
|
|
1375
|
-
};
|
|
1376
|
-
if (code === require_protocol.AgentCloseCode.INCOMPATIBLE) return {
|
|
1377
|
-
reason: "this agent is too old for Kubb Studio, or was deleted",
|
|
1378
|
-
retry: false,
|
|
1379
|
-
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.")
|
|
1380
|
-
};
|
|
1381
|
-
return {
|
|
1382
|
-
reason: "connection closed",
|
|
1383
|
-
retry: true
|
|
1384
|
-
};
|
|
1424
|
+
/**
|
|
1425
|
+
* Studio closed with {@link AgentCloseCode.INCOMPATIBLE}. Only an upgrade helps.
|
|
1426
|
+
*/
|
|
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(() => {});
|
|
1385
1435
|
}
|
|
1386
1436
|
function isValidPage({ cursor, limit }) {
|
|
1387
1437
|
if (limit === void 0) return cursor === void 0;
|
|
1388
1438
|
if (cursor !== void 0 && (!Number.isSafeInteger(cursor) || cursor < 0)) return false;
|
|
1389
1439
|
return Number.isSafeInteger(limit) && limit >= 1 && limit <= 2e3;
|
|
1390
1440
|
}
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1441
|
+
var GenerationRunTarget = class extends capnweb.RpcTarget {
|
|
1442
|
+
#run;
|
|
1443
|
+
constructor(run) {
|
|
1444
|
+
super();
|
|
1445
|
+
this.#run = run;
|
|
1446
|
+
}
|
|
1447
|
+
async events() {
|
|
1448
|
+
return this.#run.stream;
|
|
1449
|
+
}
|
|
1450
|
+
result() {
|
|
1451
|
+
return this.#run.result;
|
|
1452
|
+
}
|
|
1453
|
+
async cancel() {
|
|
1454
|
+
this.#run.stop();
|
|
1455
|
+
}
|
|
1456
|
+
[Symbol.dispose]() {
|
|
1457
|
+
this.#run.stop();
|
|
1458
|
+
}
|
|
1459
|
+
};
|
|
1460
|
+
var Agent = class extends capnweb.RpcTarget {
|
|
1396
1461
|
#options;
|
|
1397
|
-
#
|
|
1398
|
-
/**
|
|
1399
|
-
* Removers for every listener this session added (socket, shutdown signal, hooks) so `dispose`
|
|
1400
|
-
* detaches them in one pass. Listeners a host attached itself through `installLogger` survive.
|
|
1401
|
-
*/
|
|
1402
|
-
#unhooks = [];
|
|
1403
|
-
/**
|
|
1404
|
-
* What registration handed back, and the marker for whether the agent registered at all.
|
|
1405
|
-
* Before it resolves there is no sandbox flag to read.
|
|
1406
|
-
*/
|
|
1407
|
-
#registration;
|
|
1408
|
-
#rpc;
|
|
1409
|
-
#studioVersion;
|
|
1410
|
-
#disposed = false;
|
|
1411
|
-
#isGenerating = false;
|
|
1412
|
-
#activeJob;
|
|
1413
|
-
#heartbeatTimer;
|
|
1414
|
-
#lastGeneration;
|
|
1415
|
-
#store;
|
|
1462
|
+
#session;
|
|
1416
1463
|
#limits = resolveGenerationLimits();
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
#ended = Promise.withResolvers();
|
|
1423
|
-
closed = this.#ended.promise;
|
|
1424
|
-
#generationResult;
|
|
1425
|
-
constructor(options) {
|
|
1426
|
-
this.#options = applyStudioDefaults(options);
|
|
1427
|
-
this.#connectAck.promise.catch(() => {});
|
|
1464
|
+
#store;
|
|
1465
|
+
constructor({ options, session }) {
|
|
1466
|
+
super();
|
|
1467
|
+
this.#options = options;
|
|
1468
|
+
this.#session = session;
|
|
1428
1469
|
}
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
get #
|
|
1433
|
-
|
|
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;
|
|
1434
1481
|
}
|
|
1435
|
-
/**
|
|
1436
|
-
* Kept in the project's cache directory, so it survives a restart, except on a sandbox: its pool
|
|
1437
|
-
* sessions run every tenant's jobs, so it keeps them in memory. Looked up by job id only.
|
|
1438
|
-
*/
|
|
1439
1482
|
get #generations() {
|
|
1440
1483
|
this.#store ??= createGenerationStore({
|
|
1441
|
-
storage: this.#
|
|
1484
|
+
storage: this.#sandbox ? (0, _kubb_core.memoryStorage)() : (0, _kubb_core.cacheStorage)({ root: this.#options.root }),
|
|
1442
1485
|
maxCount: this.#limits.maxCount,
|
|
1443
1486
|
maxMb: this.#limits.maxMb,
|
|
1444
|
-
ttlMs: this.#
|
|
1487
|
+
ttlMs: this.#sandbox ? SANDBOX_GENERATION_TTL_MS : void 0
|
|
1445
1488
|
});
|
|
1446
1489
|
return this.#store;
|
|
1447
1490
|
}
|
|
1448
|
-
|
|
1449
|
-
return
|
|
1450
|
-
}
|
|
1451
|
-
get #canEditConfig() {
|
|
1452
|
-
return !this.#isSandbox && this.#options.permissions.allowConfigEdit;
|
|
1453
|
-
}
|
|
1454
|
-
/**
|
|
1455
|
-
* A sandbox agent always allows reading its output back; a local agent only when opted in.
|
|
1456
|
-
*/
|
|
1457
|
-
get #canRead() {
|
|
1458
|
-
return this.#isSandbox || this.#options.permissions.allowRead;
|
|
1459
|
-
}
|
|
1460
|
-
async start() {
|
|
1461
|
-
const { token, studioUrl, signal, heartbeatInterval, installLogger, instanceId, capacity } = this.#options;
|
|
1462
|
-
await installLogger?.(this.#hooks);
|
|
1463
|
-
signal?.addEventListener("abort", this.#onAbort, { once: true });
|
|
1464
|
-
this.#unhooks.push(() => signal?.removeEventListener("abort", this.#onAbort));
|
|
1465
|
-
try {
|
|
1466
|
-
signal?.throwIfAborted();
|
|
1467
|
-
await this.#hooks.callHook("studio:connecting", { url: studioUrl });
|
|
1468
|
-
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`);
|
|
1469
|
-
const registration = await registerAgent({
|
|
1470
|
-
signal,
|
|
1471
|
-
token,
|
|
1472
|
-
studioUrl,
|
|
1473
|
-
instanceId,
|
|
1474
|
-
capacity: {
|
|
1475
|
-
...capacity,
|
|
1476
|
-
maxConcurrent: Math.min(capacity.maxConcurrent, RUNTIME_MAX_CONCURRENT)
|
|
1477
|
-
}
|
|
1478
|
-
});
|
|
1479
|
-
signal?.throwIfAborted();
|
|
1480
|
-
this.#registration = registration;
|
|
1481
|
-
this.#studioVersion = registration.version;
|
|
1482
|
-
const rpc = await (this.#options.connector ?? connectWebSocketRpc)({
|
|
1483
|
-
url: registration.socketUrl,
|
|
1484
|
-
token,
|
|
1485
|
-
instanceId,
|
|
1486
|
-
local: this
|
|
1487
|
-
});
|
|
1488
|
-
this.#rpc = rpc;
|
|
1489
|
-
rpc.closed.then(this.#onClose);
|
|
1490
|
-
if (signal?.aborted || this.#disposed) {
|
|
1491
|
-
rpc.close();
|
|
1492
|
-
throw new Error("Connection stopped during startup");
|
|
1493
|
-
}
|
|
1494
|
-
this.#scheduleHeartbeat(heartbeatInterval);
|
|
1495
|
-
await this.#hooks.callHook("studio:connected", {
|
|
1496
|
-
url: studioUrl,
|
|
1497
|
-
versions: {
|
|
1498
|
-
studio: this.#studioVersion,
|
|
1499
|
-
kubb: version,
|
|
1500
|
-
agent: this.#options.version
|
|
1501
|
-
},
|
|
1502
|
-
agentSlug: registration.agentSlug,
|
|
1503
|
-
organizationSlug: registration.organizationSlug
|
|
1504
|
-
});
|
|
1505
|
-
await this.#connectAck.promise;
|
|
1506
|
-
await this.#hooks.callHook("studio:ready", {});
|
|
1507
|
-
} catch (error) {
|
|
1508
|
-
await this.dispose();
|
|
1509
|
-
if (!signal?.aborted) await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
1510
|
-
throw error;
|
|
1511
|
-
}
|
|
1512
|
-
}
|
|
1513
|
-
reportRetry(delayMs) {
|
|
1514
|
-
return this.#hooks.callHook("studio:reconnecting", { delayMs });
|
|
1515
|
-
}
|
|
1516
|
-
#warn(message, permission) {
|
|
1517
|
-
return this.#hooks.callHook("studio:warn", {
|
|
1491
|
+
#warn({ message, permission }) {
|
|
1492
|
+
return emit(this.#options.hooks, "studio:warn", {
|
|
1518
1493
|
message,
|
|
1519
1494
|
permission
|
|
1520
1495
|
});
|
|
1521
1496
|
}
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
await this.#warn(reason, permission);
|
|
1497
|
+
async #refuse({ message, permission }) {
|
|
1498
|
+
await this.#warn({
|
|
1499
|
+
message,
|
|
1500
|
+
permission
|
|
1501
|
+
});
|
|
1528
1502
|
throw new Error(message);
|
|
1529
1503
|
}
|
|
1530
|
-
#
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
return;
|
|
1539
|
-
}
|
|
1540
|
-
if (this.#rpc === rpc) this.#scheduleHeartbeat(interval);
|
|
1541
|
-
}, interval);
|
|
1542
|
-
}
|
|
1543
|
-
/**
|
|
1544
|
-
* Races `studio.ping()` against a deadline, so a half-open socket can't hang it forever.
|
|
1545
|
-
* */
|
|
1546
|
-
async #ping(rpc) {
|
|
1547
|
-
const { promise: timedOut, reject: onTimeout } = Promise.withResolvers();
|
|
1548
|
-
const timer = setTimeout(() => onTimeout(/* @__PURE__ */ new Error("Heartbeat ping timed out")), agentDefaults.heartbeatTimeoutMs);
|
|
1549
|
-
try {
|
|
1550
|
-
const load = await Promise.race([this.#load().catch(() => void 0), timedOut]);
|
|
1551
|
-
await Promise.race([rpc.studio.ping(load), timedOut]);
|
|
1552
|
-
} finally {
|
|
1553
|
-
clearTimeout(timer);
|
|
1554
|
-
}
|
|
1555
|
-
}
|
|
1556
|
-
async #load() {
|
|
1557
|
-
return {
|
|
1558
|
-
running: this.#isGenerating ? 1 : 0,
|
|
1559
|
-
rssMb: Math.round(rssMb()),
|
|
1560
|
-
storeBytes: await this.#generations.bytes(),
|
|
1561
|
-
accepting: true
|
|
1562
|
-
};
|
|
1504
|
+
async #command({ command, run }) {
|
|
1505
|
+
if (!this.#session.info) throw new Error("Studio must call connect() before any other command");
|
|
1506
|
+
const { value, info } = await run();
|
|
1507
|
+
if (info !== void 0) await emit(this.#options.hooks, "studio:command", {
|
|
1508
|
+
command,
|
|
1509
|
+
info
|
|
1510
|
+
});
|
|
1511
|
+
return value;
|
|
1563
1512
|
}
|
|
1564
|
-
/**
|
|
1565
|
-
* Reads `kubb.config.ts` and reports which plugin options Studio may edit.
|
|
1566
|
-
*
|
|
1567
|
-
* Skipped when the host did not grant `allowConfigEdit`. Not cached: the user can edit the file
|
|
1568
|
-
* between two Studio actions.
|
|
1569
|
-
*/
|
|
1570
1513
|
async #readConfigFileView(source) {
|
|
1571
|
-
if (!this.#
|
|
1514
|
+
if (!this.#permissions.allowConfigEdit) return void 0;
|
|
1572
1515
|
try {
|
|
1573
|
-
|
|
1574
|
-
return readConfig(source ?? await require_resolveConfig.read(this.#options.configFile));
|
|
1516
|
+
return readConfig(source ?? await read(this.#options.configFile));
|
|
1575
1517
|
} catch (error) {
|
|
1576
|
-
await this.#warn(`Could not read ${this.#options.configFile}: ${getErrorMessage(error)}`);
|
|
1518
|
+
await this.#warn({ message: `Could not read ${this.#options.configFile}: ${getErrorMessage(error)}` });
|
|
1577
1519
|
return;
|
|
1578
1520
|
}
|
|
1579
1521
|
}
|
|
1580
|
-
async connect() {
|
|
1581
|
-
|
|
1522
|
+
async connect(info) {
|
|
1523
|
+
this.#session.info = info;
|
|
1524
|
+
const { configPath, root, version: version$1, loadConfig } = this.#options;
|
|
1582
1525
|
const [config, file] = await Promise.all([loadConfig(), this.#readConfigFileView()]);
|
|
1583
|
-
|
|
1526
|
+
this.#session.connected.resolve(info);
|
|
1527
|
+
return {
|
|
1584
1528
|
versions: {
|
|
1585
1529
|
kubb: version,
|
|
1586
1530
|
agent: version$1
|
|
@@ -1590,176 +1534,135 @@ var StudioSession = class {
|
|
|
1590
1534
|
path: configPath,
|
|
1591
1535
|
file,
|
|
1592
1536
|
plugins: config.plugins.map((plugin) => ({
|
|
1593
|
-
name:
|
|
1537
|
+
name: toPackageName(plugin.name),
|
|
1594
1538
|
options: plugin.options ?? {}
|
|
1595
1539
|
}))
|
|
1596
1540
|
},
|
|
1597
|
-
permissions:
|
|
1598
|
-
...permissions,
|
|
1599
|
-
allowWrite: this.#canWrite,
|
|
1600
|
-
allowConfigEdit: this.#canEditConfig,
|
|
1601
|
-
allowRead: this.#canRead
|
|
1602
|
-
}
|
|
1541
|
+
permissions: this.#permissions
|
|
1603
1542
|
};
|
|
1604
|
-
this.#connectAck.resolve();
|
|
1605
|
-
return payload;
|
|
1606
|
-
}
|
|
1607
|
-
#onAbort = () => void this.#end({
|
|
1608
|
-
reason: "shutdown",
|
|
1609
|
-
retry: false
|
|
1610
|
-
}).catch(() => {});
|
|
1611
|
-
#onClose = (close) => void this.#end(planEnd(close)).catch(() => {});
|
|
1612
|
-
/** Closes this attempt and waits for its generation to stop before another can start. */
|
|
1613
|
-
dispose() {
|
|
1614
|
-
return this.#end({
|
|
1615
|
-
reason: "shutdown",
|
|
1616
|
-
retry: false
|
|
1617
|
-
});
|
|
1618
|
-
}
|
|
1619
|
-
async #end(plan) {
|
|
1620
|
-
if (this.#disposed) {
|
|
1621
|
-
await this.closed;
|
|
1622
|
-
return;
|
|
1623
|
-
}
|
|
1624
|
-
this.#disposed = true;
|
|
1625
|
-
clearTimeout(this.#heartbeatTimer);
|
|
1626
|
-
this.#rpc?.close();
|
|
1627
|
-
this.#rpc = void 0;
|
|
1628
|
-
this.#connectAck.reject(/* @__PURE__ */ new Error("Session ended before Studio called connect()"));
|
|
1629
|
-
for (const unhook of this.#unhooks) unhook();
|
|
1630
|
-
this.#unhooks.length = 0;
|
|
1631
|
-
try {
|
|
1632
|
-
await this.#activeJob?.cancel();
|
|
1633
|
-
await this.#generationResult?.catch(() => {});
|
|
1634
|
-
await this.#hooks.callHook("studio:disconnected", { reason: plan.reason });
|
|
1635
|
-
if (plan.error) await this.#hooks.callHook("studio:error", { error: plan.error });
|
|
1636
|
-
} finally {
|
|
1637
|
-
this.#ended.resolve(plan);
|
|
1638
|
-
}
|
|
1639
1543
|
}
|
|
1640
|
-
startGeneration(
|
|
1641
|
-
const
|
|
1642
|
-
this.#lastGeneration = result;
|
|
1643
|
-
} });
|
|
1544
|
+
startGeneration(input) {
|
|
1545
|
+
const stream = createGenerationStream(this.#options.hooks, input.jobId);
|
|
1644
1546
|
const controller = new AbortController();
|
|
1645
|
-
const
|
|
1646
|
-
|
|
1647
|
-
};
|
|
1648
|
-
const result = this.#runGeneration(data, controller, cancelRun).then(async (value) => {
|
|
1649
|
-
await generationStream.close();
|
|
1650
|
-
return value;
|
|
1651
|
-
}).catch((error) => {
|
|
1652
|
-
generationStream.fail(error);
|
|
1547
|
+
const result = this.#runGeneration(input, controller).then((value) => stream.close().then(() => value), (error) => {
|
|
1548
|
+
stream.fail(error);
|
|
1653
1549
|
throw error;
|
|
1654
|
-
}).finally(() => {
|
|
1655
|
-
if (this.#activeJob?.cancel === cancelRun) this.#activeJob = void 0;
|
|
1656
1550
|
});
|
|
1657
|
-
if (this.#activeJob?.cancel === cancelRun) this.#generationResult = result;
|
|
1658
1551
|
result.catch(() => {});
|
|
1659
|
-
return new GenerationRunTarget(
|
|
1660
|
-
|
|
1661
|
-
|
|
1552
|
+
return new GenerationRunTarget({
|
|
1553
|
+
stream: stream.stream,
|
|
1554
|
+
result,
|
|
1555
|
+
stop: () => {
|
|
1556
|
+
controller.abort(/* @__PURE__ */ new Error("Generation canceled"));
|
|
1557
|
+
stream.dispose();
|
|
1558
|
+
}
|
|
1662
1559
|
});
|
|
1663
1560
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
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");
|
|
1673
|
-
this.#isGenerating = true;
|
|
1674
|
-
this.#activeJob = {
|
|
1675
|
-
jobId: data.jobId,
|
|
1676
|
-
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
|
|
1677
1569
|
};
|
|
1678
|
-
const command = "generate";
|
|
1679
|
-
const { loadConfig, permissions } = this.#options;
|
|
1680
1570
|
let root = this.#options.root;
|
|
1681
1571
|
try {
|
|
1682
|
-
if (this.#
|
|
1683
|
-
await this.#
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
const adapter = await require_resolveConfig.mergeAdapter(config.adapter, patch?.adapter);
|
|
1688
|
-
const inputOverride = this.#isSandbox ? patch?.input ?? "" : void 0;
|
|
1689
|
-
if (permissions.allowWrite && this.#isSandbox) await this.#warn("Running in a sandbox, so writing files is disabled");
|
|
1690
|
-
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");
|
|
1691
|
-
const resolvedPlugins = plugins ?? config.plugins;
|
|
1692
|
-
this.#lastGeneration = void 0;
|
|
1693
|
-
const disk = await runGenerationOperation({
|
|
1694
|
-
config: {
|
|
1695
|
-
...config,
|
|
1572
|
+
if (this.#sandbox) root = await (0, node_fs_promises.mkdtemp)(node_path.default.join((0, node_os.tmpdir)(), "kubb-job-"));
|
|
1573
|
+
return await this.#command({
|
|
1574
|
+
command: "generate",
|
|
1575
|
+
run: () => this.#generate({
|
|
1576
|
+
input,
|
|
1696
1577
|
root,
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
output: permissions.allowExec ? { ...config.output } : {
|
|
1700
|
-
...config.output,
|
|
1701
|
-
format: false,
|
|
1702
|
-
lint: false,
|
|
1703
|
-
postGenerate: []
|
|
1704
|
-
},
|
|
1705
|
-
plugins: resolvedPlugins,
|
|
1706
|
-
adapter
|
|
1707
|
-
},
|
|
1708
|
-
hooks: this.#hooks,
|
|
1709
|
-
signal: controller.signal,
|
|
1710
|
-
jobId: data.jobId,
|
|
1711
|
-
store: this.#generations,
|
|
1712
|
-
snapshotRoot: this.#hasProjectOnDisk ? root : void 0,
|
|
1713
|
-
maxSnapshotMb: this.#limits.maxSnapshotMb
|
|
1578
|
+
signal: controller.signal
|
|
1579
|
+
})
|
|
1714
1580
|
});
|
|
1715
|
-
await this.#hooks.callHook("studio:command:end", {
|
|
1716
|
-
command,
|
|
1717
|
-
info: `${resolvedPlugins.length} plugin${resolvedPlugins.length === 1 ? "" : "s"}, ${this.#canWrite ? "written to disk" : "in memory"}${inputOverride !== void 0 ? ", from a Studio spec" : ""}`
|
|
1718
|
-
});
|
|
1719
|
-
const generation = this.#lastGeneration;
|
|
1720
|
-
const output = generation ? await this.#generations.keep({
|
|
1721
|
-
jobId: data.jobId,
|
|
1722
|
-
source: "output",
|
|
1723
|
-
files: generation.output,
|
|
1724
|
-
maxSetMb: this.#limits.maxMb
|
|
1725
|
-
}) : void 0;
|
|
1726
|
-
if (generation && output) {
|
|
1727
|
-
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");
|
|
1728
|
-
const { peerDependencies, missingDependencies } = generation;
|
|
1729
|
-
await this.#generations.add({
|
|
1730
|
-
jobId: data.jobId,
|
|
1731
|
-
output,
|
|
1732
|
-
disk,
|
|
1733
|
-
peerDependencies,
|
|
1734
|
-
missingDependencies
|
|
1735
|
-
});
|
|
1736
|
-
}
|
|
1737
|
-
const files = [...generation?.output.paths ?? []];
|
|
1738
|
-
return {
|
|
1739
|
-
status: "success",
|
|
1740
|
-
files,
|
|
1741
|
-
fileCount: files.length,
|
|
1742
|
-
hashes: output?.hashes ?? {},
|
|
1743
|
-
disk: disk ? { hashes: disk.hashes } : void 0
|
|
1744
|
-
};
|
|
1745
1581
|
} finally {
|
|
1746
|
-
this.#
|
|
1747
|
-
|
|
1748
|
-
|
|
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();
|
|
1749
1591
|
}
|
|
1750
1592
|
}
|
|
1751
|
-
async
|
|
1752
|
-
const
|
|
1753
|
-
await this.#
|
|
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({
|
|
1601
|
+
root,
|
|
1602
|
+
outputPath: config.output.path,
|
|
1603
|
+
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
1604
|
+
}) : void 0;
|
|
1605
|
+
const disk = diskFiles ? await this.#generations.keep({
|
|
1606
|
+
jobId,
|
|
1607
|
+
source: "disk",
|
|
1608
|
+
files: diskFiles,
|
|
1609
|
+
maxSetMb: this.#limits.maxSnapshotMb
|
|
1610
|
+
}) : void 0;
|
|
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
|
|
1619
|
+
},
|
|
1620
|
+
hooks: this.#options.hooks,
|
|
1621
|
+
signal,
|
|
1622
|
+
allowExec: this.#permissions.allowExec
|
|
1623
|
+
}).catch(async (error) => {
|
|
1624
|
+
await this.#generations.drop(jobId);
|
|
1625
|
+
throw error;
|
|
1626
|
+
});
|
|
1627
|
+
const output = await this.#generations.keep({
|
|
1628
|
+
jobId,
|
|
1629
|
+
source: "output",
|
|
1630
|
+
files,
|
|
1631
|
+
maxSetMb: this.#limits.maxMb
|
|
1632
|
+
});
|
|
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" });
|
|
1634
|
+
await this.#generations.add({
|
|
1635
|
+
jobId,
|
|
1636
|
+
output,
|
|
1637
|
+
disk
|
|
1638
|
+
});
|
|
1639
|
+
return {
|
|
1640
|
+
value: {
|
|
1641
|
+
status: "success",
|
|
1642
|
+
files: [...files.paths],
|
|
1643
|
+
fileCount: files.paths.size,
|
|
1644
|
+
hashes: output.hashes,
|
|
1645
|
+
disk: disk ? { hashes: disk.hashes } : void 0
|
|
1646
|
+
},
|
|
1647
|
+
info: `${plugins.length} plugin${plugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "in memory"}${studioInput !== void 0 ? ", from a Studio spec" : ""}`
|
|
1648
|
+
};
|
|
1649
|
+
}
|
|
1650
|
+
saveConfig(input) {
|
|
1651
|
+
return this.#command({
|
|
1652
|
+
command: "saveConfig",
|
|
1653
|
+
run: () => this.#saveConfig(input)
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
async #saveConfig(input) {
|
|
1754
1657
|
const { configPath, configFile } = this.#options;
|
|
1755
|
-
if (!Array.isArray(
|
|
1756
|
-
await this.#warn("Ignored save: the message carried no edits");
|
|
1757
|
-
return {
|
|
1658
|
+
if (!Array.isArray(input.edits)) {
|
|
1659
|
+
await this.#warn({ message: "Ignored save: the message carried no edits" });
|
|
1660
|
+
return { value: {
|
|
1758
1661
|
outcomes: [],
|
|
1759
1662
|
changed: false
|
|
1760
|
-
};
|
|
1663
|
+
} };
|
|
1761
1664
|
}
|
|
1762
|
-
const edits =
|
|
1665
|
+
const edits = input.edits;
|
|
1763
1666
|
const refuse = (reason) => ({
|
|
1764
1667
|
outcomes: edits.map((edit) => ({
|
|
1765
1668
|
edit,
|
|
@@ -1768,197 +1671,215 @@ var StudioSession = class {
|
|
|
1768
1671
|
})),
|
|
1769
1672
|
changed: false
|
|
1770
1673
|
});
|
|
1771
|
-
if (!this.#
|
|
1772
|
-
await this.#warn("Ignored save: editing kubb.config.ts was not granted");
|
|
1773
|
-
return refuse("the agent was not granted permission to edit kubb.config.ts");
|
|
1674
|
+
if (!this.#permissions.allowConfigEdit) {
|
|
1675
|
+
await this.#warn({ message: "Ignored save: editing kubb.config.ts was not granted" });
|
|
1676
|
+
return { value: refuse("the agent was not granted permission to edit kubb.config.ts") };
|
|
1774
1677
|
}
|
|
1775
|
-
if (this.#
|
|
1678
|
+
if (this.#session.active) return { value: refuse("a generation is in progress") };
|
|
1776
1679
|
try {
|
|
1777
|
-
const {
|
|
1778
|
-
const { source: patched, outcomes, changed } = await writeConfigEdits({
|
|
1680
|
+
const { source, outcomes, changed } = await writeConfigEdits({
|
|
1779
1681
|
filePath: configFile,
|
|
1780
1682
|
edits
|
|
1781
1683
|
});
|
|
1782
1684
|
const applied = outcomes.filter((outcome) => outcome.applied).length;
|
|
1783
|
-
await this.#hooks.callHook("studio:command:end", {
|
|
1784
|
-
command,
|
|
1785
|
-
info: `applied ${applied}/${outcomes.length} edits to ${configPath}`
|
|
1786
|
-
});
|
|
1787
|
-
return {
|
|
1788
|
-
outcomes,
|
|
1789
|
-
changed,
|
|
1790
|
-
file: changed ? await this.#readConfigFileView(patched) : void 0
|
|
1791
|
-
};
|
|
1792
|
-
} catch (error) {
|
|
1793
|
-
await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
1794
|
-
return refuse(getErrorMessage(error));
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
async publishSnapshot(data) {
|
|
1798
|
-
const command = "snapshot";
|
|
1799
|
-
await this.#hooks.callHook("studio:command:start", { command });
|
|
1800
|
-
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");
|
|
1801
|
-
const { name, version, bundledDependencies, uploadPath } = data;
|
|
1802
|
-
if (!name || !version || !uploadPath) return this.#refuse("Ignored snapshot: the message was missing required fields", "The request was missing required fields");
|
|
1803
|
-
const generation = await this.#generations.latest();
|
|
1804
|
-
if (!generation) return this.#refuse("Ignored snapshot: no prior generation to pack", "No prior generation exists to pack, run a generation first");
|
|
1805
|
-
const bundled = new Set(bundledDependencies ?? []);
|
|
1806
|
-
const missing = generation.missingDependencies.filter((dependency) => !bundled.has(dependency));
|
|
1807
|
-
if (missing.length) return this.#refuse(`Ignored snapshot: missing dependencies: ${missing.join(", ")}`, `Missing dependencies: ${missing.join(", ")}`);
|
|
1808
|
-
try {
|
|
1809
|
-
const { createSnapshotPackage, uploadSnapshot } = await Promise.resolve().then(() => require("./snapshotPackage-BwM3LFmZ.cjs"));
|
|
1810
|
-
const { files } = await this.#generations.read({
|
|
1811
|
-
generation,
|
|
1812
|
-
source: "output",
|
|
1813
|
-
paths: generation.output.paths
|
|
1814
|
-
});
|
|
1815
|
-
const { bytes, integrity } = await createSnapshotPackage(files, {
|
|
1816
|
-
name,
|
|
1817
|
-
version,
|
|
1818
|
-
peerDependencies: generation.peerDependencies
|
|
1819
|
-
});
|
|
1820
|
-
const { token, studioUrl, signal } = this.#options;
|
|
1821
|
-
await uploadSnapshot({
|
|
1822
|
-
bytes,
|
|
1823
|
-
uploadPath,
|
|
1824
|
-
studioUrl,
|
|
1825
|
-
token,
|
|
1826
|
-
shutdown: signal
|
|
1827
|
-
});
|
|
1828
|
-
await this.#hooks.callHook("studio:command:end", {
|
|
1829
|
-
command,
|
|
1830
|
-
info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
|
|
1831
|
-
});
|
|
1832
1685
|
return {
|
|
1833
|
-
|
|
1834
|
-
|
|
1686
|
+
value: {
|
|
1687
|
+
outcomes,
|
|
1688
|
+
changed,
|
|
1689
|
+
file: changed ? await this.#readConfigFileView(source) : void 0
|
|
1690
|
+
},
|
|
1691
|
+
info: `applied ${applied}/${outcomes.length} edits to ${configPath}`
|
|
1835
1692
|
};
|
|
1836
1693
|
} catch (error) {
|
|
1837
|
-
await this.#hooks
|
|
1838
|
-
|
|
1694
|
+
await emit(this.#options.hooks, "studio:error", { error: toError(error) });
|
|
1695
|
+
return { value: refuse(getErrorMessage(error)) };
|
|
1839
1696
|
}
|
|
1840
1697
|
}
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1698
|
+
readFiles(input) {
|
|
1699
|
+
return this.#command({
|
|
1700
|
+
command: "readFiles",
|
|
1701
|
+
run: () => this.#readFiles(input)
|
|
1702
|
+
});
|
|
1844
1703
|
}
|
|
1845
|
-
async readFiles(
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
if (
|
|
1853
|
-
const generation = await this.#generations.get(
|
|
1854
|
-
if (!generation) return this.#refuse(
|
|
1855
|
-
if (!isValidPage(
|
|
1856
|
-
const source =
|
|
1857
|
-
if (!generation[source]) return this.#refuse(
|
|
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" });
|
|
1858
1717
|
const result = await this.#generations.read({
|
|
1859
1718
|
generation,
|
|
1860
1719
|
source,
|
|
1861
1720
|
paths,
|
|
1862
|
-
cursor:
|
|
1863
|
-
limit:
|
|
1721
|
+
cursor: input.cursor,
|
|
1722
|
+
limit: input.limit
|
|
1864
1723
|
});
|
|
1865
|
-
|
|
1866
|
-
|
|
1724
|
+
return {
|
|
1725
|
+
value: result,
|
|
1867
1726
|
info: `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`
|
|
1868
|
-
}
|
|
1869
|
-
return result;
|
|
1727
|
+
};
|
|
1870
1728
|
}
|
|
1871
1729
|
};
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
const
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
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
|
+
});
|
|
1888
1747
|
});
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
await session.dispose();
|
|
1899
|
-
const end = await session.closed;
|
|
1900
|
-
if (!end.retry && end.reason !== "shutdown") return "stopped";
|
|
1901
|
-
if (error instanceof InvalidAgentTokenError) {
|
|
1902
|
-
const next = await onTokenRejected({
|
|
1903
|
-
error,
|
|
1904
|
-
credentials: current,
|
|
1905
|
-
live
|
|
1906
|
-
});
|
|
1907
|
-
if (!next) return "stopped";
|
|
1908
|
-
current = next;
|
|
1909
|
-
options = clientOptions(current);
|
|
1910
|
-
live = false;
|
|
1911
|
-
attempt = 0;
|
|
1912
|
-
continue;
|
|
1913
|
-
}
|
|
1914
|
-
} finally {
|
|
1915
|
-
await session.dispose();
|
|
1916
|
-
}
|
|
1917
|
-
onStarted?.();
|
|
1918
|
-
const cap = Math.min(1e3 * 2 ** Math.min(attempt++, 16), options.retryInterval ?? agentDefaults.retryIntervalMs);
|
|
1919
|
-
const delayMs = Math.random() * cap;
|
|
1920
|
-
await session.reportRetry(delayMs);
|
|
1921
|
-
try {
|
|
1922
|
-
await (0, node_timers_promises.setTimeout)(delayMs, void 0, { signal });
|
|
1923
|
-
} catch (error) {
|
|
1924
|
-
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();
|
|
1925
1757
|
}
|
|
1926
|
-
}
|
|
1927
|
-
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
const
|
|
1935
|
-
|
|
1936
|
-
|
|
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;
|
|
1937
1804
|
return {
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
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
|
|
1954
1825
|
},
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
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)
|
|
1958
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
|
+
});
|
|
1959
1880
|
}
|
|
1960
1881
|
//#endregion
|
|
1961
|
-
//#region src/
|
|
1882
|
+
//#region src/pair.ts
|
|
1962
1883
|
/** Labels, not secrets: a person approving the code in the browser is what authorizes a pairing. */
|
|
1963
1884
|
const CLIENT_IDS = {
|
|
1964
1885
|
cli: "kubb-cli",
|
|
@@ -1990,30 +1911,33 @@ var PairingDeniedError = class extends Error {
|
|
|
1990
1911
|
}
|
|
1991
1912
|
};
|
|
1992
1913
|
/**
|
|
1993
|
-
* Asks Studio for a pairing code
|
|
1994
|
-
* the code, so approval knows which machine it is pairing: the same machine pairing twice rotates
|
|
1995
|
-
* one agent's token instead of creating a second agent.
|
|
1914
|
+
* Asks Studio for a pairing code, sending the hashed machine id with it.
|
|
1996
1915
|
*/
|
|
1997
|
-
async function startPairing({ studioUrl =
|
|
1916
|
+
async function startPairing({ studioUrl = defaultStudioUrl, type, name, hostname, machineId, signal }) {
|
|
1998
1917
|
try {
|
|
1999
|
-
|
|
1918
|
+
const pairing = await (0, ofetch.ofetch)(secureUrl({
|
|
1919
|
+
studioUrl,
|
|
1920
|
+
path: "/api/auth/device/code"
|
|
1921
|
+
}).href, {
|
|
2000
1922
|
method: "POST",
|
|
2001
1923
|
body: {
|
|
2002
1924
|
client_id: type === "cli" ? CLIENT_IDS.cli : CLIENT_IDS.agent,
|
|
2003
1925
|
name,
|
|
2004
1926
|
hostname,
|
|
2005
|
-
machine_token:
|
|
1927
|
+
machine_token: (0, node_crypto.hash)("sha256", machineId),
|
|
2006
1928
|
agent_kind: type === "cli" ? void 0 : type
|
|
2007
1929
|
},
|
|
2008
1930
|
signal
|
|
2009
1931
|
});
|
|
1932
|
+
if (typeof pairing?.device_code !== "string") throw new Error("Kubb Studio returned no pairing code");
|
|
1933
|
+
return pairing;
|
|
2010
1934
|
} catch (error) {
|
|
2011
1935
|
if (signal?.aborted) throw new PairingCanceledError();
|
|
2012
1936
|
throw error;
|
|
2013
1937
|
}
|
|
2014
1938
|
}
|
|
2015
1939
|
function isPairingResult(response) {
|
|
2016
|
-
return
|
|
1940
|
+
return "token" in response && typeof response.token === "string";
|
|
2017
1941
|
}
|
|
2018
1942
|
/**
|
|
2019
1943
|
* Polls until the user approves or denies, honoring the server's `slow_down` back-off.
|
|
@@ -2025,7 +1949,7 @@ function isPairingResult(response) {
|
|
|
2025
1949
|
* @throws {PairingDeniedError} when the pairing is denied in the browser.
|
|
2026
1950
|
* @throws {PairingCanceledError} when `signal` aborts.
|
|
2027
1951
|
*/
|
|
2028
|
-
async function pollForPairingToken({ studioUrl =
|
|
1952
|
+
async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, signal, onRetry }) {
|
|
2029
1953
|
const deadline = Date.now() + (session.expires_in > 0 ? session.expires_in : 600) * 1e3;
|
|
2030
1954
|
let intervalMs = (session.interval > 0 ? session.interval : 5) * 1e3;
|
|
2031
1955
|
while (Date.now() < deadline) {
|
|
@@ -2037,19 +1961,24 @@ async function pollForPairingToken({ studioUrl = agentDefaults.studioUrl, sessio
|
|
|
2037
1961
|
}
|
|
2038
1962
|
let response;
|
|
2039
1963
|
try {
|
|
2040
|
-
|
|
1964
|
+
const { status, _data } = await ofetch.ofetch.raw(secureUrl({
|
|
1965
|
+
studioUrl,
|
|
1966
|
+
path: "/api/agent/token"
|
|
1967
|
+
}).href, {
|
|
2041
1968
|
method: "POST",
|
|
2042
1969
|
body: { device_code: session.device_code },
|
|
2043
|
-
|
|
2044
|
-
|
|
1970
|
+
signal,
|
|
1971
|
+
ignoreResponseError: true
|
|
2045
1972
|
});
|
|
1973
|
+
if (status >= 500 || !_data || typeof _data !== "object") throw new Error(`Kubb Studio answered with status ${status}`);
|
|
1974
|
+
response = _data;
|
|
2046
1975
|
} catch (error) {
|
|
2047
1976
|
if (signal?.aborted) throw new PairingCanceledError();
|
|
2048
1977
|
onRetry?.(toError(error));
|
|
2049
1978
|
continue;
|
|
2050
1979
|
}
|
|
2051
1980
|
if (isPairingResult(response)) return response;
|
|
2052
|
-
if (!
|
|
1981
|
+
if (!("error" in response) || typeof response.error !== "string") throw new Error("Kubb Studio returned an empty pairing response, pair again");
|
|
2053
1982
|
if (response.error === "authorization_pending") continue;
|
|
2054
1983
|
if (response.error === "slow_down") {
|
|
2055
1984
|
intervalMs += 5e3;
|
|
@@ -2081,26 +2010,220 @@ async function pairAgent({ onCode, onRetry, maxAttempts = 1, ...options }) {
|
|
|
2081
2010
|
}
|
|
2082
2011
|
}
|
|
2083
2012
|
//#endregion
|
|
2084
|
-
|
|
2085
|
-
|
|
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
|
|
2086
2219
|
exports.IncompatibleAgentError = IncompatibleAgentError;
|
|
2087
2220
|
exports.InvalidAgentTokenError = InvalidAgentTokenError;
|
|
2088
2221
|
exports.PairingCanceledError = PairingCanceledError;
|
|
2089
2222
|
exports.PairingDeniedError = PairingDeniedError;
|
|
2090
2223
|
exports.PairingExpiredError = PairingExpiredError;
|
|
2091
|
-
exports.
|
|
2092
|
-
exports.
|
|
2093
|
-
exports.createClient = createClient;
|
|
2094
|
-
exports.createFileStorage = createFileStorage;
|
|
2095
|
-
exports.createJob = createJob;
|
|
2224
|
+
exports.connectAgent = connectAgent;
|
|
2225
|
+
exports.createSnapshot = createSnapshot;
|
|
2096
2226
|
exports.defaultStudioUrl = defaultStudioUrl;
|
|
2097
|
-
exports.generationEventTypes = require_protocol.generationEventTypes;
|
|
2098
|
-
exports.machineTokenFrom = machineTokenFrom;
|
|
2099
2227
|
exports.pairAgent = pairAgent;
|
|
2100
|
-
exports.pollForPairingToken = pollForPairingToken;
|
|
2101
|
-
exports.runConnection = runConnection;
|
|
2102
|
-
exports.setStorage = setStorage;
|
|
2103
|
-
exports.startPairing = startPairing;
|
|
2104
|
-
exports.waitForJob = waitForJob;
|
|
2105
2228
|
|
|
2106
2229
|
//# sourceMappingURL=index.cjs.map
|