@kubb/studio 5.11.2 → 5.11.4
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 +3 -3
- package/dist/index.cjs +147 -88
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +150 -91
- package/dist/index.js.map +1 -1
- package/dist/{protocol-BUZ91Gd4.d.ts → protocol-a_DcTFba.d.cts} +14 -39
- package/dist/{protocol-BUZ91Gd4.d.cts → protocol-a_DcTFba.d.ts} +14 -39
- package/dist/protocol.cjs +8 -9
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.cts +2 -2
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +9 -9
- package/dist/protocol.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
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-a_DcTFba.cjs";
|
|
2
2
|
import { Config, Hookable, KubbHooks } from "@kubb/core";
|
|
3
3
|
//#region src/connect.d.ts
|
|
4
4
|
/**
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
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-a_DcTFba.js";
|
|
2
2
|
import { Config, Hookable, KubbHooks } from "@kubb/core";
|
|
3
3
|
//#region src/connect.d.ts
|
|
4
4
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission,
|
|
1
|
+
import { AGENT_INSTANCE_HEADER, AGENT_SOCKET_PATH, AgentCloseCode, AgentMessage, AgentPermission, MAX_SNAPSHOT_BYTES, generationEventTypes } from "./protocol.js";
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { createHash, hash, randomUUID } from "node:crypto";
|
|
4
|
-
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
5
4
|
import { tmpdir } from "node:os";
|
|
6
5
|
import path, { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
7
6
|
import process$1 from "node:process";
|
|
8
7
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
9
8
|
import { pathToFileURL } from "node:url";
|
|
10
9
|
import { existsSync } from "node:fs";
|
|
11
|
-
import {
|
|
10
|
+
import { mkdir, mkdir as mkdir$1, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
11
|
+
import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, getInputKind, isPackageName, readInstalledPackages, resolveCacheDir, resolveMeta } from "@kubb/core";
|
|
12
12
|
import { RpcTarget, newWebSocketRpcSession } from "capnweb";
|
|
13
13
|
import { detectCodeFormat, parseExpression, parseModule } from "magicast";
|
|
14
14
|
import { structuredPatch } from "diff";
|
|
@@ -117,6 +117,27 @@ async function read(path) {
|
|
|
117
117
|
return readFile(path, { encoding: "utf8" });
|
|
118
118
|
}
|
|
119
119
|
/**
|
|
120
|
+
* Writes UTF-8 content verbatim, including empty files and surrounding whitespace.
|
|
121
|
+
* Uses Bun.write under Bun and creates missing parent directories under either runtime.
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* await writeRaw('./cache/output.ts', ' // keep these bytes\n\n')
|
|
125
|
+
*/
|
|
126
|
+
async function writeRaw(path, content) {
|
|
127
|
+
const resolved = resolve(path);
|
|
128
|
+
if (runtime.isBun) {
|
|
129
|
+
await Bun.write(resolved, content);
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await writeFile(resolved, content, { encoding: "utf-8" });
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error.code !== "ENOENT") throw error;
|
|
136
|
+
await mkdir(dirname(resolved), { recursive: true });
|
|
137
|
+
await writeFile(resolved, content, { encoding: "utf-8" });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
120
141
|
* Resolves to `true` when `path` is `parent` itself or nested inside it. Both sides are resolved
|
|
121
142
|
* to absolute paths first, so relative and `..`-containing inputs compare correctly.
|
|
122
143
|
*
|
|
@@ -175,7 +196,7 @@ function getElapsedMs(hrStart) {
|
|
|
175
196
|
}
|
|
176
197
|
//#endregion
|
|
177
198
|
//#region package.json
|
|
178
|
-
var version = "5.11.
|
|
199
|
+
var version = "5.11.4";
|
|
179
200
|
//#endregion
|
|
180
201
|
//#region src/resolveConfig.ts
|
|
181
202
|
/**
|
|
@@ -1371,21 +1392,20 @@ const hashOf = (content) => createHash("sha1").update(content).digest("hex").sli
|
|
|
1371
1392
|
/**
|
|
1372
1393
|
* The bytes `entries` keep on disk: output, written files, and kept input.
|
|
1373
1394
|
*/
|
|
1374
|
-
const totalBytes = (entries) => entries.reduce((sum, { output,
|
|
1395
|
+
const totalBytes = (entries) => entries.reduce((sum, { output, inputBytes }) => sum + output.bytes + (inputBytes ?? 0), 0);
|
|
1375
1396
|
function positiveNumber(value) {
|
|
1376
1397
|
const parsed = Number(value);
|
|
1377
1398
|
return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
1378
1399
|
}
|
|
1379
1400
|
/**
|
|
1380
1401
|
* How many generations an agent keeps and how large they may get, read from
|
|
1381
|
-
* `KUBB_AGENT_MAX_GENERATIONS
|
|
1402
|
+
* `KUBB_AGENT_MAX_GENERATIONS` and `KUBB_AGENT_MAX_GENERATIONS_MB`.
|
|
1382
1403
|
* An unset or invalid value keeps the default.
|
|
1383
1404
|
*/
|
|
1384
1405
|
function resolveGenerationLimits() {
|
|
1385
1406
|
return {
|
|
1386
1407
|
maxCount: Math.max(1, Math.floor(positiveNumber(process$1.env.KUBB_AGENT_MAX_GENERATIONS) ?? 8)),
|
|
1387
|
-
maxMb: positiveNumber(process$1.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100
|
|
1388
|
-
maxSnapshotMb: positiveNumber(process$1.env.KUBB_AGENT_MAX_SNAPSHOT_MB) ?? 50
|
|
1408
|
+
maxMb: positiveNumber(process$1.env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? 100
|
|
1389
1409
|
};
|
|
1390
1410
|
}
|
|
1391
1411
|
/**
|
|
@@ -1432,7 +1452,7 @@ async function generate({ config, hooks, signal, allowExec }) {
|
|
|
1432
1452
|
};
|
|
1433
1453
|
}
|
|
1434
1454
|
/**
|
|
1435
|
-
* What the output directory holds
|
|
1455
|
+
* What the output directory holds when the editor connects. `undefined` when `outputPath` is not a real
|
|
1436
1456
|
* subdirectory of `root` (listing the root would take in every source file) or holds more than `maxFiles`.
|
|
1437
1457
|
*/
|
|
1438
1458
|
async function listDisk({ root, outputPath, maxFiles }) {
|
|
@@ -1452,8 +1472,8 @@ async function listDisk({ root, outputPath, maxFiles }) {
|
|
|
1452
1472
|
/**
|
|
1453
1473
|
* Keeps recent generations by job id in `storage`, with an index next to them so a store on disk
|
|
1454
1474
|
* survives a restart. A generation is only ever looked up by its job id, so on a pooled sandbox one
|
|
1455
|
-
* tenant never reaches another's output.
|
|
1456
|
-
* always
|
|
1475
|
+
* tenant never reaches another's output. Initial disk output and generated jobs each get their
|
|
1476
|
+
* own `maxCount` and `maxMb` budget. Each budget always keeps its newest entry.
|
|
1457
1477
|
*/
|
|
1458
1478
|
function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now }) {
|
|
1459
1479
|
let index;
|
|
@@ -1472,7 +1492,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1472
1492
|
/**
|
|
1473
1493
|
* Copies `files` into the store as one set of `jobId`. Above `maxSetMb` only the hashes are kept.
|
|
1474
1494
|
*/
|
|
1475
|
-
async function keep({ jobId,
|
|
1495
|
+
async function keep({ jobId, files, maxSetMb }) {
|
|
1476
1496
|
const maxSetBytes = maxSetMb * MB;
|
|
1477
1497
|
const hashes = {};
|
|
1478
1498
|
let bytes = 0;
|
|
@@ -1483,14 +1503,13 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1483
1503
|
if (path.split("/").includes("..")) return;
|
|
1484
1504
|
const content = await files.storage.readItem(resolve(files.root, path));
|
|
1485
1505
|
if (content === null) return;
|
|
1486
|
-
|
|
1487
|
-
hashes[path] = hashOf(normalize ? `${content.trim()}\n` : content);
|
|
1506
|
+
hashes[path] = hashOf(content);
|
|
1488
1507
|
bytes += Buffer.byteLength(content);
|
|
1489
|
-
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}
|
|
1508
|
+
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}output/${path}`, content);
|
|
1490
1509
|
}
|
|
1491
1510
|
});
|
|
1492
1511
|
if (bytes > maxSetBytes) {
|
|
1493
|
-
await storage.empty(`${dirOf(jobId)}
|
|
1512
|
+
await storage.empty(`${dirOf(jobId)}output/`);
|
|
1494
1513
|
return {
|
|
1495
1514
|
paths: [],
|
|
1496
1515
|
hashes,
|
|
@@ -1506,10 +1525,50 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1506
1525
|
async function drop(jobId) {
|
|
1507
1526
|
await storage.empty(dirOf(jobId));
|
|
1508
1527
|
}
|
|
1528
|
+
async function add(generation) {
|
|
1529
|
+
const current = await load();
|
|
1530
|
+
for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
|
|
1531
|
+
const live = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
|
|
1532
|
+
const sharesBudget = (entry) => Boolean(entry.initial) === Boolean(generation.initial);
|
|
1533
|
+
const entries = live.filter(sharesBudget);
|
|
1534
|
+
entries.push({
|
|
1535
|
+
...generation,
|
|
1536
|
+
keptAt: now()
|
|
1537
|
+
});
|
|
1538
|
+
while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
|
|
1539
|
+
index = [...live.filter((entry) => !sharesBudget(entry)), ...entries];
|
|
1540
|
+
await storage.writeItem(INDEX_KEY, JSON.stringify(index));
|
|
1541
|
+
}
|
|
1509
1542
|
return {
|
|
1510
1543
|
keep,
|
|
1511
1544
|
drop,
|
|
1512
1545
|
/**
|
|
1546
|
+
* Captures initial disk output, reusing a retained ID when its hashes and readable paths match.
|
|
1547
|
+
* A temporary candidate is discarded on reuse, so reconnects do not consume retention slots.
|
|
1548
|
+
*/
|
|
1549
|
+
async keepInitial(files) {
|
|
1550
|
+
const jobId = randomUUID();
|
|
1551
|
+
const output = await keep({
|
|
1552
|
+
jobId,
|
|
1553
|
+
files,
|
|
1554
|
+
maxSetMb: maxMb
|
|
1555
|
+
});
|
|
1556
|
+
const hashes = Object.entries(output.hashes);
|
|
1557
|
+
const previous = (await load()).findLast((entry) => entry.initial && isLive(entry) && entry.output.paths.length === output.paths.length && Object.keys(entry.output.hashes).length === hashes.length && hashes.every(([path, hash]) => entry.output.hashes[path] === hash));
|
|
1558
|
+
if (previous) {
|
|
1559
|
+
await drop(jobId);
|
|
1560
|
+
await add(previous);
|
|
1561
|
+
return previous;
|
|
1562
|
+
}
|
|
1563
|
+
const generation = {
|
|
1564
|
+
jobId,
|
|
1565
|
+
output,
|
|
1566
|
+
initial: true
|
|
1567
|
+
};
|
|
1568
|
+
await add(generation);
|
|
1569
|
+
return generation;
|
|
1570
|
+
},
|
|
1571
|
+
/**
|
|
1513
1572
|
* Keeps the `InputNode` of `jobId` and returns its size in bytes, or 0 when it keeps none. A ref schema loses
|
|
1514
1573
|
* the copy of its target, which `input.schemas` holds: with it, a large spec outgrows what `JSON.stringify` can return.
|
|
1515
1574
|
*/
|
|
@@ -1537,30 +1596,19 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1537
1596
|
return stored === null ? void 0 : JSON.parse(stored);
|
|
1538
1597
|
},
|
|
1539
1598
|
get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId && isLive(generation)),
|
|
1540
|
-
|
|
1541
|
-
const current = await load();
|
|
1542
|
-
for (const expired of current.filter((entry) => !isLive(entry))) await drop(expired.jobId);
|
|
1543
|
-
const entries = current.filter((entry) => entry.jobId !== generation.jobId && isLive(entry));
|
|
1544
|
-
entries.push({
|
|
1545
|
-
...generation,
|
|
1546
|
-
keptAt: now()
|
|
1547
|
-
});
|
|
1548
|
-
while (entries.length > 1 && (entries.length > maxCount || totalBytes(entries) > maxMb * MB)) await drop(entries.shift().jobId);
|
|
1549
|
-
index = entries;
|
|
1550
|
-
await storage.writeItem(INDEX_KEY, JSON.stringify(entries));
|
|
1551
|
-
},
|
|
1599
|
+
add,
|
|
1552
1600
|
/**
|
|
1553
1601
|
* Reads the requested paths the set holds, skipping any it does not, one page of lines when `limit` is given.
|
|
1554
1602
|
*/
|
|
1555
|
-
async read({ generation,
|
|
1556
|
-
const kept = new Set(generation
|
|
1603
|
+
async read({ generation, paths, cursor = 0, limit }) {
|
|
1604
|
+
const kept = new Set(generation.output.paths);
|
|
1557
1605
|
const files = {};
|
|
1558
1606
|
const pages = {};
|
|
1559
1607
|
await inParallel({
|
|
1560
1608
|
items: paths.filter((path) => kept.has(path)),
|
|
1561
1609
|
limit: READ_CONCURRENCY,
|
|
1562
1610
|
run: async (path) => {
|
|
1563
|
-
const content = await storage.readItem(`${dirOf(generation.jobId)}
|
|
1611
|
+
const content = await storage.readItem(`${dirOf(generation.jobId)}output/${path}`);
|
|
1564
1612
|
if (content === null) return;
|
|
1565
1613
|
if (limit === void 0) {
|
|
1566
1614
|
files[path] = content;
|
|
@@ -1835,6 +1883,25 @@ function isValidPage({ cursor, limit }) {
|
|
|
1835
1883
|
if (cursor !== void 0 && (!Number.isSafeInteger(cursor) || cursor < 0)) return false;
|
|
1836
1884
|
return Number.isSafeInteger(limit) && limit >= 1 && limit <= 2e3;
|
|
1837
1885
|
}
|
|
1886
|
+
/**
|
|
1887
|
+
* Maps generated paths into a temporary directory while keeping the project root for input resolution.
|
|
1888
|
+
*/
|
|
1889
|
+
function stagingStorage({ root, directory }) {
|
|
1890
|
+
const storage = fsStorage();
|
|
1891
|
+
const target = (key) => {
|
|
1892
|
+
const relative = path.relative(root, path.resolve(root, key));
|
|
1893
|
+
if (relative === ".." || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) throw new Error("Output path escapes the project root");
|
|
1894
|
+
return path.join(directory, relative);
|
|
1895
|
+
};
|
|
1896
|
+
return {
|
|
1897
|
+
name: "fs",
|
|
1898
|
+
readItem: (key) => storage.readItem(target(key)),
|
|
1899
|
+
writeItem: (key, value) => storage.writeItem(target(key), value),
|
|
1900
|
+
removeItem: (key) => storage.removeItem(target(key)),
|
|
1901
|
+
readKeys: (base) => storage.readKeys(base ? target(base) : directory),
|
|
1902
|
+
empty: (base) => base ? storage.empty(target(base)) : Promise.resolve()
|
|
1903
|
+
};
|
|
1904
|
+
}
|
|
1838
1905
|
var GenerationRunTarget = class extends RpcTarget {
|
|
1839
1906
|
#run;
|
|
1840
1907
|
constructor(run) {
|
|
@@ -1859,6 +1926,7 @@ var Agent = class extends RpcTarget {
|
|
|
1859
1926
|
#session;
|
|
1860
1927
|
#limits = resolveGenerationLimits();
|
|
1861
1928
|
#store;
|
|
1929
|
+
#sandboxCacheRoot = path.join(tmpdir(), `kubb-studio-${randomUUID()}`);
|
|
1862
1930
|
#diffs = /* @__PURE__ */ new Map();
|
|
1863
1931
|
constructor({ options, session }) {
|
|
1864
1932
|
super();
|
|
@@ -1880,13 +1948,19 @@ var Agent = class extends RpcTarget {
|
|
|
1880
1948
|
}
|
|
1881
1949
|
get #generations() {
|
|
1882
1950
|
this.#store ??= createGenerationStore({
|
|
1883
|
-
storage: this.#sandbox ?
|
|
1951
|
+
storage: cacheStorage({ root: this.#sandbox ? this.#sandboxCacheRoot : this.#options.root }),
|
|
1884
1952
|
maxCount: this.#limits.maxCount,
|
|
1885
1953
|
maxMb: this.#limits.maxMb,
|
|
1886
1954
|
ttlMs: this.#sandbox ? SANDBOX_GENERATION_TTL_MS : void 0
|
|
1887
1955
|
});
|
|
1888
1956
|
return this.#store;
|
|
1889
1957
|
}
|
|
1958
|
+
async [Symbol.asyncDispose]() {
|
|
1959
|
+
if (this.#sandbox) await rm(resolveCacheDir(this.#sandboxCacheRoot), {
|
|
1960
|
+
recursive: true,
|
|
1961
|
+
force: true
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1890
1964
|
#warn({ message, permission }) {
|
|
1891
1965
|
return emit(this.#options.hooks, "studio:warn", {
|
|
1892
1966
|
message,
|
|
@@ -1998,18 +2072,7 @@ var Agent = class extends RpcTarget {
|
|
|
1998
2072
|
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
1999
2073
|
});
|
|
2000
2074
|
if (!files) return void 0;
|
|
2001
|
-
const jobId =
|
|
2002
|
-
const output = await this.#generations.keep({
|
|
2003
|
-
jobId,
|
|
2004
|
-
source: FileSource.OUTPUT,
|
|
2005
|
-
files,
|
|
2006
|
-
maxSetMb: this.#limits.maxSnapshotMb,
|
|
2007
|
-
normalize: false
|
|
2008
|
-
});
|
|
2009
|
-
await this.#generations.add({
|
|
2010
|
-
jobId,
|
|
2011
|
-
output
|
|
2012
|
-
});
|
|
2075
|
+
const { jobId, output } = await this.#generations.keepInitial(files);
|
|
2013
2076
|
return {
|
|
2014
2077
|
jobId,
|
|
2015
2078
|
files: output.paths,
|
|
@@ -2033,7 +2096,9 @@ var Agent = class extends RpcTarget {
|
|
|
2033
2096
|
}
|
|
2034
2097
|
});
|
|
2035
2098
|
}
|
|
2036
|
-
/**
|
|
2099
|
+
/**
|
|
2100
|
+
* Keeps a job's output and permitted parsed input, then cleans up its temporary generation directories.
|
|
2101
|
+
*/
|
|
2037
2102
|
async #runGeneration(input, controller) {
|
|
2038
2103
|
this.#assertConnected();
|
|
2039
2104
|
if (this.#session.closed) throw new Diagnostics.Error({
|
|
@@ -2041,7 +2106,7 @@ var Agent = class extends RpcTarget {
|
|
|
2041
2106
|
severity: "error",
|
|
2042
2107
|
message: "Connection is closed"
|
|
2043
2108
|
});
|
|
2044
|
-
if (this.#session.active) return this.#refuse({ message:
|
|
2109
|
+
if (this.#session.active) return this.#refuse({ message: AgentMessage.GENERATION_IN_PROGRESS });
|
|
2045
2110
|
const settled = Promise.withResolvers();
|
|
2046
2111
|
this.#session.active = {
|
|
2047
2112
|
jobId: input.jobId,
|
|
@@ -2049,11 +2114,14 @@ var Agent = class extends RpcTarget {
|
|
|
2049
2114
|
settled: settled.promise
|
|
2050
2115
|
};
|
|
2051
2116
|
let root = this.#options.root;
|
|
2117
|
+
let outputRoot;
|
|
2052
2118
|
try {
|
|
2053
2119
|
if (this.#sandbox) root = await mkdtemp(path.join(tmpdir(), "kubb-job-"));
|
|
2120
|
+
if (!this.#sandbox && !this.#permissions.allowWrite) outputRoot = await mkdtemp(path.join(tmpdir(), "kubb-output-"));
|
|
2054
2121
|
return await this.#generate({
|
|
2055
2122
|
input,
|
|
2056
2123
|
root,
|
|
2124
|
+
outputRoot,
|
|
2057
2125
|
signal: controller.signal
|
|
2058
2126
|
});
|
|
2059
2127
|
} finally {
|
|
@@ -2064,15 +2132,21 @@ var Agent = class extends RpcTarget {
|
|
|
2064
2132
|
recursive: true,
|
|
2065
2133
|
force: true
|
|
2066
2134
|
})]).catch(() => {});
|
|
2135
|
+
if (outputRoot) await rm(outputRoot, {
|
|
2136
|
+
recursive: true,
|
|
2137
|
+
force: true
|
|
2138
|
+
});
|
|
2067
2139
|
this.#session.active = void 0;
|
|
2068
2140
|
settled.resolve();
|
|
2069
2141
|
}
|
|
2070
2142
|
}
|
|
2071
|
-
/**
|
|
2072
|
-
|
|
2143
|
+
/**
|
|
2144
|
+
* Merges Studio's config over the project config and confines generation to the configured output folder.
|
|
2145
|
+
*/
|
|
2146
|
+
async #generate({ input, root, outputRoot, signal }) {
|
|
2073
2147
|
const { jobId, config: patch } = input;
|
|
2074
2148
|
const studioInput = this.#sandbox ? patch?.input : void 0;
|
|
2075
|
-
if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || getInputKind(studioInput) !== "inline")) return this.#refuse({ message:
|
|
2149
|
+
if (this.#sandbox && (typeof studioInput !== "string" || !studioInput.trim() || getInputKind(studioInput) !== "inline")) return this.#refuse({ message: AgentMessage.SANDBOX_INPUT_ONLY });
|
|
2076
2150
|
const [config, extensions] = await Promise.all([this.#options.loadConfig(), this.#installedExtensions()]);
|
|
2077
2151
|
const [plugins, adapter, parsers] = await Promise.all([
|
|
2078
2152
|
mergePlugins({
|
|
@@ -2100,37 +2174,28 @@ var Agent = class extends RpcTarget {
|
|
|
2100
2174
|
});
|
|
2101
2175
|
const activePlugins = plugins ?? config.plugins;
|
|
2102
2176
|
if (patch?.input && !this.#sandbox) await emit(this.#options.hooks, "kubb:warn", { message: "Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox agent" });
|
|
2103
|
-
const diskFiles = !this.#sandbox && this.#permissions.allowRead ? await listDisk({
|
|
2104
|
-
root,
|
|
2105
|
-
outputPath: config.output.path,
|
|
2106
|
-
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2107
|
-
}) : void 0;
|
|
2108
|
-
const disk = diskFiles ? await this.#generations.keep({
|
|
2109
|
-
jobId,
|
|
2110
|
-
source: FileSource.DISK,
|
|
2111
|
-
files: diskFiles,
|
|
2112
|
-
maxSetMb: this.#limits.maxSnapshotMb
|
|
2113
|
-
}) : void 0;
|
|
2114
2177
|
const files = await generate({
|
|
2115
2178
|
config: {
|
|
2116
2179
|
...config,
|
|
2117
2180
|
root,
|
|
2118
2181
|
input: studioInput ?? config.input,
|
|
2119
|
-
storage:
|
|
2182
|
+
storage: outputRoot ? stagingStorage({
|
|
2183
|
+
root,
|
|
2184
|
+
directory: outputRoot
|
|
2185
|
+
}) : fsStorage(),
|
|
2120
2186
|
plugins: activePlugins,
|
|
2121
2187
|
adapter,
|
|
2122
2188
|
parsers
|
|
2123
2189
|
},
|
|
2124
2190
|
hooks: this.#options.hooks,
|
|
2125
2191
|
signal,
|
|
2126
|
-
allowExec: this.#permissions.allowExec
|
|
2192
|
+
allowExec: this.#permissions.allowWrite && this.#permissions.allowExec
|
|
2127
2193
|
}).catch(async (error) => {
|
|
2128
2194
|
await this.#generations.drop(jobId);
|
|
2129
2195
|
throw error;
|
|
2130
2196
|
});
|
|
2131
2197
|
const output = await this.#generations.keep({
|
|
2132
2198
|
jobId,
|
|
2133
|
-
source: FileSource.OUTPUT,
|
|
2134
2199
|
files,
|
|
2135
2200
|
maxSetMb: this.#limits.maxMb
|
|
2136
2201
|
});
|
|
@@ -2143,16 +2208,14 @@ var Agent = class extends RpcTarget {
|
|
|
2143
2208
|
await this.#generations.add({
|
|
2144
2209
|
jobId,
|
|
2145
2210
|
output,
|
|
2146
|
-
disk,
|
|
2147
2211
|
inputBytes
|
|
2148
2212
|
});
|
|
2149
|
-
await this.#logCommand("generate", `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "
|
|
2213
|
+
await this.#logCommand("generate", `${activePlugins.length} plugin${activePlugins.length === 1 ? "" : "s"}, ${this.#permissions.allowWrite ? "written to disk" : "staged on disk"}${studioInput !== void 0 ? ", from a Studio spec" : ""}`);
|
|
2150
2214
|
return {
|
|
2151
2215
|
status: "success",
|
|
2152
2216
|
files: [...files.paths],
|
|
2153
2217
|
fileCount: files.paths.size,
|
|
2154
|
-
hashes: output.hashes
|
|
2155
|
-
disk: disk ? { hashes: disk.hashes } : void 0
|
|
2218
|
+
hashes: output.hashes
|
|
2156
2219
|
};
|
|
2157
2220
|
}
|
|
2158
2221
|
async saveConfig(input) {
|
|
@@ -2205,7 +2268,7 @@ var Agent = class extends RpcTarget {
|
|
|
2205
2268
|
message: AgentMessage.READ_AST_NOT_ALLOWED,
|
|
2206
2269
|
permission: AgentPermission.READ_AST
|
|
2207
2270
|
});
|
|
2208
|
-
if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message:
|
|
2271
|
+
if (typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: AgentMessage.JOB_REQUIRED });
|
|
2209
2272
|
const generation = await this.#generations.get(input.jobId);
|
|
2210
2273
|
if (!generation) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
|
|
2211
2274
|
const kept = generation.inputBytes ? await this.#generations.readInput(input.jobId) : void 0;
|
|
@@ -2222,17 +2285,14 @@ var Agent = class extends RpcTarget {
|
|
|
2222
2285
|
message: AgentMessage.READ_NOT_ALLOWED,
|
|
2223
2286
|
permission: AgentPermission.READ
|
|
2224
2287
|
});
|
|
2225
|
-
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message:
|
|
2288
|
+
if (!Array.isArray(input.paths) || typeof input.jobId !== "string" || !input.jobId) return this.#refuse({ message: AgentMessage.FILE_REQUEST_INVALID });
|
|
2226
2289
|
const { paths } = input;
|
|
2227
|
-
if (paths.length > 50) return this.#refuse({ message:
|
|
2290
|
+
if (paths.length > 50) return this.#refuse({ message: AgentMessage.TOO_MANY_FILES });
|
|
2291
|
+
if (!isValidPage(input)) return this.#refuse({ message: AgentMessage.INVALID_PAGE });
|
|
2228
2292
|
const generation = await this.#generations.get(input.jobId);
|
|
2229
2293
|
if (!generation) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
|
|
2230
|
-
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` });
|
|
2231
|
-
const source = input.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
|
|
2232
|
-
if (!generation[source]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
|
|
2233
2294
|
const result = await this.#generations.read({
|
|
2234
2295
|
generation,
|
|
2235
|
-
source,
|
|
2236
2296
|
paths,
|
|
2237
2297
|
cursor: input.cursor,
|
|
2238
2298
|
limit: input.limit
|
|
@@ -2250,26 +2310,22 @@ var Agent = class extends RpcTarget {
|
|
|
2250
2310
|
permission: AgentPermission.READ
|
|
2251
2311
|
});
|
|
2252
2312
|
const { jobId, path, base, cursor = 0, limit } = input;
|
|
2253
|
-
if (typeof jobId !== "string" || !jobId || typeof path !== "string" || !path || typeof base?.jobId !== "string" || !base.jobId) return this.#refuse({ message:
|
|
2313
|
+
if (typeof jobId !== "string" || !jobId || typeof path !== "string" || !path || typeof base?.jobId !== "string" || !base.jobId) return this.#refuse({ message: AgentMessage.DIFF_REQUEST_INVALID });
|
|
2254
2314
|
if (!isValidPage({
|
|
2255
2315
|
cursor,
|
|
2256
2316
|
limit
|
|
2257
|
-
})) return this.#refuse({ message:
|
|
2317
|
+
})) return this.#refuse({ message: AgentMessage.INVALID_PAGE });
|
|
2258
2318
|
const [generation, baseGeneration] = await Promise.all([this.#generations.get(jobId), this.#generations.get(base.jobId)]);
|
|
2259
2319
|
if (!generation || !baseGeneration) return this.#refuse({ message: AgentMessage.GENERATION_GONE });
|
|
2260
|
-
const baseSource = base.source === FileSource.DISK ? FileSource.DISK : FileSource.OUTPUT;
|
|
2261
|
-
if (!baseGeneration[baseSource]) return this.#refuse({ message: "This agent kept no snapshot of the files on disk for that job" });
|
|
2262
2320
|
const [after, before] = await Promise.all([this.#generations.read({
|
|
2263
2321
|
generation,
|
|
2264
|
-
source: FileSource.OUTPUT,
|
|
2265
2322
|
paths: [path]
|
|
2266
2323
|
}), this.#generations.read({
|
|
2267
2324
|
generation: baseGeneration,
|
|
2268
|
-
source: baseSource,
|
|
2269
2325
|
paths: [path]
|
|
2270
2326
|
})]);
|
|
2271
2327
|
const afterContent = after.files[path];
|
|
2272
|
-
if (afterContent === void 0 && path in generation.output.hashes) return this.#refuse({ message:
|
|
2328
|
+
if (afterContent === void 0 && path in generation.output.hashes) return this.#refuse({ message: AgentMessage.HASHES_ONLY });
|
|
2273
2329
|
const beforeContent = before.files[path];
|
|
2274
2330
|
if (beforeContent === void 0) return {
|
|
2275
2331
|
hunks: [],
|
|
@@ -2286,7 +2342,6 @@ var Agent = class extends RpcTarget {
|
|
|
2286
2342
|
const key = JSON.stringify([
|
|
2287
2343
|
jobId,
|
|
2288
2344
|
base.jobId,
|
|
2289
|
-
baseSource,
|
|
2290
2345
|
path
|
|
2291
2346
|
]);
|
|
2292
2347
|
let diff = this.#diffs.get(key);
|
|
@@ -2343,14 +2398,15 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2343
2398
|
connected: Promise.withResolvers(),
|
|
2344
2399
|
closed: false
|
|
2345
2400
|
};
|
|
2401
|
+
const agent = new Agent({
|
|
2402
|
+
options,
|
|
2403
|
+
session
|
|
2404
|
+
});
|
|
2346
2405
|
const rpc = openRpc({
|
|
2347
2406
|
url,
|
|
2348
2407
|
token,
|
|
2349
2408
|
instanceId,
|
|
2350
|
-
agent
|
|
2351
|
-
options,
|
|
2352
|
-
session
|
|
2353
|
-
})
|
|
2409
|
+
agent
|
|
2354
2410
|
});
|
|
2355
2411
|
signal?.addEventListener("abort", rpc.close, { once: true });
|
|
2356
2412
|
let timer;
|
|
@@ -2385,6 +2441,7 @@ async function runSession({ options, openRpc, url, instanceId }) {
|
|
|
2385
2441
|
await emit(hooks, "studio:warn", { message: "Canceled the running generation: the connection to Kubb Studio closed" });
|
|
2386
2442
|
}
|
|
2387
2443
|
await active?.settled;
|
|
2444
|
+
await agent[Symbol.asyncDispose]();
|
|
2388
2445
|
return {
|
|
2389
2446
|
...close,
|
|
2390
2447
|
connected: session.info !== void 0
|
|
@@ -2692,10 +2749,9 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2692
2749
|
const root = await mkdtemp(join(tmpdir(), "kubb-snapshot-"));
|
|
2693
2750
|
const dist = join(root, "dist");
|
|
2694
2751
|
try {
|
|
2695
|
-
await mkdir(dist);
|
|
2752
|
+
await mkdir$1(dist);
|
|
2696
2753
|
await Promise.all([...sources].map(async ([path, { content }]) => {
|
|
2697
|
-
await
|
|
2698
|
-
await writeFile(join(root, path), content);
|
|
2754
|
+
await writeRaw(join(root, path), content);
|
|
2699
2755
|
}));
|
|
2700
2756
|
const entry = [...sources.keys()].filter((path) => /\.(?:[cm]?[jt]sx?)$/.test(path)).map((path) => join(root, path));
|
|
2701
2757
|
if (entry.length) {
|
|
@@ -2735,7 +2791,7 @@ async function createSnapshotPackage({ files, packageInfo }) {
|
|
|
2735
2791
|
const chunks = [...new Map([
|
|
2736
2792
|
["package.json", JSON.stringify(manifest, null, 2)],
|
|
2737
2793
|
...[...sources].map(([path, { content }]) => [path, content]),
|
|
2738
|
-
...await Promise.all(built.map(async (key) => [`dist/${key}`, await
|
|
2794
|
+
...await Promise.all(built.map(async (key) => [`dist/${key}`, await read(join(dist, key))]))
|
|
2739
2795
|
])].flatMap(([path, content]) => {
|
|
2740
2796
|
const bytes = Buffer.from(content);
|
|
2741
2797
|
return [
|
|
@@ -2766,7 +2822,10 @@ async function createSnapshot({ studioUrl = defaultStudioUrl, apiKey, config, ho
|
|
|
2766
2822
|
path: "/api/snapshots"
|
|
2767
2823
|
});
|
|
2768
2824
|
const generated = await generate({
|
|
2769
|
-
config
|
|
2825
|
+
config: {
|
|
2826
|
+
...config,
|
|
2827
|
+
storage: fsStorage()
|
|
2828
|
+
},
|
|
2770
2829
|
hooks,
|
|
2771
2830
|
signal,
|
|
2772
2831
|
allowExec: true
|