@kubb/studio 5.10.1 → 5.10.3
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 +1 -0
- package/dist/index.cjs +417 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +260 -0
- package/dist/index.d.ts +3 -4
- package/dist/index.js +419 -44
- package/dist/index.js.map +1 -1
- package/dist/protocol-CrMJymjT.d.cts +778 -0
- package/dist/{protocol-C1PwN9fa.d.ts → protocol-CrMJymjT.d.ts} +96 -12
- package/dist/protocol.cjs +34 -10
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +2 -0
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +32 -7
- package/dist/protocol.js.map +1 -1
- package/package.json +28 -10
- package/dist/rolldown-runtime-CRm0XQPb.js +0 -9
package/dist/index.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import "./
|
|
2
|
-
import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, GENERATION_GONE_MESSAGE, MAX_PAGE_LINES, MAX_SNAPSHOT_BYTES, NO_INPUT_MESSAGE, READ_AST_NOT_ALLOWED_MESSAGE, READ_NOT_ALLOWED_MESSAGE, generationEventTypes } from "./protocol.js";
|
|
1
|
+
import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, FileSource, MAX_PAGE_LINES, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
|
|
3
2
|
import { createRequire } from "node:module";
|
|
4
3
|
import { createHash, hash, randomUUID } from "node:crypto";
|
|
5
4
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
@@ -12,10 +11,12 @@ import { existsSync } from "node:fs";
|
|
|
12
11
|
import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, getInputKind, isPackageName, memoryStorage, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
|
|
13
12
|
import { RpcTarget, newWebSocketRpcSession } from "capnweb";
|
|
14
13
|
import { detectCodeFormat, parseExpression, parseModule } from "magicast";
|
|
14
|
+
import { structuredPatch } from "diff";
|
|
15
15
|
import { stripVTControlCharacters } from "node:util";
|
|
16
16
|
import { ast } from "@kubb/ast";
|
|
17
17
|
import { ofetch } from "ofetch";
|
|
18
18
|
import { gzipSync } from "node:zlib";
|
|
19
|
+
//#endregion
|
|
19
20
|
//#region ../../internals/utils/src/casing.ts
|
|
20
21
|
/**
|
|
21
22
|
* Shared implementation for camelCase and PascalCase conversion.
|
|
@@ -153,7 +154,7 @@ function getElapsedMs(hrStart) {
|
|
|
153
154
|
}
|
|
154
155
|
//#endregion
|
|
155
156
|
//#region package.json
|
|
156
|
-
var version = "5.10.
|
|
157
|
+
var version = "5.10.3";
|
|
157
158
|
//#endregion
|
|
158
159
|
//#region src/resolveConfig.ts
|
|
159
160
|
/**
|
|
@@ -255,22 +256,42 @@ function isAllowedPlugin({ packageName, extensions }) {
|
|
|
255
256
|
*
|
|
256
257
|
* @throws if the package cannot be imported or exports no callable factory.
|
|
257
258
|
*/
|
|
258
|
-
async function loadFactory({ packageName, extensions }) {
|
|
259
|
+
async function loadFactory({ packageName, extensions, kind = "plugin" }) {
|
|
260
|
+
const label = {
|
|
261
|
+
plugin: "Plugin",
|
|
262
|
+
adapter: "Adapter",
|
|
263
|
+
parser: "Parser"
|
|
264
|
+
}[kind];
|
|
259
265
|
let mod;
|
|
260
266
|
try {
|
|
261
267
|
mod = await importFromProject(packageName);
|
|
262
268
|
} catch (cause) {
|
|
263
|
-
throw new Error(
|
|
269
|
+
throw new Diagnostics.Error({
|
|
270
|
+
code: kind === "plugin" ? Diagnostics.code.pluginNotFound : Diagnostics.code.unknown,
|
|
271
|
+
severity: "error",
|
|
272
|
+
message: `${label} "${packageName}" could not be loaded. Make sure it is installed: \`npm install ${packageName}\``,
|
|
273
|
+
cause: toError(cause)
|
|
274
|
+
});
|
|
264
275
|
}
|
|
265
276
|
const manifestExport = extensions.get(packageName)?.manifest.export;
|
|
266
277
|
if (manifestExport) {
|
|
267
278
|
if (typeof mod[manifestExport] === "function") return mod[manifestExport];
|
|
268
|
-
throw new Error(
|
|
279
|
+
throw new Diagnostics.Error({
|
|
280
|
+
code: Diagnostics.code.incompatibleExtension,
|
|
281
|
+
severity: "error",
|
|
282
|
+
message: `${label} "${packageName}" does not export "${manifestExport}", the factory its kubb-extension.json names.`,
|
|
283
|
+
help: "Install a version of the plugin whose exports match its kubb-extension.json."
|
|
284
|
+
});
|
|
269
285
|
}
|
|
270
286
|
const exportName = toExportName(packageName);
|
|
271
287
|
if (typeof mod[exportName] === "function") return mod[exportName];
|
|
272
288
|
if (typeof mod["default"] === "function") return mod["default"];
|
|
273
|
-
throw new Error(
|
|
289
|
+
throw new Diagnostics.Error({
|
|
290
|
+
code: Diagnostics.code.incompatibleExtension,
|
|
291
|
+
severity: "error",
|
|
292
|
+
message: `${label} "${packageName}" does not export a callable factory. Tried the named export "${exportName}" and "default".`,
|
|
293
|
+
help: "Install a Kubb plugin package that exports its factory."
|
|
294
|
+
});
|
|
274
295
|
}
|
|
275
296
|
/**
|
|
276
297
|
* Resolves each plugin entry by dynamically importing its package and calling its factory with the
|
|
@@ -291,7 +312,11 @@ async function resolvePlugins({ plugins, extensions = /* @__PURE__ */ new Map()
|
|
|
291
312
|
if (!isAllowedPlugin({
|
|
292
313
|
packageName: name,
|
|
293
314
|
extensions
|
|
294
|
-
})) throw new Error(
|
|
315
|
+
})) throw new Diagnostics.Error({
|
|
316
|
+
code: Diagnostics.code.pluginNotFound,
|
|
317
|
+
severity: "error",
|
|
318
|
+
message: `Plugin "${name}" is not an installed Kubb plugin. Studio loads @kubb/plugin-* packages and installed packages that ship a kubb-extension.json with kind "plugin".`
|
|
319
|
+
});
|
|
295
320
|
return (await loadFactory({
|
|
296
321
|
packageName: name,
|
|
297
322
|
extensions
|
|
@@ -418,7 +443,12 @@ async function mergePlugins({ diskPlugins, studioPlugins, extensions = /* @__PUR
|
|
|
418
443
|
extensions
|
|
419
444
|
}).catch((cause) => {
|
|
420
445
|
if (unchanged) return [diskPlugin];
|
|
421
|
-
throw new Error(
|
|
446
|
+
throw new Diagnostics.Error({
|
|
447
|
+
code: Diagnostics.code.pluginNotFound,
|
|
448
|
+
severity: "error",
|
|
449
|
+
message: `Plugin "${diskPlugin.name}" is not an installed @kubb/plugin-* package or Kubb extension, so Studio cannot change its options. Edit it in the config file, or only enable or disable it from Studio.`,
|
|
450
|
+
cause: toError(cause)
|
|
451
|
+
});
|
|
422
452
|
});
|
|
423
453
|
return resolved ?? diskPlugin;
|
|
424
454
|
}));
|
|
@@ -442,9 +472,15 @@ async function rebuildExtension({ disk, studioOptions, kind, extensions }) {
|
|
|
442
472
|
const { name } = resolveMeta(disk);
|
|
443
473
|
return (await loadFactory({
|
|
444
474
|
packageName: [...extensions.values()].find(({ manifest }) => manifest.kind === kind && manifest.name === name)?.package ?? `@kubb/${kind}-${name}`,
|
|
445
|
-
extensions
|
|
475
|
+
extensions,
|
|
476
|
+
kind
|
|
446
477
|
}).catch((cause) => {
|
|
447
|
-
throw new Error(
|
|
478
|
+
throw new Diagnostics.Error({
|
|
479
|
+
code: kind === "plugin" ? Diagnostics.code.pluginNotFound : Diagnostics.code.unknown,
|
|
480
|
+
severity: "error",
|
|
481
|
+
message: `The ${kind} "${name}" is not an installed Kubb extension, so Studio cannot change its options. Edit it in the config file.`,
|
|
482
|
+
cause: toError(cause)
|
|
483
|
+
});
|
|
448
484
|
}))(mergeOptions(disk.options ?? {}, studioOptions));
|
|
449
485
|
}
|
|
450
486
|
/**
|
|
@@ -521,7 +557,11 @@ function invalidEdit(edit) {
|
|
|
521
557
|
}
|
|
522
558
|
/** Where a node sits in the source text. */
|
|
523
559
|
function rangeOf(node) {
|
|
524
|
-
if (typeof node.start !== "number" || typeof node.end !== "number") throw new Error(
|
|
560
|
+
if (typeof node.start !== "number" || typeof node.end !== "number") throw new Diagnostics.Error({
|
|
561
|
+
code: Diagnostics.code.internalError,
|
|
562
|
+
severity: "error",
|
|
563
|
+
message: "the config file could not be mapped back to its source text"
|
|
564
|
+
});
|
|
525
565
|
return {
|
|
526
566
|
start: node.start,
|
|
527
567
|
end: node.end
|
|
@@ -773,6 +813,44 @@ function disabledBlocks(source, config) {
|
|
|
773
813
|
}];
|
|
774
814
|
});
|
|
775
815
|
}
|
|
816
|
+
/**
|
|
817
|
+
* Reads an object option that holds some code: its literal members as `value`, and the paths of
|
|
818
|
+
* the members that are code as `locked`. `set` can write any path outside `locked`, so Studio
|
|
819
|
+
* edits those fields. `undefined` for a computed key, since it could set any path.
|
|
820
|
+
*/
|
|
821
|
+
function readPartial(node, prefix = []) {
|
|
822
|
+
const value = {};
|
|
823
|
+
const locked = [];
|
|
824
|
+
for (const [index, entry] of node.properties.entries()) {
|
|
825
|
+
if (entry.type === "SpreadElement") continue;
|
|
826
|
+
const key = memberKey(entry);
|
|
827
|
+
if (key === void 0) return void 0;
|
|
828
|
+
if (findMember(node, key)?.index !== index) continue;
|
|
829
|
+
const path = [...prefix, key];
|
|
830
|
+
if (!isSafeKey(key)) {
|
|
831
|
+
locked.push(path);
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
const member = entry.type === "ObjectProperty" && !isShadowed(node, index) ? entry.value : void 0;
|
|
835
|
+
const literal = readLiteral(member);
|
|
836
|
+
if (literal !== void 0) {
|
|
837
|
+
value[key] = literal;
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
if (member?.type !== "ObjectExpression") {
|
|
841
|
+
locked.push(path);
|
|
842
|
+
continue;
|
|
843
|
+
}
|
|
844
|
+
const nested = readPartial(member, path);
|
|
845
|
+
if (!nested) return void 0;
|
|
846
|
+
value[key] = nested.value;
|
|
847
|
+
locked.push(...nested.locked);
|
|
848
|
+
}
|
|
849
|
+
return {
|
|
850
|
+
value,
|
|
851
|
+
locked
|
|
852
|
+
};
|
|
853
|
+
}
|
|
776
854
|
/** The top-level options of a plugin call, each flagged with whether Studio may write it. */
|
|
777
855
|
function readOptions(call) {
|
|
778
856
|
const entries = {};
|
|
@@ -781,10 +859,15 @@ function readOptions(call) {
|
|
|
781
859
|
for (const [index, entry] of options.properties.entries()) {
|
|
782
860
|
const key = memberKey(entry);
|
|
783
861
|
if (key === void 0 || !isSafeKey(key)) continue;
|
|
784
|
-
const
|
|
785
|
-
|
|
862
|
+
const member = entry.type === "ObjectProperty" && !isShadowed(options, index) ? entry.value : void 0;
|
|
863
|
+
const value = readLiteral(member);
|
|
864
|
+
const partial = value === void 0 && member?.type === "ObjectExpression" ? readPartial(member) : void 0;
|
|
865
|
+
entries[key] = value !== void 0 ? {
|
|
786
866
|
literal: true,
|
|
787
867
|
value
|
|
868
|
+
} : {
|
|
869
|
+
literal: false,
|
|
870
|
+
...partial
|
|
788
871
|
};
|
|
789
872
|
}
|
|
790
873
|
return entries;
|
|
@@ -1070,6 +1153,159 @@ async function writeConfigEdits({ filePath, edits, extensions }) {
|
|
|
1070
1153
|
return result;
|
|
1071
1154
|
}
|
|
1072
1155
|
//#endregion
|
|
1156
|
+
//#region src/diff.ts
|
|
1157
|
+
const CONTEXT_LINES = 3;
|
|
1158
|
+
const DIFF_TIMEOUT_MS = 1e3;
|
|
1159
|
+
const splitLines = (text) => text === "" ? [] : text.split(/(?<=\n)/);
|
|
1160
|
+
function longestIncreasing(values) {
|
|
1161
|
+
const tails = [];
|
|
1162
|
+
const prev = [];
|
|
1163
|
+
for (const [index, value] of values.entries()) {
|
|
1164
|
+
let low = 0;
|
|
1165
|
+
let high = tails.length;
|
|
1166
|
+
while (low < high) {
|
|
1167
|
+
const mid = low + high >> 1;
|
|
1168
|
+
if (values[tails[mid]] < value) low = mid + 1;
|
|
1169
|
+
else high = mid;
|
|
1170
|
+
}
|
|
1171
|
+
prev[index] = low > 0 ? tails[low - 1] : -1;
|
|
1172
|
+
tails[low] = index;
|
|
1173
|
+
}
|
|
1174
|
+
const result = [];
|
|
1175
|
+
for (let index = tails.at(-1) ?? -1; index !== -1; index = prev[index]) result.push(index);
|
|
1176
|
+
return result.reverse();
|
|
1177
|
+
}
|
|
1178
|
+
function cutPoints(before, after) {
|
|
1179
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1180
|
+
for (const [index, line] of before.entries()) {
|
|
1181
|
+
const count = counts.get(line);
|
|
1182
|
+
if (count) count.before++;
|
|
1183
|
+
else counts.set(line, {
|
|
1184
|
+
before: 1,
|
|
1185
|
+
after: 0,
|
|
1186
|
+
index
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
const afterIndex = /* @__PURE__ */ new Map();
|
|
1190
|
+
for (const [index, line] of after.entries()) {
|
|
1191
|
+
const count = counts.get(line);
|
|
1192
|
+
if (!count) continue;
|
|
1193
|
+
count.after++;
|
|
1194
|
+
afterIndex.set(line, index);
|
|
1195
|
+
}
|
|
1196
|
+
const unique = [...counts].filter(([, count]) => count.before === 1 && count.after === 1).map(([line, count]) => [count.index, afterIndex.get(line)]);
|
|
1197
|
+
const anchors = longestIncreasing(unique.map(([, index]) => index)).map((index) => unique[index]);
|
|
1198
|
+
const cuts = [[0, 0]];
|
|
1199
|
+
let run = {
|
|
1200
|
+
before: 0,
|
|
1201
|
+
after: 0,
|
|
1202
|
+
length: 0
|
|
1203
|
+
};
|
|
1204
|
+
const closeRun = () => {
|
|
1205
|
+
if (run.length > 6) cuts.push([run.before + CONTEXT_LINES, run.after + CONTEXT_LINES]);
|
|
1206
|
+
};
|
|
1207
|
+
let i = 0;
|
|
1208
|
+
let j = 0;
|
|
1209
|
+
for (const [anchorBefore, anchorAfter] of anchors) {
|
|
1210
|
+
const unchanged = anchorBefore - i === anchorAfter - j && before.slice(i, anchorBefore).every((line, offset) => line === after[j + offset]);
|
|
1211
|
+
if (unchanged && run.length && run.before + run.length === i) run.length += anchorBefore - i + 1;
|
|
1212
|
+
else {
|
|
1213
|
+
closeRun();
|
|
1214
|
+
run = unchanged ? {
|
|
1215
|
+
before: i,
|
|
1216
|
+
after: j,
|
|
1217
|
+
length: anchorBefore - i + 1
|
|
1218
|
+
} : {
|
|
1219
|
+
before: anchorBefore,
|
|
1220
|
+
after: anchorAfter,
|
|
1221
|
+
length: 1
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1224
|
+
i = anchorBefore + 1;
|
|
1225
|
+
j = anchorAfter + 1;
|
|
1226
|
+
}
|
|
1227
|
+
closeRun();
|
|
1228
|
+
cuts.push([before.length, after.length]);
|
|
1229
|
+
return cuts;
|
|
1230
|
+
}
|
|
1231
|
+
function replaceHunk(before, after) {
|
|
1232
|
+
let start = 0;
|
|
1233
|
+
while (start < before.length && start < after.length && before[start] === after[start]) start++;
|
|
1234
|
+
let end = 0;
|
|
1235
|
+
while (end < before.length - start && end < after.length - start && before[before.length - 1 - end] === after[after.length - 1 - end]) end++;
|
|
1236
|
+
const removed = before.slice(start, before.length - end);
|
|
1237
|
+
const added = after.slice(start, after.length - end);
|
|
1238
|
+
if (!removed.length && !added.length) return [];
|
|
1239
|
+
const lines = (prefix, rows) => rows.flatMap((row) => row.endsWith("\n") ? [`${prefix}${row.slice(0, -1)}`] : [`${prefix}${row}`, "\"]);
|
|
1240
|
+
return [{
|
|
1241
|
+
oldStart: start + 1,
|
|
1242
|
+
oldLines: removed.length,
|
|
1243
|
+
newStart: start + 1,
|
|
1244
|
+
newLines: added.length,
|
|
1245
|
+
lines: [...lines("-", removed), ...lines("+", added)]
|
|
1246
|
+
}];
|
|
1247
|
+
}
|
|
1248
|
+
function diffHunks(before, after, timeout = DIFF_TIMEOUT_MS) {
|
|
1249
|
+
const beforeLines = splitLines(before);
|
|
1250
|
+
const afterLines = splitLines(after);
|
|
1251
|
+
const cuts = cutPoints(beforeLines, afterLines);
|
|
1252
|
+
const deadline = Date.now() + timeout;
|
|
1253
|
+
const hunks = [];
|
|
1254
|
+
let coarse = false;
|
|
1255
|
+
for (const [index, [beforeStart, afterStart]] of cuts.slice(0, -1).entries()) {
|
|
1256
|
+
const [beforeEnd, afterEnd] = cuts[index + 1];
|
|
1257
|
+
const segmentBefore = beforeLines.slice(beforeStart, beforeEnd);
|
|
1258
|
+
const segmentAfter = afterLines.slice(afterStart, afterEnd);
|
|
1259
|
+
const patch = structuredPatch("", "", segmentBefore.join(""), segmentAfter.join(""), void 0, void 0, {
|
|
1260
|
+
context: CONTEXT_LINES,
|
|
1261
|
+
timeout: Math.max(0, deadline - Date.now())
|
|
1262
|
+
});
|
|
1263
|
+
if (!patch) coarse = true;
|
|
1264
|
+
for (const hunk of patch?.hunks ?? replaceHunk(segmentBefore, segmentAfter)) hunks.push({
|
|
1265
|
+
...hunk,
|
|
1266
|
+
oldStart: hunk.oldStart + beforeStart,
|
|
1267
|
+
newStart: hunk.newStart + afterStart
|
|
1268
|
+
});
|
|
1269
|
+
}
|
|
1270
|
+
const lines = hunks.flatMap((hunk) => hunk.lines);
|
|
1271
|
+
return {
|
|
1272
|
+
hunks,
|
|
1273
|
+
stats: {
|
|
1274
|
+
additions: lines.filter((line) => line[0] === "+").length,
|
|
1275
|
+
deletions: lines.filter((line) => line[0] === "-").length,
|
|
1276
|
+
hunks: hunks.length,
|
|
1277
|
+
lines: lines.length,
|
|
1278
|
+
coarse
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1282
|
+
function pageHunks({ hunks, cursor, limit }) {
|
|
1283
|
+
const page = [];
|
|
1284
|
+
let used = 0;
|
|
1285
|
+
let start = 0;
|
|
1286
|
+
for (const [index, hunk] of hunks.entries()) {
|
|
1287
|
+
const offset = Math.max(0, cursor - start);
|
|
1288
|
+
start += hunk.lines.length;
|
|
1289
|
+
const remaining = hunk.lines.length - offset;
|
|
1290
|
+
if (remaining <= 0) continue;
|
|
1291
|
+
if (used > 0 && remaining > limit - used) break;
|
|
1292
|
+
let take = Math.min(remaining, limit - used);
|
|
1293
|
+
if (hunk.lines[offset + take]?.startsWith("\\")) take++;
|
|
1294
|
+
page.push({
|
|
1295
|
+
index,
|
|
1296
|
+
...hunk,
|
|
1297
|
+
lines: hunk.lines.slice(offset, offset + take)
|
|
1298
|
+
});
|
|
1299
|
+
used += take;
|
|
1300
|
+
if (used >= limit) break;
|
|
1301
|
+
}
|
|
1302
|
+
const total = hunks.reduce((sum, hunk) => sum + hunk.lines.length, 0);
|
|
1303
|
+
return {
|
|
1304
|
+
hunks: page,
|
|
1305
|
+
nextCursor: cursor + used < total ? cursor + used : null
|
|
1306
|
+
};
|
|
1307
|
+
}
|
|
1308
|
+
//#endregion
|
|
1073
1309
|
//#region src/generations.ts
|
|
1074
1310
|
const READ_CONCURRENCY = 50;
|
|
1075
1311
|
const MB = 1048576;
|
|
@@ -1199,8 +1435,8 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1199
1435
|
if (path.split("/").includes("..")) return;
|
|
1200
1436
|
const content = await files.storage.readItem(resolve(files.root, path));
|
|
1201
1437
|
if (content === null) return;
|
|
1202
|
-
if (source ===
|
|
1203
|
-
hashes[path] = hashOf(source ===
|
|
1438
|
+
if (source === FileSource.OUTPUT && !content.trim()) return;
|
|
1439
|
+
hashes[path] = hashOf(source === FileSource.OUTPUT ? `${content.trim()}\n` : content);
|
|
1204
1440
|
bytes += Buffer.byteLength(content);
|
|
1205
1441
|
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}${source}/${path}`, content);
|
|
1206
1442
|
}
|
|
@@ -1302,7 +1538,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1302
1538
|
//#region src/generationEvents.ts
|
|
1303
1539
|
const MAX_QUEUED_EVENTS = 1024;
|
|
1304
1540
|
const RESERVED_EVENTS = 64;
|
|
1305
|
-
const isDiscardable = (
|
|
1541
|
+
const isDiscardable = (type) => type === "kubb:files:processing:update" || type === "kubb:info" || type === "kubb:success";
|
|
1306
1542
|
/** Forwards selected Kubb lifecycle events to a native Cap'n Web stream. */
|
|
1307
1543
|
function createGenerationStream(hooks, jobId) {
|
|
1308
1544
|
const unhooks = [];
|
|
@@ -1334,7 +1570,7 @@ function createGenerationStream(hooks, jobId) {
|
|
|
1334
1570
|
version: 1,
|
|
1335
1571
|
timestamp: Date.now()
|
|
1336
1572
|
};
|
|
1337
|
-
if (controller.desiredSize <= RESERVED_EVENTS && isDiscardable(
|
|
1573
|
+
if (controller.desiredSize <= RESERVED_EVENTS && isDiscardable(type)) return;
|
|
1338
1574
|
if (controller.desiredSize <= 0) {
|
|
1339
1575
|
fail(/* @__PURE__ */ new Error(`Generation event stream exceeded ${MAX_QUEUED_EVENTS} queued events`));
|
|
1340
1576
|
return;
|
|
@@ -1438,7 +1674,10 @@ function createGenerationStream(hooks, jobId) {
|
|
|
1438
1674
|
}]
|
|
1439
1675
|
};
|
|
1440
1676
|
function forward(type) {
|
|
1441
|
-
on(type, (...args) =>
|
|
1677
|
+
on(type, (...args) => {
|
|
1678
|
+
emitEvent(type, projections[type](...args));
|
|
1679
|
+
if (!isDiscardable(type)) return new Promise((resolve) => setImmediate(resolve));
|
|
1680
|
+
});
|
|
1442
1681
|
}
|
|
1443
1682
|
on("kubb:build:start", ({ config }) => {
|
|
1444
1683
|
root = config.root;
|
|
@@ -1498,7 +1737,11 @@ function isLoopbackHost(hostname) {
|
|
|
1498
1737
|
*/
|
|
1499
1738
|
function secureUrl({ studioUrl, path, socket = false }) {
|
|
1500
1739
|
const url = new URL(`${studioUrl.replace(/\/+$/, "")}${path}`);
|
|
1501
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) throw new Error(
|
|
1740
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) throw new Diagnostics.Error({
|
|
1741
|
+
code: Diagnostics.code.unknown,
|
|
1742
|
+
severity: "error",
|
|
1743
|
+
message: `Refusing to reach Kubb Studio at ${url.origin}: use an https:// URL, or a loopback host for local development`
|
|
1744
|
+
});
|
|
1502
1745
|
if (socket) url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1503
1746
|
return url;
|
|
1504
1747
|
}
|
|
@@ -1568,6 +1811,7 @@ var Agent = class extends RpcTarget {
|
|
|
1568
1811
|
#session;
|
|
1569
1812
|
#limits = resolveGenerationLimits();
|
|
1570
1813
|
#store;
|
|
1814
|
+
#diffs = /* @__PURE__ */ new Map();
|
|
1571
1815
|
constructor({ options, session }) {
|
|
1572
1816
|
super();
|
|
1573
1817
|
this.#options = options;
|
|
@@ -1606,10 +1850,18 @@ var Agent = class extends RpcTarget {
|
|
|
1606
1850
|
message,
|
|
1607
1851
|
permission
|
|
1608
1852
|
});
|
|
1609
|
-
throw new Error(
|
|
1853
|
+
throw new Diagnostics.Error({
|
|
1854
|
+
code: Diagnostics.code.unknown,
|
|
1855
|
+
severity: "error",
|
|
1856
|
+
message
|
|
1857
|
+
});
|
|
1610
1858
|
}
|
|
1611
1859
|
#assertConnected() {
|
|
1612
|
-
if (!this.#session.info) throw new Error(
|
|
1860
|
+
if (!this.#session.info) throw new Diagnostics.Error({
|
|
1861
|
+
code: Diagnostics.code.internalError,
|
|
1862
|
+
severity: "error",
|
|
1863
|
+
message: "Studio must call connect() before any other command"
|
|
1864
|
+
});
|
|
1613
1865
|
}
|
|
1614
1866
|
#logCommand(command, info) {
|
|
1615
1867
|
return emit(this.#options.hooks, "studio:command", {
|
|
@@ -1706,7 +1958,11 @@ var Agent = class extends RpcTarget {
|
|
|
1706
1958
|
/** Runs one job and keeps its output, and its input with `allowRead`, for later reads. */
|
|
1707
1959
|
async #runGeneration(input, controller) {
|
|
1708
1960
|
this.#assertConnected();
|
|
1709
|
-
if (this.#session.closed) throw new Error(
|
|
1961
|
+
if (this.#session.closed) throw new Diagnostics.Error({
|
|
1962
|
+
code: Diagnostics.code.internalError,
|
|
1963
|
+
severity: "error",
|
|
1964
|
+
message: "Connection is closed"
|
|
1965
|
+
});
|
|
1710
1966
|
if (this.#session.active) return this.#refuse({ message: "A generation is already in progress, please wait for it to finish" });
|
|
1711
1967
|
const settled = Promise.withResolvers();
|
|
1712
1968
|
this.#session.active = {
|
|
@@ -1764,7 +2020,7 @@ var Agent = class extends RpcTarget {
|
|
|
1764
2020
|
}) : void 0;
|
|
1765
2021
|
const disk = diskFiles ? await this.#generations.keep({
|
|
1766
2022
|
jobId,
|
|
1767
|
-
source:
|
|
2023
|
+
source: FileSource.DISK,
|
|
1768
2024
|
files: diskFiles,
|
|
1769
2025
|
maxSetMb: this.#limits.maxSnapshotMb
|
|
1770
2026
|
}) : void 0;
|
|
@@ -1787,7 +2043,7 @@ var Agent = class extends RpcTarget {
|
|
|
1787
2043
|
});
|
|
1788
2044
|
const output = await this.#generations.keep({
|
|
1789
2045
|
jobId,
|
|
1790
|
-
source:
|
|
2046
|
+
source: FileSource.OUTPUT,
|
|
1791
2047
|
files,
|
|
1792
2048
|
maxSetMb: this.#limits.maxMb
|
|
1793
2049
|
});
|
|
@@ -1859,14 +2115,14 @@ var Agent = class extends RpcTarget {
|
|
|
1859
2115
|
async readInput(input) {
|
|
1860
2116
|
this.#assertConnected();
|
|
1861
2117
|
if (!this.#permissions.allowReadAst) return this.#refuse({
|
|
1862
|
-
message:
|
|
1863
|
-
permission:
|
|
2118
|
+
message: AgentMessage.READ_AST_NOT_ALLOWED,
|
|
2119
|
+
permission: AgentPermission.READ_AST
|
|
1864
2120
|
});
|
|
1865
2121
|
if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job" });
|
|
1866
2122
|
const generation = await this.#generations.get(input.jobId);
|
|
1867
|
-
if (!generation) return this.#refuse({ message:
|
|
2123
|
+
if (!generation) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
|
|
1868
2124
|
const kept = generation.inputBytes ? await this.#generations.readInput(input.jobId) : void 0;
|
|
1869
|
-
if (!kept) return this.#refuse({ message:
|
|
2125
|
+
if (!kept) return this.#refuse({ message: AgentMessage.NO_INPUT });
|
|
1870
2126
|
await this.#logCommand("readInput", "read the API input");
|
|
1871
2127
|
return { input: kept };
|
|
1872
2128
|
}
|
|
@@ -1876,16 +2132,16 @@ var Agent = class extends RpcTarget {
|
|
|
1876
2132
|
async readFiles(input) {
|
|
1877
2133
|
this.#assertConnected();
|
|
1878
2134
|
if (!this.#permissions.allowRead) return this.#refuse({
|
|
1879
|
-
message:
|
|
1880
|
-
permission:
|
|
2135
|
+
message: AgentMessage.READ_NOT_ALLOWED,
|
|
2136
|
+
permission: AgentPermission.READ
|
|
1881
2137
|
});
|
|
1882
2138
|
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job and carry paths" });
|
|
1883
2139
|
const { paths } = input;
|
|
1884
2140
|
if (paths.length > 50) return this.#refuse({ message: `At most 50 paths may be requested at once` });
|
|
1885
2141
|
const generation = await this.#generations.get(input.jobId);
|
|
1886
|
-
if (!generation) return this.#refuse({ message:
|
|
2142
|
+
if (!generation) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
|
|
1887
2143
|
if (!isValidPage(input)) return this.#refuse({ message: `The cursor must be a whole line from 0 and come with a limit of 1 to ${MAX_PAGE_LINES} lines` });
|
|
1888
|
-
const source = input.source ===
|
|
2144
|
+
const source = input.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
|
|
1889
2145
|
if (!generation[source]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
|
|
1890
2146
|
const result = await this.#generations.read({
|
|
1891
2147
|
generation,
|
|
@@ -1897,6 +2153,72 @@ var Agent = class extends RpcTarget {
|
|
|
1897
2153
|
await this.#logCommand("readFiles", `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`);
|
|
1898
2154
|
return result;
|
|
1899
2155
|
}
|
|
2156
|
+
/**
|
|
2157
|
+
* Returns one page of the line diff of a file between two kept sets. Refused without `allowRead`.
|
|
2158
|
+
*/
|
|
2159
|
+
async readDiff(input) {
|
|
2160
|
+
this.#assertConnected();
|
|
2161
|
+
if (!this.#permissions.allowRead) return this.#refuse({
|
|
2162
|
+
message: AgentMessage.READ_NOT_ALLOWED,
|
|
2163
|
+
permission: AgentPermission.READ
|
|
2164
|
+
});
|
|
2165
|
+
const { jobId, path, base, cursor = 0, limit } = input;
|
|
2166
|
+
if (typeof jobId !== "string" || !jobId || typeof path !== "string" || !path || typeof base?.jobId !== "string" || !base.jobId) return this.#refuse({ message: "The request must name a job, a base, and a path" });
|
|
2167
|
+
if (!isValidPage({
|
|
2168
|
+
cursor,
|
|
2169
|
+
limit
|
|
2170
|
+
})) return this.#refuse({ message: `The cursor must be a whole line from 0 and come with a limit of 1 to ${MAX_PAGE_LINES} lines` });
|
|
2171
|
+
const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
|
|
2172
|
+
if (!generation || !baseGeneration) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
|
|
2173
|
+
const baseSource = base.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
|
|
2174
|
+
if (!baseGeneration[baseSource]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
|
|
2175
|
+
const [after, before] = await Promise.all([this.#generations.read({
|
|
2176
|
+
generation,
|
|
2177
|
+
source: FileSource.OUTPUT,
|
|
2178
|
+
paths: [path]
|
|
2179
|
+
}), this.#generations.read({
|
|
2180
|
+
generation: baseGeneration,
|
|
2181
|
+
source: baseSource,
|
|
2182
|
+
paths: [path]
|
|
2183
|
+
})]);
|
|
2184
|
+
const afterContent = after.files[path];
|
|
2185
|
+
if (afterContent === void 0 && path in generation.output.hashes) return this.#refuse({ message: "This agent kept only the hashes of that job" });
|
|
2186
|
+
const beforeContent = before.files[path];
|
|
2187
|
+
if (beforeContent === void 0) return {
|
|
2188
|
+
hunks: [],
|
|
2189
|
+
nextCursor: null,
|
|
2190
|
+
hasBase: false,
|
|
2191
|
+
stats: {
|
|
2192
|
+
additions: 0,
|
|
2193
|
+
deletions: 0,
|
|
2194
|
+
hunks: 0,
|
|
2195
|
+
lines: 0,
|
|
2196
|
+
coarse: false
|
|
2197
|
+
}
|
|
2198
|
+
};
|
|
2199
|
+
const key = JSON.stringify([
|
|
2200
|
+
jobId,
|
|
2201
|
+
base.jobId,
|
|
2202
|
+
baseSource,
|
|
2203
|
+
path
|
|
2204
|
+
]);
|
|
2205
|
+
let diff = this.#diffs.get(key);
|
|
2206
|
+
if (!diff) {
|
|
2207
|
+
diff = diffHunks(beforeContent, afterContent ?? "");
|
|
2208
|
+
this.#diffs.set(key, diff);
|
|
2209
|
+
if (this.#diffs.size > 4) this.#diffs.delete(this.#diffs.keys().next().value);
|
|
2210
|
+
}
|
|
2211
|
+
await this.#logCommand("readDiff", `read the diff of ${path}`);
|
|
2212
|
+
return {
|
|
2213
|
+
...pageHunks({
|
|
2214
|
+
hunks: diff.hunks,
|
|
2215
|
+
cursor,
|
|
2216
|
+
limit
|
|
2217
|
+
}),
|
|
2218
|
+
hasBase: true,
|
|
2219
|
+
stats: diff.stats
|
|
2220
|
+
};
|
|
2221
|
+
}
|
|
1900
2222
|
};
|
|
1901
2223
|
const openWebSocketRpc = ({ url, token, instanceId, agent }) => {
|
|
1902
2224
|
const socket = new WebSocket(url, { headers: {
|
|
@@ -2105,7 +2427,12 @@ async function startPairing({ studioUrl = defaultStudioUrl, type, name, hostname
|
|
|
2105
2427
|
},
|
|
2106
2428
|
signal
|
|
2107
2429
|
});
|
|
2108
|
-
if (typeof pairing?.device_code !== "string") throw new Error(
|
|
2430
|
+
if (typeof pairing?.device_code !== "string") throw new Diagnostics.Error({
|
|
2431
|
+
code: Diagnostics.code.unknown,
|
|
2432
|
+
severity: "error",
|
|
2433
|
+
message: "Kubb Studio returned no pairing code",
|
|
2434
|
+
help: "Try again later. If it keeps failing, check the Kubb Studio status."
|
|
2435
|
+
});
|
|
2109
2436
|
return pairing;
|
|
2110
2437
|
} catch (error) {
|
|
2111
2438
|
if (signal?.aborted) throw new PairingCanceledError();
|
|
@@ -2146,7 +2473,12 @@ async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, sign
|
|
|
2146
2473
|
signal,
|
|
2147
2474
|
ignoreResponseError: true
|
|
2148
2475
|
});
|
|
2149
|
-
if (status >= 500 || !_data || typeof _data !== "object") throw new Error(
|
|
2476
|
+
if (status >= 500 || !_data || typeof _data !== "object") throw new Diagnostics.Error({
|
|
2477
|
+
code: Diagnostics.code.inputRequestFailed,
|
|
2478
|
+
severity: "error",
|
|
2479
|
+
message: `Kubb Studio answered with status ${status}`,
|
|
2480
|
+
help: "Try again later."
|
|
2481
|
+
});
|
|
2150
2482
|
response = _data;
|
|
2151
2483
|
} catch (error) {
|
|
2152
2484
|
if (signal?.aborted) throw new PairingCanceledError();
|
|
@@ -2154,7 +2486,11 @@ async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, sign
|
|
|
2154
2486
|
continue;
|
|
2155
2487
|
}
|
|
2156
2488
|
if (isPairingResult(response)) return response;
|
|
2157
|
-
if (!("error" in response) || typeof response.error !== "string") throw new Error(
|
|
2489
|
+
if (!("error" in response) || typeof response.error !== "string") throw new Diagnostics.Error({
|
|
2490
|
+
code: Diagnostics.code.unknown,
|
|
2491
|
+
severity: "error",
|
|
2492
|
+
message: "Kubb Studio returned an empty pairing response, pair again"
|
|
2493
|
+
});
|
|
2158
2494
|
if (response.error === "authorization_pending") continue;
|
|
2159
2495
|
if (response.error === "slow_down") {
|
|
2160
2496
|
intervalMs += 5e3;
|
|
@@ -2162,7 +2498,12 @@ async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, sign
|
|
|
2162
2498
|
}
|
|
2163
2499
|
if (response.error === "access_denied") throw new PairingDeniedError(response.error_description);
|
|
2164
2500
|
if (response.error === "expired_token" || response.error === "invalid_grant") throw new PairingExpiredError(response.error_description);
|
|
2165
|
-
throw new Error(
|
|
2501
|
+
throw new Diagnostics.Error({
|
|
2502
|
+
code: Diagnostics.code.unknown,
|
|
2503
|
+
severity: "error",
|
|
2504
|
+
message: response.error_description ?? `Pairing failed (${response.error})`,
|
|
2505
|
+
help: "Pair again with `kubb studio login`."
|
|
2506
|
+
});
|
|
2166
2507
|
}
|
|
2167
2508
|
throw new PairingExpiredError();
|
|
2168
2509
|
}
|
|
@@ -2212,7 +2553,12 @@ function splitEntryPath(path) {
|
|
|
2212
2553
|
prefix
|
|
2213
2554
|
};
|
|
2214
2555
|
}
|
|
2215
|
-
throw new Error(
|
|
2556
|
+
throw new Diagnostics.Error({
|
|
2557
|
+
code: Diagnostics.code.unknown,
|
|
2558
|
+
severity: "error",
|
|
2559
|
+
message: `Snapshot path is too long for a tar entry: ${path}`,
|
|
2560
|
+
help: "Shorten the output path or the generated file names."
|
|
2561
|
+
});
|
|
2216
2562
|
}
|
|
2217
2563
|
function header(path, size) {
|
|
2218
2564
|
const { name, prefix } = splitEntryPath(path);
|
|
@@ -2245,7 +2591,12 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2245
2591
|
for (const [name, content] of Object.entries(files)) {
|
|
2246
2592
|
const path = packagePath(name);
|
|
2247
2593
|
const owner = sources.get(path)?.name;
|
|
2248
|
-
if (owner) throw new Error(
|
|
2594
|
+
if (owner) throw new Diagnostics.Error({
|
|
2595
|
+
code: Diagnostics.code.unknown,
|
|
2596
|
+
severity: "error",
|
|
2597
|
+
message: `Snapshot has two generated files that sanitize to the same path "package/${path}": "${owner}" and "${name}"`,
|
|
2598
|
+
help: "Rename one of the generated files, for example with a plugin resolver."
|
|
2599
|
+
});
|
|
2249
2600
|
sources.set(path, {
|
|
2250
2601
|
name,
|
|
2251
2602
|
content
|
|
@@ -2335,7 +2686,11 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2335
2686
|
});
|
|
2336
2687
|
const files = Object.fromEntries(await Promise.all([...generated.paths].map(async (path) => {
|
|
2337
2688
|
const content = await generated.storage.readItem(resolve(generated.root, path));
|
|
2338
|
-
if (content === null) throw new Error(
|
|
2689
|
+
if (content === null) throw new Diagnostics.Error({
|
|
2690
|
+
code: Diagnostics.code.internalError,
|
|
2691
|
+
severity: "error",
|
|
2692
|
+
message: `The generation listed ${path} but did not keep its content`
|
|
2693
|
+
});
|
|
2339
2694
|
return [path, content];
|
|
2340
2695
|
})));
|
|
2341
2696
|
const pluginNames = config.plugins.map((plugin) => resolveMeta(plugin).name);
|
|
@@ -2348,7 +2703,12 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2348
2703
|
const names = [...new Set(pluginNames.map(packageOf))];
|
|
2349
2704
|
const peerDependencies = Object.fromEntries(installed.flatMap(({ package: name, version }) => names.includes(name) && version ? [[name, version]] : []));
|
|
2350
2705
|
const missing = names.filter((name) => !(name in peerDependencies) && !BUNDLED_DEPENDENCIES.includes(name));
|
|
2351
|
-
if (missing.length) throw new Error(
|
|
2706
|
+
if (missing.length) throw new Diagnostics.Error({
|
|
2707
|
+
code: Diagnostics.code.missingDependency,
|
|
2708
|
+
severity: "error",
|
|
2709
|
+
message: `Missing dependencies: ${missing.join(", ")}`,
|
|
2710
|
+
help: "Install the listed plugin packages in the project."
|
|
2711
|
+
});
|
|
2352
2712
|
const bytes = await createSnapshotPackage({
|
|
2353
2713
|
files,
|
|
2354
2714
|
packageInfo: {
|
|
@@ -2357,7 +2717,12 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2357
2717
|
peerDependencies
|
|
2358
2718
|
}
|
|
2359
2719
|
});
|
|
2360
|
-
if (bytes.length > 52428800) throw new Error(
|
|
2720
|
+
if (bytes.length > 52428800) throw new Diagnostics.Error({
|
|
2721
|
+
code: Diagnostics.code.unknown,
|
|
2722
|
+
severity: "error",
|
|
2723
|
+
message: `The snapshot is ${Math.ceil(bytes.length / 1024 / 1024)} MB, over the ${MAX_SNAPSHOT_BYTES / 1024 / 1024} MB limit`,
|
|
2724
|
+
help: "Generate fewer files, for example with `include` or `exclude` on the plugins."
|
|
2725
|
+
});
|
|
2361
2726
|
const hashes = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, hashOf(content)]));
|
|
2362
2727
|
const body = new FormData();
|
|
2363
2728
|
body.set("metadata", JSON.stringify({
|
|
@@ -2377,9 +2742,19 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2377
2742
|
});
|
|
2378
2743
|
if (!ok) {
|
|
2379
2744
|
const upgrade = _data?.data?.upgradeUrl ? ` Upgrade at ${_data.data.upgradeUrl}.` : "";
|
|
2380
|
-
throw new Error(
|
|
2745
|
+
throw new Diagnostics.Error({
|
|
2746
|
+
code: Diagnostics.code.inputRequestFailed,
|
|
2747
|
+
severity: "error",
|
|
2748
|
+
message: `Kubb Studio refused the snapshot (status ${status})${_data?.message ? `: ${_data.message}` : ""}.${upgrade}`,
|
|
2749
|
+
help: "Check the API key and your plan, then try again."
|
|
2750
|
+
});
|
|
2381
2751
|
}
|
|
2382
|
-
if (!_data?.snapshot) throw new Error(
|
|
2752
|
+
if (!_data?.snapshot) throw new Diagnostics.Error({
|
|
2753
|
+
code: Diagnostics.code.inputRequestFailed,
|
|
2754
|
+
severity: "error",
|
|
2755
|
+
message: `Kubb Studio returned no snapshot (status ${status})`,
|
|
2756
|
+
help: "Try again later."
|
|
2757
|
+
});
|
|
2383
2758
|
return _data.snapshot;
|
|
2384
2759
|
}
|
|
2385
2760
|
//#endregion
|