@kubb/studio 5.3.12 → 5.3.14
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/dist/index.cjs +208 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +210 -86
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs +5 -0
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.ts +36 -17
- package/dist/protocol.js +5 -1
- package/dist/protocol.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
|
|
2
|
-
import { AgentApi, AgentPermissions, ClientInfo, ConfigEdit, ConnectMessagePayload,
|
|
2
|
+
import { AgentApi, AgentPermissions, ClientInfo, ConfigEdit, ConnectMessagePayload, GenerateInput, GenerateResult, GenerationEvent, GenerationEventPayloads, GenerationEventType, GenerationRun, PublishSnapshotInput, PublishSnapshotResult, RpcConnection, RpcConnector, StudioApi, generationEventTypes } from "./protocol.js";
|
|
3
3
|
import { Config, Hookable, KubbHooks } from "@kubb/core";
|
|
4
4
|
import { Storage } from "unstorage";
|
|
5
5
|
//#region src/api.d.ts
|
|
@@ -554,5 +554,5 @@ export declare function pollForPairingToken({ studioUrl, session, signal }: Poll
|
|
|
554
554
|
*/
|
|
555
555
|
export declare const connectWebSocketRpc: RpcConnector;
|
|
556
556
|
//#endregion
|
|
557
|
-
export { type AgentApi, type Client, type ClientOptions, type ConfigEdit, type ConnectMessagePayload, type ConnectionOptions, type
|
|
557
|
+
export { type AgentApi, type Client, type ClientOptions, type ConfigEdit, type ConnectMessagePayload, type ConnectionOptions, type GenerateInput, type GenerateResult, type GenerationEvent, type GenerationEventPayloads, type GenerationEventType, type GenerationRun, type PublishSnapshotInput, type PublishSnapshotResult, type RpcConnection, type RpcConnector, type StudioAgent, type StudioApi, type StudioConnectedContext, type StudioJob, type StudioJobStatus, type StudioSnapshot, generationEventTypes };
|
|
558
558
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
|
|
2
|
-
import { generationEventTypes } from "./protocol.js";
|
|
2
|
+
import { GENERATION_GONE_MESSAGE, generationEventTypes } from "./protocol.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { promisify, styleText } from "node:util";
|
|
5
5
|
import process$1 from "node:process";
|
|
6
6
|
import { spawn } from "node:child_process";
|
|
7
7
|
import { glob, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
8
8
|
import path, { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
|
-
import { Diagnostics, Hookable, createKubb, fsStorage, logLevel, memoryStorage } from "@kubb/core";
|
|
9
|
+
import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, logLevel, memoryStorage } from "@kubb/core";
|
|
10
10
|
import { FetchError, ofetch } from "ofetch";
|
|
11
11
|
import { createHash, hash, randomBytes } from "node:crypto";
|
|
12
12
|
import { createStorage } from "unstorage";
|
|
@@ -44,8 +44,27 @@ const agentDefaults = {
|
|
|
44
44
|
maxHeartbeatIntervalMs: 6e4,
|
|
45
45
|
/** How long a heartbeat ping may take before the session is treated as dead. */
|
|
46
46
|
heartbeatTimeoutMs: 1e4,
|
|
47
|
-
poolSize: 1
|
|
47
|
+
poolSize: 1,
|
|
48
|
+
maxGenerations: 8,
|
|
49
|
+
maxGenerationsMb: 100,
|
|
50
|
+
maxSnapshotMb: 50
|
|
48
51
|
};
|
|
52
|
+
function positiveNumber(value) {
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
return value && Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* How many generations an agent keeps and how large they may get, read from
|
|
58
|
+
* `KUBB_AGENT_MAX_GENERATIONS`, `KUBB_AGENT_MAX_GENERATIONS_MB` and `KUBB_AGENT_MAX_SNAPSHOT_MB`.
|
|
59
|
+
* An unset or invalid value keeps the default.
|
|
60
|
+
*/
|
|
61
|
+
function resolveGenerationLimits(env = process.env) {
|
|
62
|
+
return {
|
|
63
|
+
maxCount: Math.max(1, Math.floor(positiveNumber(env.KUBB_AGENT_MAX_GENERATIONS) ?? agentDefaults.maxGenerations)),
|
|
64
|
+
maxMb: positiveNumber(env.KUBB_AGENT_MAX_GENERATIONS_MB) ?? agentDefaults.maxGenerationsMb,
|
|
65
|
+
maxSnapshotMb: positiveNumber(env.KUBB_AGENT_MAX_SNAPSHOT_MB) ?? agentDefaults.maxSnapshotMb
|
|
66
|
+
};
|
|
67
|
+
}
|
|
49
68
|
//#endregion
|
|
50
69
|
//#region ../../internals/utils/src/casing.ts
|
|
51
70
|
/**
|
|
@@ -676,7 +695,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
|
|
|
676
695
|
}
|
|
677
696
|
//#endregion
|
|
678
697
|
//#region package.json
|
|
679
|
-
var version = "5.3.
|
|
698
|
+
var version = "5.3.14";
|
|
680
699
|
//#endregion
|
|
681
700
|
//#region src/hooks.ts
|
|
682
701
|
/**
|
|
@@ -1765,6 +1784,116 @@ async function createSnapshotPackage(files, packageInfo) {
|
|
|
1765
1784
|
}
|
|
1766
1785
|
}
|
|
1767
1786
|
//#endregion
|
|
1787
|
+
//#region src/generations.ts
|
|
1788
|
+
const READ_CONCURRENCY = 50;
|
|
1789
|
+
const MB = 1048576;
|
|
1790
|
+
const INDEX_KEY = "studio/generations.json";
|
|
1791
|
+
const hashOf = (content) => createHash("sha1").update(content).digest("hex").slice(0, 16);
|
|
1792
|
+
/**
|
|
1793
|
+
* What the output directory holds on disk before a run. `undefined` when `outputPath` is not a real
|
|
1794
|
+
* subdirectory of `root` (listing the root would take in every source file) or holds more than `maxFiles`.
|
|
1795
|
+
*/
|
|
1796
|
+
async function listDisk({ root, outputPath, maxFiles }) {
|
|
1797
|
+
const outputDir = resolve(root, outputPath);
|
|
1798
|
+
const fromRoot = relative(resolve(root), outputDir);
|
|
1799
|
+
if (!fromRoot || fromRoot.startsWith("..") || fromRoot.startsWith(sep)) return void 0;
|
|
1800
|
+
const storage = fsStorage();
|
|
1801
|
+
const keys = await storage.readKeys(outputDir);
|
|
1802
|
+
if (keys.length > maxFiles) return void 0;
|
|
1803
|
+
const paths = keys.filter((key) => !key.split("/").includes("node_modules")).map((key) => relative(resolve(root), resolve(outputDir, key)).replaceAll("\\", "/"));
|
|
1804
|
+
return {
|
|
1805
|
+
storage,
|
|
1806
|
+
root,
|
|
1807
|
+
paths: new Set(paths)
|
|
1808
|
+
};
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Keeps recent generations by job id in `storage`, with an index next to them so a store on disk
|
|
1812
|
+
* survives a restart. A generation is only ever looked up by its job id, so on a pooled sandbox one
|
|
1813
|
+
* tenant never reaches another's output. The oldest go past `maxCount` or `maxMb`, but the newest
|
|
1814
|
+
* always stays.
|
|
1815
|
+
*/
|
|
1816
|
+
function createGenerationStore({ storage, maxCount, maxMb }) {
|
|
1817
|
+
let index;
|
|
1818
|
+
const dirOf = (jobId) => `studio/generations/${hashOf(jobId)}/`;
|
|
1819
|
+
async function load() {
|
|
1820
|
+
if (index) return index;
|
|
1821
|
+
const stored = await storage.readItem(INDEX_KEY).catch(() => null);
|
|
1822
|
+
try {
|
|
1823
|
+
index = stored ? JSON.parse(stored) : [];
|
|
1824
|
+
} catch {
|
|
1825
|
+
index = [];
|
|
1826
|
+
}
|
|
1827
|
+
return index;
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Copies `files` into the store as one set of `jobId`. Above `maxSetMb` only the hashes are kept.
|
|
1831
|
+
*/
|
|
1832
|
+
async function keep({ jobId, source, files, maxSetMb }) {
|
|
1833
|
+
const maxSetBytes = maxSetMb * MB;
|
|
1834
|
+
const hashes = {};
|
|
1835
|
+
let bytes = 0;
|
|
1836
|
+
await inParallel({
|
|
1837
|
+
items: [...files.paths],
|
|
1838
|
+
limit: READ_CONCURRENCY,
|
|
1839
|
+
run: async (path) => {
|
|
1840
|
+
if (path.split("/").includes("..")) return;
|
|
1841
|
+
const content = await files.storage.readItem(resolve(files.root, path));
|
|
1842
|
+
if (content === null) return;
|
|
1843
|
+
hashes[path] = hashOf(content);
|
|
1844
|
+
bytes += Buffer.byteLength(content);
|
|
1845
|
+
if (bytes <= maxSetBytes) await storage.writeItem(`${dirOf(jobId)}${source}/${path}`, content);
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
if (bytes > maxSetBytes) {
|
|
1849
|
+
await storage.empty(`${dirOf(jobId)}${source}/`);
|
|
1850
|
+
return {
|
|
1851
|
+
paths: [],
|
|
1852
|
+
hashes,
|
|
1853
|
+
bytes: 0
|
|
1854
|
+
};
|
|
1855
|
+
}
|
|
1856
|
+
return {
|
|
1857
|
+
paths: Object.keys(hashes),
|
|
1858
|
+
hashes,
|
|
1859
|
+
bytes
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
async function drop(jobId) {
|
|
1863
|
+
await storage.empty(dirOf(jobId));
|
|
1864
|
+
}
|
|
1865
|
+
return {
|
|
1866
|
+
keep,
|
|
1867
|
+
drop,
|
|
1868
|
+
get: async (jobId) => (await load()).find((generation) => generation.jobId === jobId),
|
|
1869
|
+
latest: async () => (await load()).at(-1),
|
|
1870
|
+
async add(generation) {
|
|
1871
|
+
const entries = (await load()).filter((entry) => entry.jobId !== generation.jobId);
|
|
1872
|
+
entries.push(generation);
|
|
1873
|
+
const weight = () => entries.reduce((sum, { output, disk }) => sum + output.bytes + (disk?.bytes ?? 0), 0);
|
|
1874
|
+
while (entries.length > 1 && (entries.length > maxCount || weight() > maxMb * MB)) await drop(entries.shift().jobId);
|
|
1875
|
+
index = entries;
|
|
1876
|
+
await storage.writeItem(INDEX_KEY, JSON.stringify(entries));
|
|
1877
|
+
},
|
|
1878
|
+
/**
|
|
1879
|
+
* Reads the requested paths the set holds, skipping any it does not.
|
|
1880
|
+
*/
|
|
1881
|
+
async read({ generation, source, paths }) {
|
|
1882
|
+
const kept = new Set(generation[source]?.paths);
|
|
1883
|
+
const files = {};
|
|
1884
|
+
await inParallel({
|
|
1885
|
+
items: paths.filter((path) => kept.has(path)),
|
|
1886
|
+
limit: READ_CONCURRENCY,
|
|
1887
|
+
run: async (path) => {
|
|
1888
|
+
const content = await storage.readItem(`${dirOf(generation.jobId)}${source}/${path}`);
|
|
1889
|
+
if (content !== null) files[path] = content;
|
|
1890
|
+
}
|
|
1891
|
+
});
|
|
1892
|
+
return files;
|
|
1893
|
+
}
|
|
1894
|
+
};
|
|
1895
|
+
}
|
|
1896
|
+
//#endregion
|
|
1768
1897
|
//#region src/ws.ts
|
|
1769
1898
|
/**
|
|
1770
1899
|
* How long the initial handshake may take before the socket is closed and the reconnect loop
|
|
@@ -1775,12 +1904,6 @@ const require = createRequire(import.meta.url);
|
|
|
1775
1904
|
function relativeStoragePath(root, filePath) {
|
|
1776
1905
|
return (isAbsolute(filePath) ? relative(resolve(root), filePath) : filePath).replaceAll("\\", "/");
|
|
1777
1906
|
}
|
|
1778
|
-
/**
|
|
1779
|
-
* Inverse of {@link relativeStoragePath}: rebuilds the storage key a relative path came from.
|
|
1780
|
-
*/
|
|
1781
|
-
function absoluteStoragePath(root, relativePath) {
|
|
1782
|
-
return resolve(root, relativePath);
|
|
1783
|
-
}
|
|
1784
1907
|
async function resolvePeerDependencies(names) {
|
|
1785
1908
|
const uniqueNames = [...new Set(names.map(toPackageName))];
|
|
1786
1909
|
const peerDependencies = {};
|
|
@@ -1906,10 +2029,12 @@ function createGenerationStream(hooks, jobId, options = {}) {
|
|
|
1906
2029
|
const { peerDependencies, missingDependencies } = await resolvePeerDependencies(config.plugins.map(({ name }) => name));
|
|
1907
2030
|
const keys = await storage.readKeys();
|
|
1908
2031
|
const paths = new Set(keys.map((key) => relativeStoragePath(config.root, key)));
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
2032
|
+
options.onGenerationEnd?.({
|
|
2033
|
+
output: {
|
|
2034
|
+
storage,
|
|
2035
|
+
root: config.root,
|
|
2036
|
+
paths
|
|
2037
|
+
},
|
|
1913
2038
|
peerDependencies,
|
|
1914
2039
|
missingDependencies
|
|
1915
2040
|
});
|
|
@@ -2060,39 +2185,9 @@ const connectWebSocketRpc = async ({ url, token, local }) => {
|
|
|
2060
2185
|
//#endregion
|
|
2061
2186
|
//#region src/StudioSession.ts
|
|
2062
2187
|
/**
|
|
2063
|
-
*
|
|
2188
|
+
* Past this many files in the output directory, no snapshot of it is taken before a run.
|
|
2064
2189
|
*/
|
|
2065
|
-
const
|
|
2066
|
-
/**
|
|
2067
|
-
* Reads every file a run produced back out of its storage.
|
|
2068
|
-
*/
|
|
2069
|
-
async function readSnapshot(generation) {
|
|
2070
|
-
const snapshot = /* @__PURE__ */ new Map();
|
|
2071
|
-
await inParallel({
|
|
2072
|
-
items: [...generation.paths],
|
|
2073
|
-
limit: FILE_READ_CONCURRENCY,
|
|
2074
|
-
run: async (path) => {
|
|
2075
|
-
const content = await generation.storage.readItem(absoluteStoragePath(generation.root, path));
|
|
2076
|
-
if (content !== null) snapshot.set(path, content);
|
|
2077
|
-
}
|
|
2078
|
-
});
|
|
2079
|
-
return snapshot;
|
|
2080
|
-
}
|
|
2081
|
-
/**
|
|
2082
|
-
* How each path differs between two runs. Paths with identical content are left out.
|
|
2083
|
-
*/
|
|
2084
|
-
function diffSnapshots(previous, current) {
|
|
2085
|
-
const changes = {};
|
|
2086
|
-
for (const [path, content] of current) {
|
|
2087
|
-
if (!previous.has(path)) {
|
|
2088
|
-
changes[path] = "added";
|
|
2089
|
-
continue;
|
|
2090
|
-
}
|
|
2091
|
-
if (previous.get(path) !== content) changes[path] = "changed";
|
|
2092
|
-
}
|
|
2093
|
-
for (const path of previous.keys()) if (!current.has(path)) changes[path] = "removed";
|
|
2094
|
-
return changes;
|
|
2095
|
-
}
|
|
2190
|
+
const DISK_SNAPSHOT_MAX_FILES = 1e4;
|
|
2096
2191
|
var GenerationRunTarget = class extends RpcTarget {
|
|
2097
2192
|
generationStream;
|
|
2098
2193
|
generationResult;
|
|
@@ -2191,7 +2286,8 @@ var StudioSession = class {
|
|
|
2191
2286
|
#isGenerating = false;
|
|
2192
2287
|
#heartbeatTimer;
|
|
2193
2288
|
#lastGeneration;
|
|
2194
|
-
#
|
|
2289
|
+
#store;
|
|
2290
|
+
#limits = resolveGenerationLimits();
|
|
2195
2291
|
/**
|
|
2196
2292
|
* Resolves when Studio calls {@link StudioSession.connect}. `studio:ready` waits on this so the
|
|
2197
2293
|
* host does not queue jobs before the agent session is registered.
|
|
@@ -2207,6 +2303,18 @@ var StudioSession = class {
|
|
|
2207
2303
|
get #isSandbox() {
|
|
2208
2304
|
return this.#session?.isSandbox === true;
|
|
2209
2305
|
}
|
|
2306
|
+
/**
|
|
2307
|
+
* Kept in the project's cache directory, so it survives a restart, except on a sandbox: its pool
|
|
2308
|
+
* sessions run every tenant's jobs, so it keeps them in memory. Looked up by job id only.
|
|
2309
|
+
*/
|
|
2310
|
+
get #generations() {
|
|
2311
|
+
this.#store ??= createGenerationStore({
|
|
2312
|
+
storage: this.#isSandbox ? memoryStorage() : cacheStorage({ root: this.#options.root }),
|
|
2313
|
+
maxCount: this.#limits.maxCount,
|
|
2314
|
+
maxMb: this.#limits.maxMb
|
|
2315
|
+
});
|
|
2316
|
+
return this.#store;
|
|
2317
|
+
}
|
|
2210
2318
|
get #canWrite() {
|
|
2211
2319
|
return !this.#isSandbox && this.#options.permissions.allowWrite;
|
|
2212
2320
|
}
|
|
@@ -2416,8 +2524,18 @@ var StudioSession = class {
|
|
|
2416
2524
|
await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
|
|
2417
2525
|
}
|
|
2418
2526
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2419
|
-
if (this.#lastGeneration) this.#previousGeneration = await readSnapshot(this.#lastGeneration);
|
|
2420
2527
|
this.#lastGeneration = void 0;
|
|
2528
|
+
const diskFiles = this.#hasProjectOnDisk ? await listDisk({
|
|
2529
|
+
root,
|
|
2530
|
+
outputPath: config.output.path,
|
|
2531
|
+
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2532
|
+
}) : void 0;
|
|
2533
|
+
const disk = diskFiles ? await this.#generations.keep({
|
|
2534
|
+
jobId: data.jobId,
|
|
2535
|
+
source: "disk",
|
|
2536
|
+
files: diskFiles,
|
|
2537
|
+
maxSetMb: this.#limits.maxSnapshotMb
|
|
2538
|
+
}) : void 0;
|
|
2421
2539
|
const detach = [setupHookListener(this.#hooks, root, controller.signal)];
|
|
2422
2540
|
try {
|
|
2423
2541
|
await generate({
|
|
@@ -2438,6 +2556,9 @@ var StudioSession = class {
|
|
|
2438
2556
|
hooks: this.#hooks,
|
|
2439
2557
|
signal: controller.signal
|
|
2440
2558
|
});
|
|
2559
|
+
} catch (error) {
|
|
2560
|
+
await this.#generations.drop(data.jobId);
|
|
2561
|
+
throw error;
|
|
2441
2562
|
} finally {
|
|
2442
2563
|
for (const remove of detach) remove();
|
|
2443
2564
|
}
|
|
@@ -2446,18 +2567,30 @@ var StudioSession = class {
|
|
|
2446
2567
|
info: `${resolvedPlugins.length} plugin${resolvedPlugins.length === 1 ? "" : "s"}, ${this.#canWrite ? "written to disk" : "in memory"}${inputOverride !== void 0 ? ", from a Studio spec" : ""}`
|
|
2447
2568
|
});
|
|
2448
2569
|
const generation = this.#lastGeneration;
|
|
2449
|
-
const
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2570
|
+
const output = generation ? await this.#generations.keep({
|
|
2571
|
+
jobId: data.jobId,
|
|
2572
|
+
source: "output",
|
|
2573
|
+
files: generation.output,
|
|
2574
|
+
maxSetMb: this.#limits.maxMb
|
|
2575
|
+
}) : void 0;
|
|
2576
|
+
if (generation && output) {
|
|
2577
|
+
if (!output.paths.length && Object.keys(output.hashes).length) await this.#warn("Kept only the hashes of this generation: its output is too large to keep");
|
|
2578
|
+
const { peerDependencies, missingDependencies } = generation;
|
|
2579
|
+
await this.#generations.add({
|
|
2580
|
+
jobId: data.jobId,
|
|
2581
|
+
output,
|
|
2582
|
+
disk,
|
|
2583
|
+
peerDependencies,
|
|
2584
|
+
missingDependencies
|
|
2585
|
+
});
|
|
2586
|
+
}
|
|
2587
|
+
const files = [...generation?.output.paths ?? []];
|
|
2456
2588
|
return {
|
|
2457
2589
|
status: "success",
|
|
2458
2590
|
files,
|
|
2459
2591
|
fileCount: files.length,
|
|
2460
|
-
|
|
2592
|
+
hashes: output?.hashes ?? {},
|
|
2593
|
+
disk: disk ? { hashes: disk.hashes } : void 0
|
|
2461
2594
|
};
|
|
2462
2595
|
} finally {
|
|
2463
2596
|
this.#isGenerating = false;
|
|
@@ -2512,20 +2645,16 @@ var StudioSession = class {
|
|
|
2512
2645
|
if (this.#isSandbox) return this.#refuse("Ignored snapshot: a sandbox agent has no project to build a package from", "A sandbox agent has no project to build a package from");
|
|
2513
2646
|
const { name, version, bundledDependencies, uploadPath } = data;
|
|
2514
2647
|
if (!name || !version || !uploadPath) return this.#refuse("Ignored snapshot: the message was missing required fields", "The request was missing required fields");
|
|
2515
|
-
const generation = this.#
|
|
2648
|
+
const generation = await this.#generations.latest();
|
|
2516
2649
|
if (!generation) return this.#refuse("Ignored snapshot: no prior generation to pack", "No prior generation exists to pack, run a generation first");
|
|
2517
2650
|
const bundled = new Set(bundledDependencies ?? []);
|
|
2518
2651
|
const missing = generation.missingDependencies.filter((dependency) => !bundled.has(dependency));
|
|
2519
2652
|
if (missing.length) return this.#refuse(`Ignored snapshot: missing dependencies: ${missing.join(", ")}`, `Missing dependencies: ${missing.join(", ")}`);
|
|
2520
2653
|
try {
|
|
2521
|
-
const files = {
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
run: async (relativePath) => {
|
|
2526
|
-
const content = await generation.storage.readItem(absoluteStoragePath(generation.root, relativePath));
|
|
2527
|
-
if (content !== null) files[relativePath] = content;
|
|
2528
|
-
}
|
|
2654
|
+
const files = await this.#generations.read({
|
|
2655
|
+
generation,
|
|
2656
|
+
source: "output",
|
|
2657
|
+
paths: generation.output.paths
|
|
2529
2658
|
});
|
|
2530
2659
|
const { bytes, integrity } = await createSnapshotPackage(files, {
|
|
2531
2660
|
name,
|
|
@@ -2563,6 +2692,13 @@ var StudioSession = class {
|
|
|
2563
2692
|
throw error;
|
|
2564
2693
|
}
|
|
2565
2694
|
}
|
|
2695
|
+
/**
|
|
2696
|
+
* An agent with a project on disk can show a run against what its output directory held before.
|
|
2697
|
+
* A sandbox agent has no project.
|
|
2698
|
+
*/
|
|
2699
|
+
get #hasProjectOnDisk() {
|
|
2700
|
+
return !this.#isSandbox && this.#canRead;
|
|
2701
|
+
}
|
|
2566
2702
|
async readFiles(data) {
|
|
2567
2703
|
const command = "readFiles";
|
|
2568
2704
|
await this.#hooks.callHook("studio:command:start", { command });
|
|
@@ -2575,27 +2711,15 @@ var StudioSession = class {
|
|
|
2575
2711
|
if (!Array.isArray(data.paths)) return this.#refuse("Ignored files: the message carried no paths", "The request carried no paths");
|
|
2576
2712
|
const { paths } = data;
|
|
2577
2713
|
if (paths.length > 50) return this.#refuse(`Ignored files: requested ${paths.length} paths, more than the 50 allowed per request`, `At most 50 paths may be requested at once`);
|
|
2578
|
-
if (data.
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
}
|
|
2588
|
-
const generation = this.#lastGeneration;
|
|
2589
|
-
if (!generation) return this.#refuse("Ignored files: no prior generation to read from", "No prior generation to read from, run a generation first");
|
|
2590
|
-
const requested = paths.filter((path) => generation.paths.has(path));
|
|
2591
|
-
const files = {};
|
|
2592
|
-
await inParallel({
|
|
2593
|
-
items: requested,
|
|
2594
|
-
limit: FILE_READ_CONCURRENCY,
|
|
2595
|
-
run: async (path) => {
|
|
2596
|
-
const content = await generation.storage.readItem(absoluteStoragePath(generation.root, path));
|
|
2597
|
-
if (content !== null) files[path] = content;
|
|
2598
|
-
}
|
|
2714
|
+
if (typeof data.jobId !== "string" || !data.jobId) return this.#refuse("Ignored files: the message named no job", "The request named no generation job");
|
|
2715
|
+
const generation = await this.#generations.get(data.jobId);
|
|
2716
|
+
if (!generation) return this.#refuse(`Ignored files: job ${data.jobId} is not kept on this agent`, GENERATION_GONE_MESSAGE);
|
|
2717
|
+
const source = data.source === "disk" ? "disk" : "output";
|
|
2718
|
+
if (!generation[source]) return this.#refuse("Ignored files: that job has no snapshot of the files on disk", "This agent kept no snapshot of the files on disk for that job");
|
|
2719
|
+
const files = await this.#generations.read({
|
|
2720
|
+
generation,
|
|
2721
|
+
source,
|
|
2722
|
+
paths
|
|
2599
2723
|
});
|
|
2600
2724
|
await this.#hooks.callHook("studio:command:end", {
|
|
2601
2725
|
command,
|