@kubb/studio 5.10.1 → 5.10.2
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 +287 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +288 -40
- package/dist/index.js.map +1 -1
- package/dist/{protocol-C1PwN9fa.d.ts → protocol-DQfI1aLV.d.ts} +76 -11
- package/dist/protocol.cjs +34 -10
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +32 -7
- package/dist/protocol.js.map +1 -1
- package/package.json +5 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
|
|
2
|
-
import {
|
|
2
|
+
import { E as InstalledExtensions, N as PublishedSnapshot, U as SnapshotUploadMetadata, a as AgentConnectInfo, c as AgentPermissions, d as ConfigEditOutcome, s as AgentPermission, u as ConfigEdit } from "./protocol-DQfI1aLV.js";
|
|
3
3
|
import { Config, Hookable, KubbHooks } from "@kubb/core";
|
|
4
4
|
//#region src/connect.d.ts
|
|
5
5
|
/**
|
|
@@ -46,7 +46,7 @@ declare global {
|
|
|
46
46
|
* A finished command and what it did, such as `applied 2/3 edits to kubb.config.ts`.
|
|
47
47
|
*/
|
|
48
48
|
'studio:command': [ctx: {
|
|
49
|
-
command: 'generate' | 'saveConfig' | 'readFiles' | 'readInput';
|
|
49
|
+
command: 'generate' | 'saveConfig' | 'readFiles' | 'readInput' | 'readDiff';
|
|
50
50
|
info: string;
|
|
51
51
|
}];
|
|
52
52
|
/**
|
|
@@ -54,7 +54,7 @@ declare global {
|
|
|
54
54
|
*/
|
|
55
55
|
'studio:warn': [ctx: {
|
|
56
56
|
message: string;
|
|
57
|
-
permission?:
|
|
57
|
+
permission?: AgentPermission;
|
|
58
58
|
}];
|
|
59
59
|
'studio:error': [ctx: {
|
|
60
60
|
error: Error;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import "./rolldown-runtime-CRm0XQPb.js";
|
|
2
|
-
import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode,
|
|
2
|
+
import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, FileSource, MAX_PAGE_LINES, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { createHash, hash, randomUUID } from "node:crypto";
|
|
5
5
|
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
@@ -12,6 +12,7 @@ import { existsSync } from "node:fs";
|
|
|
12
12
|
import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, getInputKind, isPackageName, memoryStorage, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
|
|
13
13
|
import { RpcTarget, newWebSocketRpcSession } from "capnweb";
|
|
14
14
|
import { detectCodeFormat, parseExpression, parseModule } from "magicast";
|
|
15
|
+
import { structuredPatch } from "diff";
|
|
15
16
|
import { stripVTControlCharacters } from "node:util";
|
|
16
17
|
import { ast } from "@kubb/ast";
|
|
17
18
|
import { ofetch } from "ofetch";
|
|
@@ -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.2";
|
|
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,38 @@ 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 = 1e4;
|
|
1159
|
+
function diffHunks(before, after) {
|
|
1160
|
+
return new Promise((resolve) => {
|
|
1161
|
+
structuredPatch("", "", before, after, void 0, void 0, {
|
|
1162
|
+
context: CONTEXT_LINES,
|
|
1163
|
+
timeout: DIFF_TIMEOUT_MS,
|
|
1164
|
+
callback: (patch) => resolve(patch?.hunks)
|
|
1165
|
+
});
|
|
1166
|
+
});
|
|
1167
|
+
}
|
|
1168
|
+
function pageHunks({ hunks, cursor, limit }) {
|
|
1169
|
+
const page = [];
|
|
1170
|
+
const end = cursor + limit;
|
|
1171
|
+
let start = 0;
|
|
1172
|
+
for (const [index, hunk] of hunks.entries()) {
|
|
1173
|
+
const next = start + hunk.lines.length;
|
|
1174
|
+
if (next > cursor && start < end) page.push({
|
|
1175
|
+
index,
|
|
1176
|
+
oldStart: hunk.oldStart,
|
|
1177
|
+
newStart: hunk.newStart,
|
|
1178
|
+
lines: hunk.lines.slice(Math.max(0, cursor - start), end - start)
|
|
1179
|
+
});
|
|
1180
|
+
start = next;
|
|
1181
|
+
}
|
|
1182
|
+
return {
|
|
1183
|
+
hunks: page,
|
|
1184
|
+
nextCursor: end < start ? end : null
|
|
1185
|
+
};
|
|
1186
|
+
}
|
|
1187
|
+
//#endregion
|
|
1073
1188
|
//#region src/generations.ts
|
|
1074
1189
|
const READ_CONCURRENCY = 50;
|
|
1075
1190
|
const MB = 1048576;
|
|
@@ -1199,8 +1314,8 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1199
1314
|
if (path.split("/").includes("..")) return;
|
|
1200
1315
|
const content = await files.storage.readItem(resolve(files.root, path));
|
|
1201
1316
|
if (content === null) return;
|
|
1202
|
-
if (source ===
|
|
1203
|
-
hashes[path] = hashOf(source ===
|
|
1317
|
+
if (source === FileSource.OUTPUT && !content.trim()) return;
|
|
1318
|
+
hashes[path] = hashOf(source === FileSource.OUTPUT ? `${content.trim()}\n` : content);
|
|
1204
1319
|
bytes += Buffer.byteLength(content);
|
|
1205
1320
|
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}${source}/${path}`, content);
|
|
1206
1321
|
}
|
|
@@ -1498,7 +1613,11 @@ function isLoopbackHost(hostname) {
|
|
|
1498
1613
|
*/
|
|
1499
1614
|
function secureUrl({ studioUrl, path, socket = false }) {
|
|
1500
1615
|
const url = new URL(`${studioUrl.replace(/\/+$/, "")}${path}`);
|
|
1501
|
-
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) throw new Error(
|
|
1616
|
+
if (url.protocol !== "https:" && !(url.protocol === "http:" && isLoopbackHost(url.hostname))) throw new Diagnostics.Error({
|
|
1617
|
+
code: Diagnostics.code.unknown,
|
|
1618
|
+
severity: "error",
|
|
1619
|
+
message: `Refusing to reach Kubb Studio at ${url.origin}: use an https:// URL, or a loopback host for local development`
|
|
1620
|
+
});
|
|
1502
1621
|
if (socket) url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
1503
1622
|
return url;
|
|
1504
1623
|
}
|
|
@@ -1568,6 +1687,7 @@ var Agent = class extends RpcTarget {
|
|
|
1568
1687
|
#session;
|
|
1569
1688
|
#limits = resolveGenerationLimits();
|
|
1570
1689
|
#store;
|
|
1690
|
+
#diffs = /* @__PURE__ */ new Map();
|
|
1571
1691
|
constructor({ options, session }) {
|
|
1572
1692
|
super();
|
|
1573
1693
|
this.#options = options;
|
|
@@ -1606,10 +1726,18 @@ var Agent = class extends RpcTarget {
|
|
|
1606
1726
|
message,
|
|
1607
1727
|
permission
|
|
1608
1728
|
});
|
|
1609
|
-
throw new Error(
|
|
1729
|
+
throw new Diagnostics.Error({
|
|
1730
|
+
code: Diagnostics.code.unknown,
|
|
1731
|
+
severity: "error",
|
|
1732
|
+
message
|
|
1733
|
+
});
|
|
1610
1734
|
}
|
|
1611
1735
|
#assertConnected() {
|
|
1612
|
-
if (!this.#session.info) throw new Error(
|
|
1736
|
+
if (!this.#session.info) throw new Diagnostics.Error({
|
|
1737
|
+
code: Diagnostics.code.internalError,
|
|
1738
|
+
severity: "error",
|
|
1739
|
+
message: "Studio must call connect() before any other command"
|
|
1740
|
+
});
|
|
1613
1741
|
}
|
|
1614
1742
|
#logCommand(command, info) {
|
|
1615
1743
|
return emit(this.#options.hooks, "studio:command", {
|
|
@@ -1706,7 +1834,11 @@ var Agent = class extends RpcTarget {
|
|
|
1706
1834
|
/** Runs one job and keeps its output, and its input with `allowRead`, for later reads. */
|
|
1707
1835
|
async #runGeneration(input, controller) {
|
|
1708
1836
|
this.#assertConnected();
|
|
1709
|
-
if (this.#session.closed) throw new Error(
|
|
1837
|
+
if (this.#session.closed) throw new Diagnostics.Error({
|
|
1838
|
+
code: Diagnostics.code.internalError,
|
|
1839
|
+
severity: "error",
|
|
1840
|
+
message: "Connection is closed"
|
|
1841
|
+
});
|
|
1710
1842
|
if (this.#session.active) return this.#refuse({ message: "A generation is already in progress, please wait for it to finish" });
|
|
1711
1843
|
const settled = Promise.withResolvers();
|
|
1712
1844
|
this.#session.active = {
|
|
@@ -1764,7 +1896,7 @@ var Agent = class extends RpcTarget {
|
|
|
1764
1896
|
}) : void 0;
|
|
1765
1897
|
const disk = diskFiles ? await this.#generations.keep({
|
|
1766
1898
|
jobId,
|
|
1767
|
-
source:
|
|
1899
|
+
source: FileSource.DISK,
|
|
1768
1900
|
files: diskFiles,
|
|
1769
1901
|
maxSetMb: this.#limits.maxSnapshotMb
|
|
1770
1902
|
}) : void 0;
|
|
@@ -1787,7 +1919,7 @@ var Agent = class extends RpcTarget {
|
|
|
1787
1919
|
});
|
|
1788
1920
|
const output = await this.#generations.keep({
|
|
1789
1921
|
jobId,
|
|
1790
|
-
source:
|
|
1922
|
+
source: FileSource.OUTPUT,
|
|
1791
1923
|
files,
|
|
1792
1924
|
maxSetMb: this.#limits.maxMb
|
|
1793
1925
|
});
|
|
@@ -1859,14 +1991,14 @@ var Agent = class extends RpcTarget {
|
|
|
1859
1991
|
async readInput(input) {
|
|
1860
1992
|
this.#assertConnected();
|
|
1861
1993
|
if (!this.#permissions.allowReadAst) return this.#refuse({
|
|
1862
|
-
message:
|
|
1863
|
-
permission:
|
|
1994
|
+
message: AgentMessage.READ_AST_NOT_ALLOWED,
|
|
1995
|
+
permission: AgentPermission.READ_AST
|
|
1864
1996
|
});
|
|
1865
1997
|
if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: "The request must name a job" });
|
|
1866
1998
|
const generation = await this.#generations.get(input.jobId);
|
|
1867
|
-
if (!generation) return this.#refuse({ message:
|
|
1999
|
+
if (!generation) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
|
|
1868
2000
|
const kept = generation.inputBytes ? await this.#generations.readInput(input.jobId) : void 0;
|
|
1869
|
-
if (!kept) return this.#refuse({ message:
|
|
2001
|
+
if (!kept) return this.#refuse({ message: AgentMessage.NO_INPUT });
|
|
1870
2002
|
await this.#logCommand("readInput", "read the API input");
|
|
1871
2003
|
return { input: kept };
|
|
1872
2004
|
}
|
|
@@ -1876,16 +2008,16 @@ var Agent = class extends RpcTarget {
|
|
|
1876
2008
|
async readFiles(input) {
|
|
1877
2009
|
this.#assertConnected();
|
|
1878
2010
|
if (!this.#permissions.allowRead) return this.#refuse({
|
|
1879
|
-
message:
|
|
1880
|
-
permission:
|
|
2011
|
+
message: AgentMessage.READ_NOT_ALLOWED,
|
|
2012
|
+
permission: AgentPermission.READ
|
|
1881
2013
|
});
|
|
1882
2014
|
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
2015
|
const { paths } = input;
|
|
1884
2016
|
if (paths.length > 50) return this.#refuse({ message: `At most 50 paths may be requested at once` });
|
|
1885
2017
|
const generation = await this.#generations.get(input.jobId);
|
|
1886
|
-
if (!generation) return this.#refuse({ message:
|
|
2018
|
+
if (!generation) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
|
|
1887
2019
|
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 ===
|
|
2020
|
+
const source = input.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
|
|
1889
2021
|
if (!generation[source]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
|
|
1890
2022
|
const result = await this.#generations.read({
|
|
1891
2023
|
generation,
|
|
@@ -1897,6 +2029,69 @@ var Agent = class extends RpcTarget {
|
|
|
1897
2029
|
await this.#logCommand("readFiles", `read ${Object.keys(result.files).length}/${paths.length} requested file${paths.length === 1 ? "" : "s"}`);
|
|
1898
2030
|
return result;
|
|
1899
2031
|
}
|
|
2032
|
+
/**
|
|
2033
|
+
* Returns one page of the line diff of a file between two kept sets. Refused without `allowRead`.
|
|
2034
|
+
*/
|
|
2035
|
+
async readDiff(input) {
|
|
2036
|
+
this.#assertConnected();
|
|
2037
|
+
if (!this.#permissions.allowRead) return this.#refuse({
|
|
2038
|
+
message: AgentMessage.READ_NOT_ALLOWED,
|
|
2039
|
+
permission: AgentPermission.READ
|
|
2040
|
+
});
|
|
2041
|
+
const { jobId, path, base, cursor = 0, limit } = input;
|
|
2042
|
+
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" });
|
|
2043
|
+
if (!isValidPage({
|
|
2044
|
+
cursor,
|
|
2045
|
+
limit
|
|
2046
|
+
})) 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` });
|
|
2047
|
+
const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
|
|
2048
|
+
if (!generation || !baseGeneration) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
|
|
2049
|
+
const baseSource = base.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
|
|
2050
|
+
if (!baseGeneration[baseSource]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
|
|
2051
|
+
const [after, before] = await Promise.all([this.#generations.read({
|
|
2052
|
+
generation,
|
|
2053
|
+
source: FileSource.OUTPUT,
|
|
2054
|
+
paths: [path]
|
|
2055
|
+
}), this.#generations.read({
|
|
2056
|
+
generation: baseGeneration,
|
|
2057
|
+
source: baseSource,
|
|
2058
|
+
paths: [path]
|
|
2059
|
+
})]);
|
|
2060
|
+
const afterContent = after.files[path];
|
|
2061
|
+
if (afterContent === void 0 && path in generation.output.hashes) return this.#refuse({ message: "This agent kept only the hashes of that job" });
|
|
2062
|
+
const beforeContent = before.files[path];
|
|
2063
|
+
if (beforeContent === void 0) return {
|
|
2064
|
+
hunks: [],
|
|
2065
|
+
nextCursor: null,
|
|
2066
|
+
hasBase: false
|
|
2067
|
+
};
|
|
2068
|
+
const key = JSON.stringify([
|
|
2069
|
+
jobId,
|
|
2070
|
+
base.jobId,
|
|
2071
|
+
baseSource,
|
|
2072
|
+
path
|
|
2073
|
+
]);
|
|
2074
|
+
let hunks = this.#diffs.get(key);
|
|
2075
|
+
if (!hunks) {
|
|
2076
|
+
hunks = diffHunks(beforeContent, afterContent ?? "");
|
|
2077
|
+
this.#diffs.set(key, hunks);
|
|
2078
|
+
if (this.#diffs.size > 4) this.#diffs.delete(this.#diffs.keys().next().value);
|
|
2079
|
+
}
|
|
2080
|
+
const all = await hunks;
|
|
2081
|
+
if (!all) {
|
|
2082
|
+
this.#diffs.delete(key);
|
|
2083
|
+
return this.#refuse({ message: "The two versions differ too much to diff" });
|
|
2084
|
+
}
|
|
2085
|
+
await this.#logCommand("readDiff", `read the diff of ${path}`);
|
|
2086
|
+
return {
|
|
2087
|
+
...pageHunks({
|
|
2088
|
+
hunks: all,
|
|
2089
|
+
cursor,
|
|
2090
|
+
limit
|
|
2091
|
+
}),
|
|
2092
|
+
hasBase: true
|
|
2093
|
+
};
|
|
2094
|
+
}
|
|
1900
2095
|
};
|
|
1901
2096
|
const openWebSocketRpc = ({ url, token, instanceId, agent }) => {
|
|
1902
2097
|
const socket = new WebSocket(url, { headers: {
|
|
@@ -2105,7 +2300,12 @@ async function startPairing({ studioUrl = defaultStudioUrl, type, name, hostname
|
|
|
2105
2300
|
},
|
|
2106
2301
|
signal
|
|
2107
2302
|
});
|
|
2108
|
-
if (typeof pairing?.device_code !== "string") throw new Error(
|
|
2303
|
+
if (typeof pairing?.device_code !== "string") throw new Diagnostics.Error({
|
|
2304
|
+
code: Diagnostics.code.unknown,
|
|
2305
|
+
severity: "error",
|
|
2306
|
+
message: "Kubb Studio returned no pairing code",
|
|
2307
|
+
help: "Try again later. If it keeps failing, check the Kubb Studio status."
|
|
2308
|
+
});
|
|
2109
2309
|
return pairing;
|
|
2110
2310
|
} catch (error) {
|
|
2111
2311
|
if (signal?.aborted) throw new PairingCanceledError();
|
|
@@ -2146,7 +2346,12 @@ async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, sign
|
|
|
2146
2346
|
signal,
|
|
2147
2347
|
ignoreResponseError: true
|
|
2148
2348
|
});
|
|
2149
|
-
if (status >= 500 || !_data || typeof _data !== "object") throw new Error(
|
|
2349
|
+
if (status >= 500 || !_data || typeof _data !== "object") throw new Diagnostics.Error({
|
|
2350
|
+
code: Diagnostics.code.inputRequestFailed,
|
|
2351
|
+
severity: "error",
|
|
2352
|
+
message: `Kubb Studio answered with status ${status}`,
|
|
2353
|
+
help: "Try again later."
|
|
2354
|
+
});
|
|
2150
2355
|
response = _data;
|
|
2151
2356
|
} catch (error) {
|
|
2152
2357
|
if (signal?.aborted) throw new PairingCanceledError();
|
|
@@ -2154,7 +2359,11 @@ async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, sign
|
|
|
2154
2359
|
continue;
|
|
2155
2360
|
}
|
|
2156
2361
|
if (isPairingResult(response)) return response;
|
|
2157
|
-
if (!("error" in response) || typeof response.error !== "string") throw new Error(
|
|
2362
|
+
if (!("error" in response) || typeof response.error !== "string") throw new Diagnostics.Error({
|
|
2363
|
+
code: Diagnostics.code.unknown,
|
|
2364
|
+
severity: "error",
|
|
2365
|
+
message: "Kubb Studio returned an empty pairing response, pair again"
|
|
2366
|
+
});
|
|
2158
2367
|
if (response.error === "authorization_pending") continue;
|
|
2159
2368
|
if (response.error === "slow_down") {
|
|
2160
2369
|
intervalMs += 5e3;
|
|
@@ -2162,7 +2371,12 @@ async function pollForPairingToken({ studioUrl = defaultStudioUrl, session, sign
|
|
|
2162
2371
|
}
|
|
2163
2372
|
if (response.error === "access_denied") throw new PairingDeniedError(response.error_description);
|
|
2164
2373
|
if (response.error === "expired_token" || response.error === "invalid_grant") throw new PairingExpiredError(response.error_description);
|
|
2165
|
-
throw new Error(
|
|
2374
|
+
throw new Diagnostics.Error({
|
|
2375
|
+
code: Diagnostics.code.unknown,
|
|
2376
|
+
severity: "error",
|
|
2377
|
+
message: response.error_description ?? `Pairing failed (${response.error})`,
|
|
2378
|
+
help: "Pair again with `kubb studio login`."
|
|
2379
|
+
});
|
|
2166
2380
|
}
|
|
2167
2381
|
throw new PairingExpiredError();
|
|
2168
2382
|
}
|
|
@@ -2212,7 +2426,12 @@ function splitEntryPath(path) {
|
|
|
2212
2426
|
prefix
|
|
2213
2427
|
};
|
|
2214
2428
|
}
|
|
2215
|
-
throw new Error(
|
|
2429
|
+
throw new Diagnostics.Error({
|
|
2430
|
+
code: Diagnostics.code.unknown,
|
|
2431
|
+
severity: "error",
|
|
2432
|
+
message: `Snapshot path is too long for a tar entry: ${path}`,
|
|
2433
|
+
help: "Shorten the output path or the generated file names."
|
|
2434
|
+
});
|
|
2216
2435
|
}
|
|
2217
2436
|
function header(path, size) {
|
|
2218
2437
|
const { name, prefix } = splitEntryPath(path);
|
|
@@ -2245,7 +2464,12 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2245
2464
|
for (const [name, content] of Object.entries(files)) {
|
|
2246
2465
|
const path = packagePath(name);
|
|
2247
2466
|
const owner = sources.get(path)?.name;
|
|
2248
|
-
if (owner) throw new Error(
|
|
2467
|
+
if (owner) throw new Diagnostics.Error({
|
|
2468
|
+
code: Diagnostics.code.unknown,
|
|
2469
|
+
severity: "error",
|
|
2470
|
+
message: `Snapshot has two generated files that sanitize to the same path "package/${path}": "${owner}" and "${name}"`,
|
|
2471
|
+
help: "Rename one of the generated files, for example with a plugin resolver."
|
|
2472
|
+
});
|
|
2249
2473
|
sources.set(path, {
|
|
2250
2474
|
name,
|
|
2251
2475
|
content
|
|
@@ -2335,7 +2559,11 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2335
2559
|
});
|
|
2336
2560
|
const files = Object.fromEntries(await Promise.all([...generated.paths].map(async (path) => {
|
|
2337
2561
|
const content = await generated.storage.readItem(resolve(generated.root, path));
|
|
2338
|
-
if (content === null) throw new Error(
|
|
2562
|
+
if (content === null) throw new Diagnostics.Error({
|
|
2563
|
+
code: Diagnostics.code.internalError,
|
|
2564
|
+
severity: "error",
|
|
2565
|
+
message: `The generation listed ${path} but did not keep its content`
|
|
2566
|
+
});
|
|
2339
2567
|
return [path, content];
|
|
2340
2568
|
})));
|
|
2341
2569
|
const pluginNames = config.plugins.map((plugin) => resolveMeta(plugin).name);
|
|
@@ -2348,7 +2576,12 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2348
2576
|
const names = [...new Set(pluginNames.map(packageOf))];
|
|
2349
2577
|
const peerDependencies = Object.fromEntries(installed.flatMap(({ package: name, version }) => names.includes(name) && version ? [[name, version]] : []));
|
|
2350
2578
|
const missing = names.filter((name) => !(name in peerDependencies) && !BUNDLED_DEPENDENCIES.includes(name));
|
|
2351
|
-
if (missing.length) throw new Error(
|
|
2579
|
+
if (missing.length) throw new Diagnostics.Error({
|
|
2580
|
+
code: Diagnostics.code.missingDependency,
|
|
2581
|
+
severity: "error",
|
|
2582
|
+
message: `Missing dependencies: ${missing.join(", ")}`,
|
|
2583
|
+
help: "Install the listed plugin packages in the project."
|
|
2584
|
+
});
|
|
2352
2585
|
const bytes = await createSnapshotPackage({
|
|
2353
2586
|
files,
|
|
2354
2587
|
packageInfo: {
|
|
@@ -2357,7 +2590,12 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2357
2590
|
peerDependencies
|
|
2358
2591
|
}
|
|
2359
2592
|
});
|
|
2360
|
-
if (bytes.length > 52428800) throw new Error(
|
|
2593
|
+
if (bytes.length > 52428800) throw new Diagnostics.Error({
|
|
2594
|
+
code: Diagnostics.code.unknown,
|
|
2595
|
+
severity: "error",
|
|
2596
|
+
message: `The snapshot is ${Math.ceil(bytes.length / 1024 / 1024)} MB, over the ${MAX_SNAPSHOT_BYTES / 1024 / 1024} MB limit`,
|
|
2597
|
+
help: "Generate fewer files, for example with `include` or `exclude` on the plugins."
|
|
2598
|
+
});
|
|
2361
2599
|
const hashes = Object.fromEntries(Object.entries(files).map(([path, content]) => [path, hashOf(content)]));
|
|
2362
2600
|
const body = new FormData();
|
|
2363
2601
|
body.set("metadata", JSON.stringify({
|
|
@@ -2377,9 +2615,19 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2377
2615
|
});
|
|
2378
2616
|
if (!ok) {
|
|
2379
2617
|
const upgrade = _data?.data?.upgradeUrl ? ` Upgrade at ${_data.data.upgradeUrl}.` : "";
|
|
2380
|
-
throw new Error(
|
|
2618
|
+
throw new Diagnostics.Error({
|
|
2619
|
+
code: Diagnostics.code.inputRequestFailed,
|
|
2620
|
+
severity: "error",
|
|
2621
|
+
message: `Kubb Studio refused the snapshot (status ${status})${_data?.message ? `: ${_data.message}` : ""}.${upgrade}`,
|
|
2622
|
+
help: "Check the API key and your plan, then try again."
|
|
2623
|
+
});
|
|
2381
2624
|
}
|
|
2382
|
-
if (!_data?.snapshot) throw new Error(
|
|
2625
|
+
if (!_data?.snapshot) throw new Diagnostics.Error({
|
|
2626
|
+
code: Diagnostics.code.inputRequestFailed,
|
|
2627
|
+
severity: "error",
|
|
2628
|
+
message: `Kubb Studio returned no snapshot (status ${status})`,
|
|
2629
|
+
help: "Try again later."
|
|
2630
|
+
});
|
|
2383
2631
|
return _data.snapshot;
|
|
2384
2632
|
}
|
|
2385
2633
|
//#endregion
|