@kubb/studio 5.3.17 → 5.3.19
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 +8 -1
- package/dist/index.cjs +367 -355
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +28 -14
- package/dist/index.js +367 -355
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.ts +5 -16
- package/dist/protocol.js.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -8,7 +8,6 @@ import path, { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
|
8
8
|
import process$1 from "node:process";
|
|
9
9
|
import { spawn } from "node:child_process";
|
|
10
10
|
import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, memoryStorage, resolveCacheDir } from "@kubb/core";
|
|
11
|
-
import { x } from "tinyexec";
|
|
12
11
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
13
12
|
import { FetchError, ofetch } from "ofetch";
|
|
14
13
|
import { promisify, styleText } from "node:util";
|
|
@@ -22,6 +21,7 @@ import { gzip } from "node:zlib";
|
|
|
22
21
|
import { build } from "tsdown";
|
|
23
22
|
import { RpcTarget, newWebSocketRpcSession } from "capnweb";
|
|
24
23
|
import WebSocket from "ws";
|
|
24
|
+
import { x } from "tinyexec";
|
|
25
25
|
//#region ../../internals/utils/src/casing.ts
|
|
26
26
|
/**
|
|
27
27
|
* Shared implementation for camelCase and PascalCase conversion.
|
|
@@ -345,89 +345,9 @@ function getElapsedMs(hrStart) {
|
|
|
345
345
|
}
|
|
346
346
|
//#endregion
|
|
347
347
|
//#region package.json
|
|
348
|
-
var version = "5.3.
|
|
349
|
-
//#endregion
|
|
350
|
-
//#region src/hooks.ts
|
|
351
|
-
/**
|
|
352
|
-
* Register a `kubb:hook:start` listener that spawns the requested command via tinyexec,
|
|
353
|
-
* streams each stdout line as a `kubb:hook:line` event, and calls `kubb:hook:end` with the result.
|
|
354
|
-
* Streaming the output lets Kubb Studio render live hook progress over the WebSocket connection.
|
|
355
|
-
*
|
|
356
|
-
* Returns a remover, so a session that runs one generation after another on the same emitter does
|
|
357
|
-
* not stack a listener per run.
|
|
358
|
-
*/
|
|
359
|
-
function setupHookListener(hooks, root, signal) {
|
|
360
|
-
return hooks.hook("kubb:hook:start", async (ctx) => {
|
|
361
|
-
const { id, command, args } = ctx;
|
|
362
|
-
if (!id) return;
|
|
363
|
-
const commandWithArgs = args?.length ? `${command} ${args.join(" ")}` : command;
|
|
364
|
-
try {
|
|
365
|
-
const proc = x(command, [...args ?? []], {
|
|
366
|
-
signal,
|
|
367
|
-
nodeOptions: {
|
|
368
|
-
cwd: root,
|
|
369
|
-
detached: true
|
|
370
|
-
}
|
|
371
|
-
});
|
|
372
|
-
for await (const line of proc) await hooks.callHook("kubb:hook:line", {
|
|
373
|
-
id,
|
|
374
|
-
line
|
|
375
|
-
});
|
|
376
|
-
const { exitCode } = await proc;
|
|
377
|
-
if (exitCode !== 0) {
|
|
378
|
-
const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
|
|
379
|
-
await hooks.callHook("kubb:hook:end", {
|
|
380
|
-
id,
|
|
381
|
-
command,
|
|
382
|
-
args,
|
|
383
|
-
success: false,
|
|
384
|
-
error
|
|
385
|
-
});
|
|
386
|
-
await hooks.callHook("kubb:error", { error });
|
|
387
|
-
return;
|
|
388
|
-
}
|
|
389
|
-
await hooks.callHook("kubb:hook:end", {
|
|
390
|
-
id,
|
|
391
|
-
command,
|
|
392
|
-
args,
|
|
393
|
-
success: true,
|
|
394
|
-
error: null
|
|
395
|
-
});
|
|
396
|
-
} catch (caughtError) {
|
|
397
|
-
const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
|
|
398
|
-
error.cause = caughtError;
|
|
399
|
-
await hooks.callHook("kubb:hook:end", {
|
|
400
|
-
id,
|
|
401
|
-
command,
|
|
402
|
-
args,
|
|
403
|
-
success: false,
|
|
404
|
-
error
|
|
405
|
-
});
|
|
406
|
-
await hooks.callHook("kubb:error", { error });
|
|
407
|
-
}
|
|
408
|
-
});
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Waits for the `kubb:hook:end` matching `hookId`. Register this before calling `kubb:hook:start`:
|
|
412
|
-
* `callHook` awaits its listeners, and {@link setupHookListener} calls `kubb:hook:end` from inside
|
|
413
|
-
* that same listener, so a handler added afterward would already have missed it.
|
|
414
|
-
*/
|
|
415
|
-
function waitForHookEnd(hooks, hookId) {
|
|
416
|
-
return new Promise((resolve, reject) => {
|
|
417
|
-
const handleHookEnd = (ctx) => {
|
|
418
|
-
if (ctx.id !== hookId) return;
|
|
419
|
-
hooks.removeHook("kubb:hook:end", handleHookEnd);
|
|
420
|
-
if (ctx.success) {
|
|
421
|
-
resolve();
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
reject(ctx.error);
|
|
425
|
-
};
|
|
426
|
-
hooks.hook("kubb:hook:end", handleHookEnd);
|
|
427
|
-
});
|
|
428
|
-
}
|
|
348
|
+
var version = "5.3.19";
|
|
429
349
|
//#endregion
|
|
430
|
-
//#region src/machine.ts
|
|
350
|
+
//#region src/operations/machine.ts
|
|
431
351
|
/**
|
|
432
352
|
* Key-value storage the runtime uses for its machine secret and the last Studio config.
|
|
433
353
|
*
|
|
@@ -487,7 +407,7 @@ async function getMachineToken() {
|
|
|
487
407
|
return machineTokenFrom(await fallbackSecretPromise);
|
|
488
408
|
}
|
|
489
409
|
//#endregion
|
|
490
|
-
//#region src/api.ts
|
|
410
|
+
//#region src/operations/api.ts
|
|
491
411
|
/**
|
|
492
412
|
* Reads a human-readable message from a Studio JSON error body, when it has one. `FetchError`'s own
|
|
493
413
|
* message stops at the status line, so the detail Studio sends with a failure (an agent limit, a
|
|
@@ -623,7 +543,7 @@ function withJitter(ms) {
|
|
|
623
543
|
* const finished = await waitForJob({ studioUrl, token, id: job.id })
|
|
624
544
|
* ```
|
|
625
545
|
*/
|
|
626
|
-
async function createJob({ studioUrl, token, type, agentId, name, version, commit, baseId, config, timeoutMs = 6e4, signal }) {
|
|
546
|
+
async function createJob({ studioUrl, token, type, agentId, name, version, commit, baseId, config, instanceId, timeoutMs = 6e4, signal }) {
|
|
627
547
|
const deadline = Date.now() + timeoutMs;
|
|
628
548
|
let interval = CREATE_JOB_INITIAL_DELAY_MS;
|
|
629
549
|
for (;;) {
|
|
@@ -639,7 +559,8 @@ async function createJob({ studioUrl, token, type, agentId, name, version, commi
|
|
|
639
559
|
version,
|
|
640
560
|
commit,
|
|
641
561
|
baseId,
|
|
642
|
-
config
|
|
562
|
+
config,
|
|
563
|
+
instanceId
|
|
643
564
|
},
|
|
644
565
|
retry: false,
|
|
645
566
|
timeout: Math.max(deadline - Date.now(), 1),
|
|
@@ -728,7 +649,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
|
|
|
728
649
|
}
|
|
729
650
|
}
|
|
730
651
|
//#endregion
|
|
731
|
-
//#region src/resolveConfig.ts
|
|
652
|
+
//#region src/operations/resolveConfig.ts
|
|
732
653
|
/**
|
|
733
654
|
* Imports a package, falling back to how the user's project would resolve it.
|
|
734
655
|
*
|
|
@@ -886,7 +807,7 @@ async function mergeAdapter(diskAdapter, studioOptions) {
|
|
|
886
807
|
return factory(mergeDeep(diskAdapter.options ?? {}, studioOptions));
|
|
887
808
|
}
|
|
888
809
|
//#endregion
|
|
889
|
-
//#region src/configFile.ts
|
|
810
|
+
//#region src/operations/configFile.ts
|
|
890
811
|
/**
|
|
891
812
|
* A valid JavaScript identifier, so an import name can only ever print as `import { name } from`,
|
|
892
813
|
* never as source that breaks out of the import statement.
|
|
@@ -1426,6 +1347,11 @@ function applyConfigEdits(source, edits) {
|
|
|
1426
1347
|
changed: current !== source
|
|
1427
1348
|
};
|
|
1428
1349
|
}
|
|
1350
|
+
async function writeConfigEdits({ filePath, edits }) {
|
|
1351
|
+
const result = applyConfigEdits(await read(filePath), edits);
|
|
1352
|
+
if (result.changed) await writeFile(filePath, result.source, "utf-8");
|
|
1353
|
+
return result;
|
|
1354
|
+
}
|
|
1429
1355
|
/**
|
|
1430
1356
|
* The 1-based line where the file's last import declaration ends, or `0` when it has none. Read
|
|
1431
1357
|
* off the parsed module, so a multi-line `import {\n x,\n} from '...'` reports its closing line
|
|
@@ -1458,144 +1384,7 @@ function withTrailingNewline(code, hadTrailingNewline) {
|
|
|
1458
1384
|
return `${code}\n`;
|
|
1459
1385
|
}
|
|
1460
1386
|
//#endregion
|
|
1461
|
-
//#region src/
|
|
1462
|
-
/**
|
|
1463
|
-
* `isToolAvailable` spawns a process, and a long-lived connection generates repeatedly, so each
|
|
1464
|
-
* executable is probed once per process. The CLI deliberately does not memoize: a `--watch` build
|
|
1465
|
-
* should keep noticing a tool installed mid-session.
|
|
1466
|
-
*/
|
|
1467
|
-
const detectTool = memoize(/* @__PURE__ */ new Map(), detectTool$1);
|
|
1468
|
-
/**
|
|
1469
|
-
* The two post-build tool steps. Formatting and linting differ only in which tools they look for,
|
|
1470
|
-
* so they run through one loop rather than two near-identical blocks.
|
|
1471
|
-
*
|
|
1472
|
-
* `noun` and `verbing` are spelled out instead of built from `kind`. Concatenating `` `${kind}ter` ``
|
|
1473
|
-
* and `` `${kind}ting` `` works for `format`, but doubles the `t` in `lint`, giving "lintter" and
|
|
1474
|
-
* "lintting" instead of "linter" and "linting".
|
|
1475
|
-
*/
|
|
1476
|
-
const TOOL_STEPS = [{
|
|
1477
|
-
kind: "format",
|
|
1478
|
-
noun: "formatter",
|
|
1479
|
-
verbing: "Formatting",
|
|
1480
|
-
tools: formatters,
|
|
1481
|
-
detect: FORMATTER_PREFERENCE
|
|
1482
|
-
}, {
|
|
1483
|
-
kind: "lint",
|
|
1484
|
-
noun: "linter",
|
|
1485
|
-
verbing: "Linting",
|
|
1486
|
-
tools: linters,
|
|
1487
|
-
detect: LINTER_PREFERENCE
|
|
1488
|
-
}];
|
|
1489
|
-
/**
|
|
1490
|
-
* Absolute path of the directory the formatter and linter are pointed at.
|
|
1491
|
-
*/
|
|
1492
|
-
function outputPath(config) {
|
|
1493
|
-
return path.isAbsolute(config.output.path) ? config.output.path : path.resolve(process$1.cwd(), config.root, config.output.path);
|
|
1494
|
-
}
|
|
1495
|
-
/**
|
|
1496
|
-
* Emits `kubb:hook:start` and waits for the matching `kubb:hook:end`. The host spawns the process:
|
|
1497
|
-
* this only describes what to run and when it finished.
|
|
1498
|
-
*
|
|
1499
|
-
* @throws whatever the command failed with, so callers can report it their own way.
|
|
1500
|
-
*/
|
|
1501
|
-
async function runHook({ hooks, id, command, args }) {
|
|
1502
|
-
const hookId = hash("sha256", id);
|
|
1503
|
-
const hookEnd = waitForHookEnd(hooks, hookId);
|
|
1504
|
-
await hooks.callHook("kubb:hook:start", {
|
|
1505
|
-
id: hookId,
|
|
1506
|
-
command,
|
|
1507
|
-
args: [...args]
|
|
1508
|
-
});
|
|
1509
|
-
await hookEnd;
|
|
1510
|
-
}
|
|
1511
|
-
function isProblemErrorDiagnostic(diagnostic) {
|
|
1512
|
-
return (diagnostic.kind ?? "problem") === "problem" && diagnostic.severity === "error";
|
|
1513
|
-
}
|
|
1514
|
-
/**
|
|
1515
|
-
* Folds error-severity diagnostics into one thrown error so logs name the failing plugin.
|
|
1516
|
-
*/
|
|
1517
|
-
function formatGenerationFailure(diagnostics) {
|
|
1518
|
-
const reasons = diagnostics.filter(isProblemErrorDiagnostic).map((diagnostic) => diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message);
|
|
1519
|
-
if (!reasons.length) return /* @__PURE__ */ new Error("Generation failed");
|
|
1520
|
-
return /* @__PURE__ */ new Error(`Generation failed: ${reasons.length} error${reasons.length === 1 ? "" : "s"}: ${reasons.join("; ")}`);
|
|
1521
|
-
}
|
|
1522
|
-
/**
|
|
1523
|
-
* Runs a full Kubb code-generation cycle for the given config.
|
|
1524
|
-
*
|
|
1525
|
-
* Emits lifecycle events on the provided `hooks` emitter so callers (e.g. the WebSocket stream)
|
|
1526
|
-
* can forward progress to connected clients. After a successful build, auto-formatting and
|
|
1527
|
-
* linting are applied when configured, followed by any user-defined `hooks.done` commands.
|
|
1528
|
-
*/
|
|
1529
|
-
async function generate({ config, hooks, signal }) {
|
|
1530
|
-
signal?.throwIfAborted();
|
|
1531
|
-
const hrStart = process$1.hrtime();
|
|
1532
|
-
await hooks.callHook("kubb:generation:start", { config });
|
|
1533
|
-
await hooks.callHook("kubb:info", { message: config.name ? `Setup generation ${config.name}` : "Setup generation" });
|
|
1534
|
-
const kubb = createKubb(config, {
|
|
1535
|
-
hooks,
|
|
1536
|
-
signal
|
|
1537
|
-
});
|
|
1538
|
-
await kubb.setup();
|
|
1539
|
-
await hooks.callHook("kubb:info", { message: config.name ? `Build generation ${config.name}` : "Build generation" });
|
|
1540
|
-
const { files, diagnostics, storage } = await kubb.safeBuild();
|
|
1541
|
-
signal?.throwIfAborted();
|
|
1542
|
-
await hooks.callHook("kubb:info", { message: "Load summary" });
|
|
1543
|
-
for (const diagnostic of diagnostics.filter(isProblemErrorDiagnostic)) await hooks.callHook("kubb:error", { error: new Error(diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message) });
|
|
1544
|
-
const status = Diagnostics.hasError(diagnostics) ? "failed" : "success";
|
|
1545
|
-
await hooks.callHook("kubb:generation:end", {
|
|
1546
|
-
config,
|
|
1547
|
-
storage: {
|
|
1548
|
-
...storage,
|
|
1549
|
-
readKeys: async () => [...new Set(files.map((file) => file.path))]
|
|
1550
|
-
},
|
|
1551
|
-
diagnostics,
|
|
1552
|
-
status,
|
|
1553
|
-
hrStart,
|
|
1554
|
-
filesCreated: files.length
|
|
1555
|
-
});
|
|
1556
|
-
if (status === "failed") throw formatGenerationFailure(diagnostics);
|
|
1557
|
-
await hooks.callHook("kubb:success", { message: "Generation successfully" });
|
|
1558
|
-
for (const step of TOOL_STEPS) {
|
|
1559
|
-
const setting = config.output[step.kind];
|
|
1560
|
-
if (!setting) continue;
|
|
1561
|
-
await hooks.callHook(`kubb:${step.kind}:start`);
|
|
1562
|
-
const tool = setting === "auto" ? await detectTool(step.detect) : setting;
|
|
1563
|
-
if (!tool) await hooks.callHook("kubb:warn", { message: `No ${step.noun} found (${step.detect.join(", ")}). Skipping ${step.verbing.toLowerCase()}.` });
|
|
1564
|
-
if (tool && setting === "auto") await hooks.callHook("kubb:info", { message: `Auto-detected ${step.noun}: ${styleText("dim", tool)}` });
|
|
1565
|
-
const command = tool ? step.tools[tool] : void 0;
|
|
1566
|
-
if (command) try {
|
|
1567
|
-
await runHook({
|
|
1568
|
-
hooks,
|
|
1569
|
-
id: [config.name, tool].filter(Boolean).join("-"),
|
|
1570
|
-
command: command.command,
|
|
1571
|
-
args: command.args(outputPath(config))
|
|
1572
|
-
});
|
|
1573
|
-
await hooks.callHook("kubb:success", { message: `${step.verbing} with ${tool} successfully` });
|
|
1574
|
-
} catch (caughtError) {
|
|
1575
|
-
await hooks.callHook("kubb:error", { error: new Error(command.errorMessage, { cause: caughtError }) });
|
|
1576
|
-
signal?.throwIfAborted();
|
|
1577
|
-
}
|
|
1578
|
-
await hooks.callHook(`kubb:${step.kind}:end`);
|
|
1579
|
-
}
|
|
1580
|
-
if (config.output.postGenerate?.length) {
|
|
1581
|
-
await hooks.callHook("kubb:hooks:start");
|
|
1582
|
-
for (const entry of config.output.postGenerate) {
|
|
1583
|
-
const line = typeof entry === "string" ? entry : entry.command;
|
|
1584
|
-
const [cmd, ...args] = tokenize(line);
|
|
1585
|
-
if (!cmd) continue;
|
|
1586
|
-
await runHook({
|
|
1587
|
-
hooks,
|
|
1588
|
-
id: line,
|
|
1589
|
-
command: cmd,
|
|
1590
|
-
args
|
|
1591
|
-
});
|
|
1592
|
-
await hooks.callHook("kubb:success", { message: `${line} successfully executed` });
|
|
1593
|
-
}
|
|
1594
|
-
await hooks.callHook("kubb:hooks:end");
|
|
1595
|
-
}
|
|
1596
|
-
}
|
|
1597
|
-
//#endregion
|
|
1598
|
-
//#region src/constants.ts
|
|
1387
|
+
//#region src/operations/constants.ts
|
|
1599
1388
|
/**
|
|
1600
1389
|
* Hosted Kubb Studio URL. Exported so credential stores can bind tokens to the resolved instance,
|
|
1601
1390
|
* not whatever default the client would pick on its own.
|
|
@@ -1639,18 +1428,16 @@ function resolveGenerationLimits(env = process.env) {
|
|
|
1639
1428
|
};
|
|
1640
1429
|
}
|
|
1641
1430
|
/**
|
|
1642
|
-
* An agent's capacity read from `KUBB_AGENT_MAX_CONCURRENT
|
|
1643
|
-
*
|
|
1431
|
+
* An agent's capacity read from `KUBB_AGENT_MAX_CONCURRENT`. An unset or invalid value keeps the
|
|
1432
|
+
* default: one job at a time.
|
|
1644
1433
|
*/
|
|
1645
1434
|
function resolveAgentCapacity(env = process.env) {
|
|
1646
|
-
return {
|
|
1647
|
-
maxConcurrent: Math.max(1, Math.floor(positiveNumber(env.KUBB_AGENT_MAX_CONCURRENT) ?? agentDefaults.maxConcurrent)),
|
|
1648
|
-
memoryBudgetMb: positiveNumber(env.KUBB_AGENT_MEMORY_BUDGET_MB)
|
|
1649
|
-
};
|
|
1435
|
+
return { maxConcurrent: Math.max(1, Math.floor(positiveNumber(env.KUBB_AGENT_MAX_CONCURRENT) ?? agentDefaults.maxConcurrent)) };
|
|
1650
1436
|
}
|
|
1651
1437
|
//#endregion
|
|
1652
|
-
//#region src/snapshotPackage.ts
|
|
1438
|
+
//#region src/operations/snapshotPackage.ts
|
|
1653
1439
|
const gzipAsync = promisify(gzip);
|
|
1440
|
+
const UPLOAD_TIMEOUT_MS = 12e4;
|
|
1654
1441
|
/**
|
|
1655
1442
|
* Maps a generated file's path to its place inside the tarball, stripping everything before a
|
|
1656
1443
|
* `src`/`dist` segment and any `..`/empty path segment so a crafted file name cannot escape the
|
|
@@ -1788,8 +1575,31 @@ async function createSnapshotPackage(files, packageInfo) {
|
|
|
1788
1575
|
});
|
|
1789
1576
|
}
|
|
1790
1577
|
}
|
|
1578
|
+
async function uploadSnapshot({ bytes, uploadPath, studioUrl, token, shutdown }) {
|
|
1579
|
+
const uploadUrl = new URL(uploadPath, studioUrl);
|
|
1580
|
+
if (uploadUrl.origin !== new URL(studioUrl).origin) throw new Error("Snapshot upload path must stay on the Studio origin");
|
|
1581
|
+
const timeout = AbortSignal.timeout(UPLOAD_TIMEOUT_MS);
|
|
1582
|
+
const signal = shutdown ? AbortSignal.any([shutdown, timeout]) : timeout;
|
|
1583
|
+
const redirect = await fetch(uploadUrl, {
|
|
1584
|
+
method: "PUT",
|
|
1585
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1586
|
+
redirect: "manual",
|
|
1587
|
+
signal
|
|
1588
|
+
});
|
|
1589
|
+
const storageUrl = redirect.headers.get("location");
|
|
1590
|
+
if (redirect.status !== 307 || !storageUrl) throw new Error(`Studio did not provide a storage URL (status ${redirect.status})`);
|
|
1591
|
+
const storage = new URL(storageUrl);
|
|
1592
|
+
if (storage.protocol !== "https:" && storage.hostname !== "localhost" && storage.hostname !== "127.0.0.1") throw new Error(`Refusing snapshot upload to ${storage.origin}`);
|
|
1593
|
+
const response = await fetch(storage, {
|
|
1594
|
+
method: "PUT",
|
|
1595
|
+
body: new Uint8Array(bytes),
|
|
1596
|
+
redirect: "error",
|
|
1597
|
+
signal
|
|
1598
|
+
});
|
|
1599
|
+
if (!response.ok) throw new Error(`Snapshot upload failed with status ${response.status}`);
|
|
1600
|
+
}
|
|
1791
1601
|
//#endregion
|
|
1792
|
-
//#region src/generations.ts
|
|
1602
|
+
//#region src/operations/generations.ts
|
|
1793
1603
|
const READ_CONCURRENCY = 50;
|
|
1794
1604
|
const MB$1 = 1048576;
|
|
1795
1605
|
const INDEX_KEY = "studio/generations.json";
|
|
@@ -1907,12 +1717,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1907
1717
|
};
|
|
1908
1718
|
}
|
|
1909
1719
|
//#endregion
|
|
1910
|
-
//#region src/
|
|
1911
|
-
/**
|
|
1912
|
-
* How long the initial handshake may take before the socket is closed and the reconnect loop
|
|
1913
|
-
* takes over.
|
|
1914
|
-
*/
|
|
1915
|
-
const CONNECT_TIMEOUT_MS = 5e3;
|
|
1720
|
+
//#region src/operations/generationEvents.ts
|
|
1916
1721
|
const require = createRequire(import.meta.url);
|
|
1917
1722
|
function relativeStoragePath(root, filePath) {
|
|
1918
1723
|
return (isAbsolute(filePath) ? relative(resolve(root), filePath) : filePath).replaceAll("\\", "/");
|
|
@@ -1942,27 +1747,24 @@ async function resolvePeerDependencies(names) {
|
|
|
1942
1747
|
missingDependencies
|
|
1943
1748
|
};
|
|
1944
1749
|
}
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
function createWebsocket(url, options) {
|
|
1949
|
-
const ws = new WebSocket(url, options);
|
|
1950
|
-
const timer = setTimeout(() => {
|
|
1951
|
-
if (ws.readyState === WebSocket.CONNECTING) ws.close(3008, "Connection timeout");
|
|
1952
|
-
}, CONNECT_TIMEOUT_MS);
|
|
1953
|
-
ws.once("open", () => clearTimeout(timer));
|
|
1954
|
-
ws.once("close", () => clearTimeout(timer));
|
|
1955
|
-
return ws;
|
|
1956
|
-
}
|
|
1750
|
+
const MAX_QUEUED_EVENTS = 1024;
|
|
1751
|
+
const RESERVED_EVENTS = 64;
|
|
1752
|
+
const isDiscardable = (event) => event.type === "kubb:files:processing:update" || event.type === "kubb:info" || event.type === "kubb:success";
|
|
1957
1753
|
/** Forwards selected Kubb lifecycle events to a native Cap'n Web stream. */
|
|
1958
1754
|
function createGenerationStream(hooks, jobId, options = {}) {
|
|
1959
1755
|
const unhooks = [];
|
|
1960
1756
|
let root = "";
|
|
1961
|
-
|
|
1962
|
-
const writer = transform.writable.getWriter();
|
|
1963
|
-
let writes = Promise.resolve();
|
|
1757
|
+
let controller;
|
|
1964
1758
|
let closed = false;
|
|
1965
|
-
|
|
1759
|
+
const stream = new ReadableStream({
|
|
1760
|
+
start: (value) => {
|
|
1761
|
+
controller = value;
|
|
1762
|
+
},
|
|
1763
|
+
cancel: () => {
|
|
1764
|
+
closed = true;
|
|
1765
|
+
detach();
|
|
1766
|
+
}
|
|
1767
|
+
}, { highWaterMark: MAX_QUEUED_EVENTS });
|
|
1966
1768
|
/**
|
|
1967
1769
|
* Registers a listener and keeps its remover, so one generation's listeners come off the session
|
|
1968
1770
|
* emitter again when that generation ends.
|
|
@@ -1971,6 +1773,7 @@ function createGenerationStream(hooks, jobId, options = {}) {
|
|
|
1971
1773
|
unhooks.push(hooks.hook(name, handler));
|
|
1972
1774
|
}
|
|
1973
1775
|
function emitEvent(type, data) {
|
|
1776
|
+
if (closed) return;
|
|
1974
1777
|
const event = {
|
|
1975
1778
|
jobId,
|
|
1976
1779
|
type,
|
|
@@ -1978,9 +1781,12 @@ function createGenerationStream(hooks, jobId, options = {}) {
|
|
|
1978
1781
|
version: 1,
|
|
1979
1782
|
timestamp: Date.now()
|
|
1980
1783
|
};
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1784
|
+
if (controller.desiredSize <= RESERVED_EVENTS && isDiscardable(event)) return;
|
|
1785
|
+
if (controller.desiredSize <= 0) {
|
|
1786
|
+
fail(/* @__PURE__ */ new Error(`Generation event stream exceeded ${MAX_QUEUED_EVENTS} queued events`));
|
|
1787
|
+
return;
|
|
1788
|
+
}
|
|
1789
|
+
controller.enqueue(event);
|
|
1984
1790
|
}
|
|
1985
1791
|
on("kubb:plugin:start", (ctx) => {
|
|
1986
1792
|
emitEvent("kubb:plugin:start", [{ plugin: { name: ctx.plugin.name } }]);
|
|
@@ -2126,25 +1932,38 @@ function createGenerationStream(hooks, jobId, options = {}) {
|
|
|
2126
1932
|
if (closed) return;
|
|
2127
1933
|
closed = true;
|
|
2128
1934
|
detach();
|
|
2129
|
-
|
|
2130
|
-
if (streamError) return;
|
|
2131
|
-
await writer.close().catch(() => void 0);
|
|
1935
|
+
controller.close();
|
|
2132
1936
|
}
|
|
2133
1937
|
function fail(error) {
|
|
2134
1938
|
detach();
|
|
2135
1939
|
if (closed) return;
|
|
2136
1940
|
closed = true;
|
|
2137
|
-
|
|
1941
|
+
controller.error(error);
|
|
2138
1942
|
}
|
|
2139
1943
|
return {
|
|
2140
|
-
stream
|
|
1944
|
+
stream,
|
|
2141
1945
|
close,
|
|
2142
1946
|
dispose: () => fail(),
|
|
2143
1947
|
fail
|
|
2144
1948
|
};
|
|
2145
1949
|
}
|
|
2146
1950
|
//#endregion
|
|
2147
|
-
//#region src/
|
|
1951
|
+
//#region src/operations/websocket.ts
|
|
1952
|
+
const CONNECT_TIMEOUT_MS = 5e3;
|
|
1953
|
+
/**
|
|
1954
|
+
* Opens a Studio WebSocket connection and closes it when the initial handshake exceeds the configured timeout.
|
|
1955
|
+
*/
|
|
1956
|
+
function createWebsocket(url, options) {
|
|
1957
|
+
const ws = new WebSocket(url, options);
|
|
1958
|
+
const timer = setTimeout(() => {
|
|
1959
|
+
if (ws.readyState === WebSocket.CONNECTING) ws.close(3008, "Connection timeout");
|
|
1960
|
+
}, CONNECT_TIMEOUT_MS);
|
|
1961
|
+
ws.once("open", () => clearTimeout(timer));
|
|
1962
|
+
ws.once("close", () => clearTimeout(timer));
|
|
1963
|
+
return ws;
|
|
1964
|
+
}
|
|
1965
|
+
//#endregion
|
|
1966
|
+
//#region src/operations/rpc.ts
|
|
2148
1967
|
/**
|
|
2149
1968
|
* The only methods Studio may call on an agent. A `StudioSession` carries far more than
|
|
2150
1969
|
* {@link AgentApi}, so it is wrapped rather than exposed: what Cap'n Web can reach is exactly what
|
|
@@ -2205,16 +2024,253 @@ const connectWebSocketRpc = async ({ url, token, instanceId, local }) => {
|
|
|
2205
2024
|
};
|
|
2206
2025
|
};
|
|
2207
2026
|
//#endregion
|
|
2208
|
-
//#region src/
|
|
2027
|
+
//#region src/operations/hooks.ts
|
|
2209
2028
|
/**
|
|
2210
|
-
*
|
|
2029
|
+
* Register a `kubb:hook:start` listener that spawns the requested command via tinyexec,
|
|
2030
|
+
* streams each stdout line as a `kubb:hook:line` event, and calls `kubb:hook:end` with the result.
|
|
2031
|
+
* Streaming the output lets Kubb Studio render live hook progress over the WebSocket connection.
|
|
2032
|
+
*
|
|
2033
|
+
* Returns a remover, so a session that runs one generation after another on the same emitter does
|
|
2034
|
+
* not stack a listener per run.
|
|
2211
2035
|
*/
|
|
2036
|
+
function setupHookListener(hooks, root, signal) {
|
|
2037
|
+
return hooks.hook("kubb:hook:start", async (ctx) => {
|
|
2038
|
+
const { id, command, args } = ctx;
|
|
2039
|
+
if (!id) return;
|
|
2040
|
+
const commandWithArgs = args?.length ? `${command} ${args.join(" ")}` : command;
|
|
2041
|
+
try {
|
|
2042
|
+
const proc = x(command, [...args ?? []], {
|
|
2043
|
+
signal,
|
|
2044
|
+
nodeOptions: {
|
|
2045
|
+
cwd: root,
|
|
2046
|
+
detached: true
|
|
2047
|
+
}
|
|
2048
|
+
});
|
|
2049
|
+
for await (const line of proc) await hooks.callHook("kubb:hook:line", {
|
|
2050
|
+
id,
|
|
2051
|
+
line
|
|
2052
|
+
});
|
|
2053
|
+
const { exitCode } = await proc;
|
|
2054
|
+
if (exitCode !== 0) {
|
|
2055
|
+
const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
|
|
2056
|
+
await hooks.callHook("kubb:hook:end", {
|
|
2057
|
+
id,
|
|
2058
|
+
command,
|
|
2059
|
+
args,
|
|
2060
|
+
success: false,
|
|
2061
|
+
error
|
|
2062
|
+
});
|
|
2063
|
+
await hooks.callHook("kubb:error", { error });
|
|
2064
|
+
return;
|
|
2065
|
+
}
|
|
2066
|
+
await hooks.callHook("kubb:hook:end", {
|
|
2067
|
+
id,
|
|
2068
|
+
command,
|
|
2069
|
+
args,
|
|
2070
|
+
success: true,
|
|
2071
|
+
error: null
|
|
2072
|
+
});
|
|
2073
|
+
} catch (caughtError) {
|
|
2074
|
+
const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
|
|
2075
|
+
error.cause = caughtError;
|
|
2076
|
+
await hooks.callHook("kubb:hook:end", {
|
|
2077
|
+
id,
|
|
2078
|
+
command,
|
|
2079
|
+
args,
|
|
2080
|
+
success: false,
|
|
2081
|
+
error
|
|
2082
|
+
});
|
|
2083
|
+
await hooks.callHook("kubb:error", { error });
|
|
2084
|
+
}
|
|
2085
|
+
});
|
|
2086
|
+
}
|
|
2087
|
+
/**
|
|
2088
|
+
* Waits for the `kubb:hook:end` matching `hookId`. Register this before calling `kubb:hook:start`:
|
|
2089
|
+
* `callHook` awaits its listeners, and {@link setupHookListener} calls `kubb:hook:end` from inside
|
|
2090
|
+
* that same listener, so a handler added afterward would already have missed it.
|
|
2091
|
+
*/
|
|
2092
|
+
function waitForHookEnd(hooks, hookId) {
|
|
2093
|
+
return new Promise((resolve, reject) => {
|
|
2094
|
+
const handleHookEnd = (ctx) => {
|
|
2095
|
+
if (ctx.id !== hookId) return;
|
|
2096
|
+
hooks.removeHook("kubb:hook:end", handleHookEnd);
|
|
2097
|
+
if (ctx.success) {
|
|
2098
|
+
resolve();
|
|
2099
|
+
return;
|
|
2100
|
+
}
|
|
2101
|
+
reject(ctx.error);
|
|
2102
|
+
};
|
|
2103
|
+
hooks.hook("kubb:hook:end", handleHookEnd);
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
//#endregion
|
|
2107
|
+
//#region src/operations/generate.ts
|
|
2212
2108
|
const DISK_SNAPSHOT_MAX_FILES = 1e4;
|
|
2213
2109
|
/**
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2216
|
-
*
|
|
2110
|
+
* `isToolAvailable` spawns a process, and a long-lived connection generates repeatedly, so each
|
|
2111
|
+
* executable is probed once per process. The CLI deliberately does not memoize: a `--watch` build
|
|
2112
|
+
* should keep noticing a tool installed mid-session.
|
|
2113
|
+
*/
|
|
2114
|
+
const detectTool = memoize(/* @__PURE__ */ new Map(), detectTool$1);
|
|
2115
|
+
/**
|
|
2116
|
+
* The two post-build tool steps. Formatting and linting differ only in which tools they look for,
|
|
2117
|
+
* so they run through one loop rather than two near-identical blocks.
|
|
2118
|
+
*
|
|
2119
|
+
* `noun` and `verbing` are spelled out instead of built from `kind`. Concatenating `` `${kind}ter` ``
|
|
2120
|
+
* and `` `${kind}ting` `` works for `format`, but doubles the `t` in `lint`, giving "lintter" and
|
|
2121
|
+
* "lintting" instead of "linter" and "linting".
|
|
2122
|
+
*/
|
|
2123
|
+
const TOOL_STEPS = [{
|
|
2124
|
+
kind: "format",
|
|
2125
|
+
noun: "formatter",
|
|
2126
|
+
verbing: "Formatting",
|
|
2127
|
+
tools: formatters,
|
|
2128
|
+
detect: FORMATTER_PREFERENCE
|
|
2129
|
+
}, {
|
|
2130
|
+
kind: "lint",
|
|
2131
|
+
noun: "linter",
|
|
2132
|
+
verbing: "Linting",
|
|
2133
|
+
tools: linters,
|
|
2134
|
+
detect: LINTER_PREFERENCE
|
|
2135
|
+
}];
|
|
2136
|
+
/**
|
|
2137
|
+
* Absolute path of the directory the formatter and linter are pointed at.
|
|
2138
|
+
*/
|
|
2139
|
+
function outputPath(config) {
|
|
2140
|
+
return path.isAbsolute(config.output.path) ? config.output.path : path.resolve(process$1.cwd(), config.root, config.output.path);
|
|
2141
|
+
}
|
|
2142
|
+
/**
|
|
2143
|
+
* Emits `kubb:hook:start` and waits for the matching `kubb:hook:end`. The host spawns the process:
|
|
2144
|
+
* this only describes what to run and when it finished.
|
|
2145
|
+
*
|
|
2146
|
+
* @throws whatever the command failed with, so callers can report it their own way.
|
|
2147
|
+
*/
|
|
2148
|
+
async function runHook({ hooks, id, command, args }) {
|
|
2149
|
+
const hookId = hash("sha256", id);
|
|
2150
|
+
const hookEnd = waitForHookEnd(hooks, hookId);
|
|
2151
|
+
await hooks.callHook("kubb:hook:start", {
|
|
2152
|
+
id: hookId,
|
|
2153
|
+
command,
|
|
2154
|
+
args: [...args]
|
|
2155
|
+
});
|
|
2156
|
+
await hookEnd;
|
|
2157
|
+
}
|
|
2158
|
+
function isProblemErrorDiagnostic(diagnostic) {
|
|
2159
|
+
return (diagnostic.kind ?? "problem") === "problem" && diagnostic.severity === "error";
|
|
2160
|
+
}
|
|
2161
|
+
/**
|
|
2162
|
+
* Folds error-severity diagnostics into one thrown error so logs name the failing plugin.
|
|
2163
|
+
*/
|
|
2164
|
+
function formatGenerationFailure(diagnostics) {
|
|
2165
|
+
const reasons = diagnostics.filter(isProblemErrorDiagnostic).map((diagnostic) => diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message);
|
|
2166
|
+
if (!reasons.length) return /* @__PURE__ */ new Error("Generation failed");
|
|
2167
|
+
return /* @__PURE__ */ new Error(`Generation failed: ${reasons.length} error${reasons.length === 1 ? "" : "s"}: ${reasons.join("; ")}`);
|
|
2168
|
+
}
|
|
2169
|
+
/**
|
|
2170
|
+
* Runs a full Kubb code-generation cycle for the given config.
|
|
2171
|
+
*
|
|
2172
|
+
* Emits lifecycle events on the provided `hooks` emitter so callers (e.g. the WebSocket stream)
|
|
2173
|
+
* can forward progress to connected clients. After a successful build, auto-formatting and
|
|
2174
|
+
* linting are applied when configured, followed by any user-defined `hooks.done` commands.
|
|
2217
2175
|
*/
|
|
2176
|
+
async function generate({ config, hooks, signal }) {
|
|
2177
|
+
signal?.throwIfAborted();
|
|
2178
|
+
const hrStart = process$1.hrtime();
|
|
2179
|
+
await hooks.callHook("kubb:generation:start", { config });
|
|
2180
|
+
await hooks.callHook("kubb:info", { message: config.name ? `Setup generation ${config.name}` : "Setup generation" });
|
|
2181
|
+
const kubb = createKubb(config, {
|
|
2182
|
+
hooks,
|
|
2183
|
+
signal
|
|
2184
|
+
});
|
|
2185
|
+
await kubb.setup();
|
|
2186
|
+
await hooks.callHook("kubb:info", { message: config.name ? `Build generation ${config.name}` : "Build generation" });
|
|
2187
|
+
const { files, diagnostics, storage } = await kubb.safeBuild();
|
|
2188
|
+
signal?.throwIfAborted();
|
|
2189
|
+
await hooks.callHook("kubb:info", { message: "Load summary" });
|
|
2190
|
+
for (const diagnostic of diagnostics.filter(isProblemErrorDiagnostic)) await hooks.callHook("kubb:error", { error: new Error(diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message) });
|
|
2191
|
+
const status = Diagnostics.hasError(diagnostics) ? "failed" : "success";
|
|
2192
|
+
await hooks.callHook("kubb:generation:end", {
|
|
2193
|
+
config,
|
|
2194
|
+
storage: {
|
|
2195
|
+
...storage,
|
|
2196
|
+
readKeys: async () => [...new Set(files.map((file) => file.path))]
|
|
2197
|
+
},
|
|
2198
|
+
diagnostics,
|
|
2199
|
+
status,
|
|
2200
|
+
hrStart,
|
|
2201
|
+
filesCreated: files.length
|
|
2202
|
+
});
|
|
2203
|
+
if (status === "failed") throw formatGenerationFailure(diagnostics);
|
|
2204
|
+
await hooks.callHook("kubb:success", { message: "Generation successfully" });
|
|
2205
|
+
for (const step of TOOL_STEPS) {
|
|
2206
|
+
const setting = config.output[step.kind];
|
|
2207
|
+
if (!setting) continue;
|
|
2208
|
+
await hooks.callHook(`kubb:${step.kind}:start`);
|
|
2209
|
+
const tool = setting === "auto" ? await detectTool(step.detect) : setting;
|
|
2210
|
+
if (!tool) await hooks.callHook("kubb:warn", { message: `No ${step.noun} found (${step.detect.join(", ")}). Skipping ${step.verbing.toLowerCase()}.` });
|
|
2211
|
+
if (tool && setting === "auto") await hooks.callHook("kubb:info", { message: `Auto-detected ${step.noun}: ${styleText("dim", tool)}` });
|
|
2212
|
+
const command = tool ? step.tools[tool] : void 0;
|
|
2213
|
+
if (command) try {
|
|
2214
|
+
await runHook({
|
|
2215
|
+
hooks,
|
|
2216
|
+
id: [config.name, tool].filter(Boolean).join("-"),
|
|
2217
|
+
command: command.command,
|
|
2218
|
+
args: command.args(outputPath(config))
|
|
2219
|
+
});
|
|
2220
|
+
await hooks.callHook("kubb:success", { message: `${step.verbing} with ${tool} successfully` });
|
|
2221
|
+
} catch (caughtError) {
|
|
2222
|
+
await hooks.callHook("kubb:error", { error: new Error(command.errorMessage, { cause: caughtError }) });
|
|
2223
|
+
signal?.throwIfAborted();
|
|
2224
|
+
}
|
|
2225
|
+
await hooks.callHook(`kubb:${step.kind}:end`);
|
|
2226
|
+
}
|
|
2227
|
+
if (config.output.postGenerate?.length) {
|
|
2228
|
+
await hooks.callHook("kubb:hooks:start");
|
|
2229
|
+
for (const entry of config.output.postGenerate) {
|
|
2230
|
+
const line = typeof entry === "string" ? entry : entry.command;
|
|
2231
|
+
const [cmd, ...args] = tokenize(line);
|
|
2232
|
+
if (!cmd) continue;
|
|
2233
|
+
await runHook({
|
|
2234
|
+
hooks,
|
|
2235
|
+
id: line,
|
|
2236
|
+
command: cmd,
|
|
2237
|
+
args
|
|
2238
|
+
});
|
|
2239
|
+
await hooks.callHook("kubb:success", { message: `${line} successfully executed` });
|
|
2240
|
+
}
|
|
2241
|
+
await hooks.callHook("kubb:hooks:end");
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
async function runGenerationOperation({ config, hooks, signal, jobId, store, snapshotRoot, maxSnapshotMb }) {
|
|
2245
|
+
const diskFiles = snapshotRoot ? await listDisk({
|
|
2246
|
+
root: snapshotRoot,
|
|
2247
|
+
outputPath: config.output.path,
|
|
2248
|
+
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2249
|
+
}) : void 0;
|
|
2250
|
+
const disk = diskFiles ? await store.keep({
|
|
2251
|
+
jobId,
|
|
2252
|
+
source: "disk",
|
|
2253
|
+
files: diskFiles,
|
|
2254
|
+
maxSetMb: maxSnapshotMb
|
|
2255
|
+
}) : void 0;
|
|
2256
|
+
const removeHookListener = setupHookListener(hooks, config.root, signal);
|
|
2257
|
+
try {
|
|
2258
|
+
await generate({
|
|
2259
|
+
config,
|
|
2260
|
+
hooks,
|
|
2261
|
+
signal
|
|
2262
|
+
});
|
|
2263
|
+
} catch (error) {
|
|
2264
|
+
await store.drop(jobId);
|
|
2265
|
+
throw error;
|
|
2266
|
+
} finally {
|
|
2267
|
+
removeHookListener();
|
|
2268
|
+
}
|
|
2269
|
+
return disk;
|
|
2270
|
+
}
|
|
2271
|
+
//#endregion
|
|
2272
|
+
//#region src/runtime/StudioSession.ts
|
|
2273
|
+
/** A sandbox shares one in-memory generation store across tenants, so old generations expire. */
|
|
2218
2274
|
const SANDBOX_GENERATION_TTL_MS = 9e5;
|
|
2219
2275
|
/**
|
|
2220
2276
|
* A fresh root for one sandbox job. Kubb keys its output manifest cache by root, so tenants that
|
|
@@ -2276,7 +2332,6 @@ function applyStudioDefaults(options) {
|
|
|
2276
2332
|
permissions: {
|
|
2277
2333
|
allowWrite: false,
|
|
2278
2334
|
allowConfigEdit: false,
|
|
2279
|
-
allowInput: false,
|
|
2280
2335
|
allowExec: false,
|
|
2281
2336
|
allowRead: false,
|
|
2282
2337
|
...options.permissions
|
|
@@ -2412,13 +2467,6 @@ var StudioSession = class {
|
|
|
2412
2467
|
return !this.#isSandbox && this.#options.permissions.allowConfigEdit;
|
|
2413
2468
|
}
|
|
2414
2469
|
/**
|
|
2415
|
-
* A sandbox agent always generates from the spec Studio supplies. A local agent only when the
|
|
2416
|
-
* host opted in.
|
|
2417
|
-
*/
|
|
2418
|
-
get #canUseInput() {
|
|
2419
|
-
return this.#isSandbox || this.#options.permissions.allowInput;
|
|
2420
|
-
}
|
|
2421
|
-
/**
|
|
2422
2470
|
* A sandbox agent always allows reading its output back; a local agent only when opted in.
|
|
2423
2471
|
*/
|
|
2424
2472
|
get #canRead() {
|
|
@@ -2524,20 +2572,12 @@ var StudioSession = class {
|
|
|
2524
2572
|
clearTimeout(timer);
|
|
2525
2573
|
}
|
|
2526
2574
|
}
|
|
2527
|
-
/**
|
|
2528
|
-
* Whether memory still leaves room for another job. Always, when the host set no budget.
|
|
2529
|
-
*/
|
|
2530
|
-
#isAccepting(memoryMb = rssMb()) {
|
|
2531
|
-
const budget = this.#options.capacity.memoryBudgetMb;
|
|
2532
|
-
return budget === void 0 || memoryMb <= budget * 1.5;
|
|
2533
|
-
}
|
|
2534
2575
|
async #load() {
|
|
2535
|
-
const memoryMb = rssMb();
|
|
2536
2576
|
return {
|
|
2537
2577
|
running: this.#isGenerating ? 1 : 0,
|
|
2538
|
-
rssMb: Math.round(
|
|
2578
|
+
rssMb: Math.round(rssMb()),
|
|
2539
2579
|
storeBytes: await this.#generations.bytes(),
|
|
2540
|
-
accepting:
|
|
2580
|
+
accepting: true
|
|
2541
2581
|
};
|
|
2542
2582
|
}
|
|
2543
2583
|
/**
|
|
@@ -2575,7 +2615,6 @@ var StudioSession = class {
|
|
|
2575
2615
|
permissions: {
|
|
2576
2616
|
...permissions,
|
|
2577
2617
|
allowWrite: this.#canWrite,
|
|
2578
|
-
allowInput: this.#canUseInput,
|
|
2579
2618
|
allowConfigEdit: this.#canEditConfig,
|
|
2580
2619
|
allowRead: this.#canRead
|
|
2581
2620
|
}
|
|
@@ -2646,7 +2685,6 @@ var StudioSession = class {
|
|
|
2646
2685
|
}
|
|
2647
2686
|
async #runGeneration(data, controller, cancelRun) {
|
|
2648
2687
|
if (this.#isGenerating) return this.#refuse("Ignored generate: a generation is already in progress", "A generation is already in progress, please wait for it to finish");
|
|
2649
|
-
if (!this.#isAccepting()) return this.#refuse("Ignored generate: the agent is past its memory budget", "The agent is past its memory budget, try again once it frees memory");
|
|
2650
2688
|
this.#isGenerating = true;
|
|
2651
2689
|
this.#activeJob = {
|
|
2652
2690
|
jobId: data.jobId,
|
|
@@ -2662,48 +2700,33 @@ var StudioSession = class {
|
|
|
2662
2700
|
const patch = data.config;
|
|
2663
2701
|
const plugins = await mergePlugins(config.plugins, patch?.plugins);
|
|
2664
2702
|
const adapter = await mergeAdapter(config.adapter, patch?.adapter);
|
|
2665
|
-
const inputOverride = this.#isSandbox ? patch?.input ?? "" :
|
|
2703
|
+
const inputOverride = this.#isSandbox ? patch?.input ?? "" : void 0;
|
|
2666
2704
|
if (permissions.allowWrite && this.#isSandbox) await this.#warn("Running in a sandbox, so writing files is disabled");
|
|
2667
|
-
if (patch?.input && !this.#
|
|
2705
|
+
if (patch?.input && !this.#isSandbox) await this.#warn("Ignored the spec from Studio: generating from a Studio spec is only available to a sandbox agent");
|
|
2668
2706
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2669
2707
|
this.#lastGeneration = void 0;
|
|
2670
|
-
const
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
const detach = [setupHookListener(this.#hooks, root, controller.signal)];
|
|
2682
|
-
try {
|
|
2683
|
-
await generate({
|
|
2684
|
-
config: {
|
|
2685
|
-
...config,
|
|
2686
|
-
root,
|
|
2687
|
-
input: inputOverride ?? config.input,
|
|
2688
|
-
storage: this.#canWrite ? fsStorage() : memoryStorage(),
|
|
2689
|
-
output: permissions.allowExec ? { ...config.output } : {
|
|
2690
|
-
...config.output,
|
|
2691
|
-
format: false,
|
|
2692
|
-
lint: false,
|
|
2693
|
-
postGenerate: []
|
|
2694
|
-
},
|
|
2695
|
-
plugins: resolvedPlugins,
|
|
2696
|
-
adapter
|
|
2708
|
+
const disk = await runGenerationOperation({
|
|
2709
|
+
config: {
|
|
2710
|
+
...config,
|
|
2711
|
+
root,
|
|
2712
|
+
input: inputOverride ?? config.input,
|
|
2713
|
+
storage: this.#canWrite ? fsStorage() : memoryStorage(),
|
|
2714
|
+
output: permissions.allowExec ? { ...config.output } : {
|
|
2715
|
+
...config.output,
|
|
2716
|
+
format: false,
|
|
2717
|
+
lint: false,
|
|
2718
|
+
postGenerate: []
|
|
2697
2719
|
},
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
}
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2720
|
+
plugins: resolvedPlugins,
|
|
2721
|
+
adapter
|
|
2722
|
+
},
|
|
2723
|
+
hooks: this.#hooks,
|
|
2724
|
+
signal: controller.signal,
|
|
2725
|
+
jobId: data.jobId,
|
|
2726
|
+
store: this.#generations,
|
|
2727
|
+
snapshotRoot: this.#hasProjectOnDisk ? root : void 0,
|
|
2728
|
+
maxSnapshotMb: this.#limits.maxSnapshotMb
|
|
2729
|
+
});
|
|
2707
2730
|
await this.#hooks.callHook("studio:command:end", {
|
|
2708
2731
|
command,
|
|
2709
2732
|
info: `${resolvedPlugins.length} plugin${resolvedPlugins.length === 1 ? "" : "s"}, ${this.#canWrite ? "written to disk" : "in memory"}${inputOverride !== void 0 ? ", from a Studio spec" : ""}`
|
|
@@ -2765,8 +2788,10 @@ var StudioSession = class {
|
|
|
2765
2788
|
}
|
|
2766
2789
|
if (this.#isGenerating) return refuse("a generation is in progress");
|
|
2767
2790
|
try {
|
|
2768
|
-
const { source: patched, outcomes, changed } =
|
|
2769
|
-
|
|
2791
|
+
const { source: patched, outcomes, changed } = await writeConfigEdits({
|
|
2792
|
+
filePath: configFile,
|
|
2793
|
+
edits
|
|
2794
|
+
});
|
|
2770
2795
|
const applied = outcomes.filter((outcome) => outcome.applied).length;
|
|
2771
2796
|
await this.#hooks.callHook("studio:command:end", {
|
|
2772
2797
|
command,
|
|
@@ -2804,24 +2829,14 @@ var StudioSession = class {
|
|
|
2804
2829
|
version,
|
|
2805
2830
|
peerDependencies: generation.peerDependencies
|
|
2806
2831
|
});
|
|
2807
|
-
const { token, studioUrl } = this.#options;
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
});
|
|
2815
|
-
const storageUrl = redirect.headers.get("location");
|
|
2816
|
-
if (redirect.status !== 307 || !storageUrl) throw new Error(`Studio did not provide a storage URL (status ${redirect.status})`);
|
|
2817
|
-
const storage = new URL(storageUrl);
|
|
2818
|
-
if (storage.protocol !== "https:" && storage.hostname !== "localhost" && storage.hostname !== "127.0.0.1") throw new Error(`Refusing snapshot upload to ${storage.origin}`);
|
|
2819
|
-
const response = await fetch(storage, {
|
|
2820
|
-
method: "PUT",
|
|
2821
|
-
body: new Uint8Array(bytes),
|
|
2822
|
-
redirect: "error"
|
|
2832
|
+
const { token, studioUrl, signal } = this.#options;
|
|
2833
|
+
await uploadSnapshot({
|
|
2834
|
+
bytes,
|
|
2835
|
+
uploadPath,
|
|
2836
|
+
studioUrl,
|
|
2837
|
+
token,
|
|
2838
|
+
shutdown: signal
|
|
2823
2839
|
});
|
|
2824
|
-
if (!response.ok) throw new Error(`Snapshot upload failed with status ${response.status}`);
|
|
2825
2840
|
await this.#hooks.callHook("studio:command:end", {
|
|
2826
2841
|
command,
|
|
2827
2842
|
info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
|
|
@@ -2835,10 +2850,7 @@ var StudioSession = class {
|
|
|
2835
2850
|
throw error;
|
|
2836
2851
|
}
|
|
2837
2852
|
}
|
|
2838
|
-
/**
|
|
2839
|
-
* An agent with a project on disk can show a run against what its output directory held before.
|
|
2840
|
-
* A sandbox agent has no project.
|
|
2841
|
-
*/
|
|
2853
|
+
/** A sandbox has no project directory to snapshot. */
|
|
2842
2854
|
get #hasProjectOnDisk() {
|
|
2843
2855
|
return !this.#isSandbox && this.#canRead;
|
|
2844
2856
|
}
|
|
@@ -2867,7 +2879,7 @@ var StudioSession = class {
|
|
|
2867
2879
|
}
|
|
2868
2880
|
};
|
|
2869
2881
|
//#endregion
|
|
2870
|
-
//#region src/client.ts
|
|
2882
|
+
//#region src/runtime/client.ts
|
|
2871
2883
|
/**
|
|
2872
2884
|
* Creates the Kubb Studio client: the connection, the command loop, and the generation event
|
|
2873
2885
|
* stream shared by the `kubb studio` CLI command and the Docker agent.
|
|
@@ -2883,7 +2895,7 @@ var StudioSession = class {
|
|
|
2883
2895
|
*/
|
|
2884
2896
|
function createClient({ onAuthRequired, ...options }) {
|
|
2885
2897
|
const controller = new AbortController();
|
|
2886
|
-
const instanceId = randomUUID();
|
|
2898
|
+
const instanceId = options.instanceId ?? randomUUID();
|
|
2887
2899
|
function notifyAuthRequired(error) {
|
|
2888
2900
|
if (controller.signal.aborted) return;
|
|
2889
2901
|
controller.abort();
|
|
@@ -2904,7 +2916,7 @@ function createClient({ onAuthRequired, ...options }) {
|
|
|
2904
2916
|
};
|
|
2905
2917
|
}
|
|
2906
2918
|
//#endregion
|
|
2907
|
-
//#region src/runConnection.ts
|
|
2919
|
+
//#region src/runtime/runConnection.ts
|
|
2908
2920
|
/**
|
|
2909
2921
|
* Waits for whichever comes first: the shutdown signal, or Studio rejecting the token during a
|
|
2910
2922
|
* background reconnect. Resolves with the rejection, or nothing when the run is being shut down.
|
|
@@ -2977,7 +2989,7 @@ async function runConnection({ credentials, clientOptions, onTokenRejected, sign
|
|
|
2977
2989
|
}
|
|
2978
2990
|
}
|
|
2979
2991
|
//#endregion
|
|
2980
|
-
//#region src/pair.ts
|
|
2992
|
+
//#region src/operations/pair.ts
|
|
2981
2993
|
/** Labels, not secrets: a person approving the code in the browser is what authorizes a pairing. */
|
|
2982
2994
|
const CLIENT_IDS = {
|
|
2983
2995
|
cli: "kubb-cli",
|