@kubb/studio 5.3.17 → 5.3.18
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 -0
- package/dist/index.cjs +365 -344
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +17 -12
- package/dist/index.js +365 -344
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.ts +3 -8
- package/dist/protocol.js.map +1 -1
- package/package.json +4 -4
package/dist/index.cjs
CHANGED
|
@@ -35,7 +35,6 @@ let node_process = require("node:process");
|
|
|
35
35
|
node_process = __toESM(node_process, 1);
|
|
36
36
|
let node_child_process = require("node:child_process");
|
|
37
37
|
let _kubb_core = require("@kubb/core");
|
|
38
|
-
let tinyexec = require("tinyexec");
|
|
39
38
|
let node_timers_promises = require("node:timers/promises");
|
|
40
39
|
let ofetch = require("ofetch");
|
|
41
40
|
let node_util = require("node:util");
|
|
@@ -52,6 +51,7 @@ let tsdown = require("tsdown");
|
|
|
52
51
|
let capnweb = require("capnweb");
|
|
53
52
|
let ws = require("ws");
|
|
54
53
|
ws = __toESM(ws, 1);
|
|
54
|
+
let tinyexec = require("tinyexec");
|
|
55
55
|
//#region ../../internals/utils/src/casing.ts
|
|
56
56
|
/**
|
|
57
57
|
* Shared implementation for camelCase and PascalCase conversion.
|
|
@@ -375,89 +375,9 @@ function getElapsedMs(hrStart) {
|
|
|
375
375
|
}
|
|
376
376
|
//#endregion
|
|
377
377
|
//#region package.json
|
|
378
|
-
var version = "5.3.
|
|
378
|
+
var version = "5.3.18";
|
|
379
379
|
//#endregion
|
|
380
|
-
//#region src/
|
|
381
|
-
/**
|
|
382
|
-
* Register a `kubb:hook:start` listener that spawns the requested command via tinyexec,
|
|
383
|
-
* streams each stdout line as a `kubb:hook:line` event, and calls `kubb:hook:end` with the result.
|
|
384
|
-
* Streaming the output lets Kubb Studio render live hook progress over the WebSocket connection.
|
|
385
|
-
*
|
|
386
|
-
* Returns a remover, so a session that runs one generation after another on the same emitter does
|
|
387
|
-
* not stack a listener per run.
|
|
388
|
-
*/
|
|
389
|
-
function setupHookListener(hooks, root, signal) {
|
|
390
|
-
return hooks.hook("kubb:hook:start", async (ctx) => {
|
|
391
|
-
const { id, command, args } = ctx;
|
|
392
|
-
if (!id) return;
|
|
393
|
-
const commandWithArgs = args?.length ? `${command} ${args.join(" ")}` : command;
|
|
394
|
-
try {
|
|
395
|
-
const proc = (0, tinyexec.x)(command, [...args ?? []], {
|
|
396
|
-
signal,
|
|
397
|
-
nodeOptions: {
|
|
398
|
-
cwd: root,
|
|
399
|
-
detached: true
|
|
400
|
-
}
|
|
401
|
-
});
|
|
402
|
-
for await (const line of proc) await hooks.callHook("kubb:hook:line", {
|
|
403
|
-
id,
|
|
404
|
-
line
|
|
405
|
-
});
|
|
406
|
-
const { exitCode } = await proc;
|
|
407
|
-
if (exitCode !== 0) {
|
|
408
|
-
const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
|
|
409
|
-
await hooks.callHook("kubb:hook:end", {
|
|
410
|
-
id,
|
|
411
|
-
command,
|
|
412
|
-
args,
|
|
413
|
-
success: false,
|
|
414
|
-
error
|
|
415
|
-
});
|
|
416
|
-
await hooks.callHook("kubb:error", { error });
|
|
417
|
-
return;
|
|
418
|
-
}
|
|
419
|
-
await hooks.callHook("kubb:hook:end", {
|
|
420
|
-
id,
|
|
421
|
-
command,
|
|
422
|
-
args,
|
|
423
|
-
success: true,
|
|
424
|
-
error: null
|
|
425
|
-
});
|
|
426
|
-
} catch (caughtError) {
|
|
427
|
-
const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
|
|
428
|
-
error.cause = caughtError;
|
|
429
|
-
await hooks.callHook("kubb:hook:end", {
|
|
430
|
-
id,
|
|
431
|
-
command,
|
|
432
|
-
args,
|
|
433
|
-
success: false,
|
|
434
|
-
error
|
|
435
|
-
});
|
|
436
|
-
await hooks.callHook("kubb:error", { error });
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
/**
|
|
441
|
-
* Waits for the `kubb:hook:end` matching `hookId`. Register this before calling `kubb:hook:start`:
|
|
442
|
-
* `callHook` awaits its listeners, and {@link setupHookListener} calls `kubb:hook:end` from inside
|
|
443
|
-
* that same listener, so a handler added afterward would already have missed it.
|
|
444
|
-
*/
|
|
445
|
-
function waitForHookEnd(hooks, hookId) {
|
|
446
|
-
return new Promise((resolve, reject) => {
|
|
447
|
-
const handleHookEnd = (ctx) => {
|
|
448
|
-
if (ctx.id !== hookId) return;
|
|
449
|
-
hooks.removeHook("kubb:hook:end", handleHookEnd);
|
|
450
|
-
if (ctx.success) {
|
|
451
|
-
resolve();
|
|
452
|
-
return;
|
|
453
|
-
}
|
|
454
|
-
reject(ctx.error);
|
|
455
|
-
};
|
|
456
|
-
hooks.hook("kubb:hook:end", handleHookEnd);
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
//#endregion
|
|
460
|
-
//#region src/machine.ts
|
|
380
|
+
//#region src/operations/machine.ts
|
|
461
381
|
/**
|
|
462
382
|
* Key-value storage the runtime uses for its machine secret and the last Studio config.
|
|
463
383
|
*
|
|
@@ -517,7 +437,7 @@ async function getMachineToken() {
|
|
|
517
437
|
return machineTokenFrom(await fallbackSecretPromise);
|
|
518
438
|
}
|
|
519
439
|
//#endregion
|
|
520
|
-
//#region src/api.ts
|
|
440
|
+
//#region src/operations/api.ts
|
|
521
441
|
/**
|
|
522
442
|
* Reads a human-readable message from a Studio JSON error body, when it has one. `FetchError`'s own
|
|
523
443
|
* message stops at the status line, so the detail Studio sends with a failure (an agent limit, a
|
|
@@ -653,7 +573,7 @@ function withJitter(ms) {
|
|
|
653
573
|
* const finished = await waitForJob({ studioUrl, token, id: job.id })
|
|
654
574
|
* ```
|
|
655
575
|
*/
|
|
656
|
-
async function createJob({ studioUrl, token, type, agentId, name, version, commit, baseId, config, timeoutMs = 6e4, signal }) {
|
|
576
|
+
async function createJob({ studioUrl, token, type, agentId, name, version, commit, baseId, config, instanceId, timeoutMs = 6e4, signal }) {
|
|
657
577
|
const deadline = Date.now() + timeoutMs;
|
|
658
578
|
let interval = CREATE_JOB_INITIAL_DELAY_MS;
|
|
659
579
|
for (;;) {
|
|
@@ -669,7 +589,8 @@ async function createJob({ studioUrl, token, type, agentId, name, version, commi
|
|
|
669
589
|
version,
|
|
670
590
|
commit,
|
|
671
591
|
baseId,
|
|
672
|
-
config
|
|
592
|
+
config,
|
|
593
|
+
instanceId
|
|
673
594
|
},
|
|
674
595
|
retry: false,
|
|
675
596
|
timeout: Math.max(deadline - Date.now(), 1),
|
|
@@ -758,7 +679,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
|
|
|
758
679
|
}
|
|
759
680
|
}
|
|
760
681
|
//#endregion
|
|
761
|
-
//#region src/resolveConfig.ts
|
|
682
|
+
//#region src/operations/resolveConfig.ts
|
|
762
683
|
/**
|
|
763
684
|
* Imports a package, falling back to how the user's project would resolve it.
|
|
764
685
|
*
|
|
@@ -916,7 +837,7 @@ async function mergeAdapter(diskAdapter, studioOptions) {
|
|
|
916
837
|
return factory((0, remeda.mergeDeep)(diskAdapter.options ?? {}, studioOptions));
|
|
917
838
|
}
|
|
918
839
|
//#endregion
|
|
919
|
-
//#region src/configFile.ts
|
|
840
|
+
//#region src/operations/configFile.ts
|
|
920
841
|
/**
|
|
921
842
|
* A valid JavaScript identifier, so an import name can only ever print as `import { name } from`,
|
|
922
843
|
* never as source that breaks out of the import statement.
|
|
@@ -1456,6 +1377,11 @@ function applyConfigEdits(source, edits) {
|
|
|
1456
1377
|
changed: current !== source
|
|
1457
1378
|
};
|
|
1458
1379
|
}
|
|
1380
|
+
async function writeConfigEdits({ filePath, edits }) {
|
|
1381
|
+
const result = applyConfigEdits(await read(filePath), edits);
|
|
1382
|
+
if (result.changed) await (0, node_fs_promises.writeFile)(filePath, result.source, "utf-8");
|
|
1383
|
+
return result;
|
|
1384
|
+
}
|
|
1459
1385
|
/**
|
|
1460
1386
|
* The 1-based line where the file's last import declaration ends, or `0` when it has none. Read
|
|
1461
1387
|
* off the parsed module, so a multi-line `import {\n x,\n} from '...'` reports its closing line
|
|
@@ -1488,144 +1414,7 @@ function withTrailingNewline(code, hadTrailingNewline) {
|
|
|
1488
1414
|
return `${code}\n`;
|
|
1489
1415
|
}
|
|
1490
1416
|
//#endregion
|
|
1491
|
-
//#region src/
|
|
1492
|
-
/**
|
|
1493
|
-
* `isToolAvailable` spawns a process, and a long-lived connection generates repeatedly, so each
|
|
1494
|
-
* executable is probed once per process. The CLI deliberately does not memoize: a `--watch` build
|
|
1495
|
-
* should keep noticing a tool installed mid-session.
|
|
1496
|
-
*/
|
|
1497
|
-
const detectTool = memoize(/* @__PURE__ */ new Map(), detectTool$1);
|
|
1498
|
-
/**
|
|
1499
|
-
* The two post-build tool steps. Formatting and linting differ only in which tools they look for,
|
|
1500
|
-
* so they run through one loop rather than two near-identical blocks.
|
|
1501
|
-
*
|
|
1502
|
-
* `noun` and `verbing` are spelled out instead of built from `kind`. Concatenating `` `${kind}ter` ``
|
|
1503
|
-
* and `` `${kind}ting` `` works for `format`, but doubles the `t` in `lint`, giving "lintter" and
|
|
1504
|
-
* "lintting" instead of "linter" and "linting".
|
|
1505
|
-
*/
|
|
1506
|
-
const TOOL_STEPS = [{
|
|
1507
|
-
kind: "format",
|
|
1508
|
-
noun: "formatter",
|
|
1509
|
-
verbing: "Formatting",
|
|
1510
|
-
tools: formatters,
|
|
1511
|
-
detect: FORMATTER_PREFERENCE
|
|
1512
|
-
}, {
|
|
1513
|
-
kind: "lint",
|
|
1514
|
-
noun: "linter",
|
|
1515
|
-
verbing: "Linting",
|
|
1516
|
-
tools: linters,
|
|
1517
|
-
detect: LINTER_PREFERENCE
|
|
1518
|
-
}];
|
|
1519
|
-
/**
|
|
1520
|
-
* Absolute path of the directory the formatter and linter are pointed at.
|
|
1521
|
-
*/
|
|
1522
|
-
function outputPath(config) {
|
|
1523
|
-
return node_path.default.isAbsolute(config.output.path) ? config.output.path : node_path.default.resolve(node_process.default.cwd(), config.root, config.output.path);
|
|
1524
|
-
}
|
|
1525
|
-
/**
|
|
1526
|
-
* Emits `kubb:hook:start` and waits for the matching `kubb:hook:end`. The host spawns the process:
|
|
1527
|
-
* this only describes what to run and when it finished.
|
|
1528
|
-
*
|
|
1529
|
-
* @throws whatever the command failed with, so callers can report it their own way.
|
|
1530
|
-
*/
|
|
1531
|
-
async function runHook({ hooks, id, command, args }) {
|
|
1532
|
-
const hookId = (0, node_crypto.hash)("sha256", id);
|
|
1533
|
-
const hookEnd = waitForHookEnd(hooks, hookId);
|
|
1534
|
-
await hooks.callHook("kubb:hook:start", {
|
|
1535
|
-
id: hookId,
|
|
1536
|
-
command,
|
|
1537
|
-
args: [...args]
|
|
1538
|
-
});
|
|
1539
|
-
await hookEnd;
|
|
1540
|
-
}
|
|
1541
|
-
function isProblemErrorDiagnostic(diagnostic) {
|
|
1542
|
-
return (diagnostic.kind ?? "problem") === "problem" && diagnostic.severity === "error";
|
|
1543
|
-
}
|
|
1544
|
-
/**
|
|
1545
|
-
* Folds error-severity diagnostics into one thrown error so logs name the failing plugin.
|
|
1546
|
-
*/
|
|
1547
|
-
function formatGenerationFailure(diagnostics) {
|
|
1548
|
-
const reasons = diagnostics.filter(isProblemErrorDiagnostic).map((diagnostic) => diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message);
|
|
1549
|
-
if (!reasons.length) return /* @__PURE__ */ new Error("Generation failed");
|
|
1550
|
-
return /* @__PURE__ */ new Error(`Generation failed: ${reasons.length} error${reasons.length === 1 ? "" : "s"}: ${reasons.join("; ")}`);
|
|
1551
|
-
}
|
|
1552
|
-
/**
|
|
1553
|
-
* Runs a full Kubb code-generation cycle for the given config.
|
|
1554
|
-
*
|
|
1555
|
-
* Emits lifecycle events on the provided `hooks` emitter so callers (e.g. the WebSocket stream)
|
|
1556
|
-
* can forward progress to connected clients. After a successful build, auto-formatting and
|
|
1557
|
-
* linting are applied when configured, followed by any user-defined `hooks.done` commands.
|
|
1558
|
-
*/
|
|
1559
|
-
async function generate({ config, hooks, signal }) {
|
|
1560
|
-
signal?.throwIfAborted();
|
|
1561
|
-
const hrStart = node_process.default.hrtime();
|
|
1562
|
-
await hooks.callHook("kubb:generation:start", { config });
|
|
1563
|
-
await hooks.callHook("kubb:info", { message: config.name ? `Setup generation ${config.name}` : "Setup generation" });
|
|
1564
|
-
const kubb = (0, _kubb_core.createKubb)(config, {
|
|
1565
|
-
hooks,
|
|
1566
|
-
signal
|
|
1567
|
-
});
|
|
1568
|
-
await kubb.setup();
|
|
1569
|
-
await hooks.callHook("kubb:info", { message: config.name ? `Build generation ${config.name}` : "Build generation" });
|
|
1570
|
-
const { files, diagnostics, storage } = await kubb.safeBuild();
|
|
1571
|
-
signal?.throwIfAborted();
|
|
1572
|
-
await hooks.callHook("kubb:info", { message: "Load summary" });
|
|
1573
|
-
for (const diagnostic of diagnostics.filter(isProblemErrorDiagnostic)) await hooks.callHook("kubb:error", { error: new Error(diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message) });
|
|
1574
|
-
const status = _kubb_core.Diagnostics.hasError(diagnostics) ? "failed" : "success";
|
|
1575
|
-
await hooks.callHook("kubb:generation:end", {
|
|
1576
|
-
config,
|
|
1577
|
-
storage: {
|
|
1578
|
-
...storage,
|
|
1579
|
-
readKeys: async () => [...new Set(files.map((file) => file.path))]
|
|
1580
|
-
},
|
|
1581
|
-
diagnostics,
|
|
1582
|
-
status,
|
|
1583
|
-
hrStart,
|
|
1584
|
-
filesCreated: files.length
|
|
1585
|
-
});
|
|
1586
|
-
if (status === "failed") throw formatGenerationFailure(diagnostics);
|
|
1587
|
-
await hooks.callHook("kubb:success", { message: "Generation successfully" });
|
|
1588
|
-
for (const step of TOOL_STEPS) {
|
|
1589
|
-
const setting = config.output[step.kind];
|
|
1590
|
-
if (!setting) continue;
|
|
1591
|
-
await hooks.callHook(`kubb:${step.kind}:start`);
|
|
1592
|
-
const tool = setting === "auto" ? await detectTool(step.detect) : setting;
|
|
1593
|
-
if (!tool) await hooks.callHook("kubb:warn", { message: `No ${step.noun} found (${step.detect.join(", ")}). Skipping ${step.verbing.toLowerCase()}.` });
|
|
1594
|
-
if (tool && setting === "auto") await hooks.callHook("kubb:info", { message: `Auto-detected ${step.noun}: ${(0, node_util.styleText)("dim", tool)}` });
|
|
1595
|
-
const command = tool ? step.tools[tool] : void 0;
|
|
1596
|
-
if (command) try {
|
|
1597
|
-
await runHook({
|
|
1598
|
-
hooks,
|
|
1599
|
-
id: [config.name, tool].filter(Boolean).join("-"),
|
|
1600
|
-
command: command.command,
|
|
1601
|
-
args: command.args(outputPath(config))
|
|
1602
|
-
});
|
|
1603
|
-
await hooks.callHook("kubb:success", { message: `${step.verbing} with ${tool} successfully` });
|
|
1604
|
-
} catch (caughtError) {
|
|
1605
|
-
await hooks.callHook("kubb:error", { error: new Error(command.errorMessage, { cause: caughtError }) });
|
|
1606
|
-
signal?.throwIfAborted();
|
|
1607
|
-
}
|
|
1608
|
-
await hooks.callHook(`kubb:${step.kind}:end`);
|
|
1609
|
-
}
|
|
1610
|
-
if (config.output.postGenerate?.length) {
|
|
1611
|
-
await hooks.callHook("kubb:hooks:start");
|
|
1612
|
-
for (const entry of config.output.postGenerate) {
|
|
1613
|
-
const line = typeof entry === "string" ? entry : entry.command;
|
|
1614
|
-
const [cmd, ...args] = tokenize(line);
|
|
1615
|
-
if (!cmd) continue;
|
|
1616
|
-
await runHook({
|
|
1617
|
-
hooks,
|
|
1618
|
-
id: line,
|
|
1619
|
-
command: cmd,
|
|
1620
|
-
args
|
|
1621
|
-
});
|
|
1622
|
-
await hooks.callHook("kubb:success", { message: `${line} successfully executed` });
|
|
1623
|
-
}
|
|
1624
|
-
await hooks.callHook("kubb:hooks:end");
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
//#endregion
|
|
1628
|
-
//#region src/constants.ts
|
|
1417
|
+
//#region src/operations/constants.ts
|
|
1629
1418
|
/**
|
|
1630
1419
|
* Hosted Kubb Studio URL. Exported so credential stores can bind tokens to the resolved instance,
|
|
1631
1420
|
* not whatever default the client would pick on its own.
|
|
@@ -1669,18 +1458,16 @@ function resolveGenerationLimits(env = process.env) {
|
|
|
1669
1458
|
};
|
|
1670
1459
|
}
|
|
1671
1460
|
/**
|
|
1672
|
-
* An agent's capacity read from `KUBB_AGENT_MAX_CONCURRENT
|
|
1673
|
-
*
|
|
1461
|
+
* An agent's capacity read from `KUBB_AGENT_MAX_CONCURRENT`. An unset or invalid value keeps the
|
|
1462
|
+
* default: one job at a time.
|
|
1674
1463
|
*/
|
|
1675
1464
|
function resolveAgentCapacity(env = process.env) {
|
|
1676
|
-
return {
|
|
1677
|
-
maxConcurrent: Math.max(1, Math.floor(positiveNumber(env.KUBB_AGENT_MAX_CONCURRENT) ?? agentDefaults.maxConcurrent)),
|
|
1678
|
-
memoryBudgetMb: positiveNumber(env.KUBB_AGENT_MEMORY_BUDGET_MB)
|
|
1679
|
-
};
|
|
1465
|
+
return { maxConcurrent: Math.max(1, Math.floor(positiveNumber(env.KUBB_AGENT_MAX_CONCURRENT) ?? agentDefaults.maxConcurrent)) };
|
|
1680
1466
|
}
|
|
1681
1467
|
//#endregion
|
|
1682
|
-
//#region src/snapshotPackage.ts
|
|
1468
|
+
//#region src/operations/snapshotPackage.ts
|
|
1683
1469
|
const gzipAsync = (0, node_util.promisify)(node_zlib.gzip);
|
|
1470
|
+
const UPLOAD_TIMEOUT_MS = 12e4;
|
|
1684
1471
|
/**
|
|
1685
1472
|
* Maps a generated file's path to its place inside the tarball, stripping everything before a
|
|
1686
1473
|
* `src`/`dist` segment and any `..`/empty path segment so a crafted file name cannot escape the
|
|
@@ -1818,8 +1605,31 @@ async function createSnapshotPackage(files, packageInfo) {
|
|
|
1818
1605
|
});
|
|
1819
1606
|
}
|
|
1820
1607
|
}
|
|
1608
|
+
async function uploadSnapshot({ bytes, uploadPath, studioUrl, token, shutdown }) {
|
|
1609
|
+
const uploadUrl = new URL(uploadPath, studioUrl);
|
|
1610
|
+
if (uploadUrl.origin !== new URL(studioUrl).origin) throw new Error("Snapshot upload path must stay on the Studio origin");
|
|
1611
|
+
const timeout = AbortSignal.timeout(UPLOAD_TIMEOUT_MS);
|
|
1612
|
+
const signal = shutdown ? AbortSignal.any([shutdown, timeout]) : timeout;
|
|
1613
|
+
const redirect = await fetch(uploadUrl, {
|
|
1614
|
+
method: "PUT",
|
|
1615
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
1616
|
+
redirect: "manual",
|
|
1617
|
+
signal
|
|
1618
|
+
});
|
|
1619
|
+
const storageUrl = redirect.headers.get("location");
|
|
1620
|
+
if (redirect.status !== 307 || !storageUrl) throw new Error(`Studio did not provide a storage URL (status ${redirect.status})`);
|
|
1621
|
+
const storage = new URL(storageUrl);
|
|
1622
|
+
if (storage.protocol !== "https:" && storage.hostname !== "localhost" && storage.hostname !== "127.0.0.1") throw new Error(`Refusing snapshot upload to ${storage.origin}`);
|
|
1623
|
+
const response = await fetch(storage, {
|
|
1624
|
+
method: "PUT",
|
|
1625
|
+
body: new Uint8Array(bytes),
|
|
1626
|
+
redirect: "error",
|
|
1627
|
+
signal
|
|
1628
|
+
});
|
|
1629
|
+
if (!response.ok) throw new Error(`Snapshot upload failed with status ${response.status}`);
|
|
1630
|
+
}
|
|
1821
1631
|
//#endregion
|
|
1822
|
-
//#region src/generations.ts
|
|
1632
|
+
//#region src/operations/generations.ts
|
|
1823
1633
|
const READ_CONCURRENCY = 50;
|
|
1824
1634
|
const MB$1 = 1048576;
|
|
1825
1635
|
const INDEX_KEY = "studio/generations.json";
|
|
@@ -1937,12 +1747,7 @@ function createGenerationStore({ storage, maxCount, maxMb, ttlMs, now = Date.now
|
|
|
1937
1747
|
};
|
|
1938
1748
|
}
|
|
1939
1749
|
//#endregion
|
|
1940
|
-
//#region src/
|
|
1941
|
-
/**
|
|
1942
|
-
* How long the initial handshake may take before the socket is closed and the reconnect loop
|
|
1943
|
-
* takes over.
|
|
1944
|
-
*/
|
|
1945
|
-
const CONNECT_TIMEOUT_MS = 5e3;
|
|
1750
|
+
//#region src/operations/generationEvents.ts
|
|
1946
1751
|
const require$1 = (0, node_module.createRequire)(require("url").pathToFileURL(__filename).href);
|
|
1947
1752
|
function relativeStoragePath(root, filePath) {
|
|
1948
1753
|
return ((0, node_path.isAbsolute)(filePath) ? (0, node_path.relative)((0, node_path.resolve)(root), filePath) : filePath).replaceAll("\\", "/");
|
|
@@ -1972,27 +1777,24 @@ async function resolvePeerDependencies(names) {
|
|
|
1972
1777
|
missingDependencies
|
|
1973
1778
|
};
|
|
1974
1779
|
}
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
function createWebsocket(url, options) {
|
|
1979
|
-
const ws$1 = new ws.default(url, options);
|
|
1980
|
-
const timer = setTimeout(() => {
|
|
1981
|
-
if (ws$1.readyState === ws.default.CONNECTING) ws$1.close(3008, "Connection timeout");
|
|
1982
|
-
}, CONNECT_TIMEOUT_MS);
|
|
1983
|
-
ws$1.once("open", () => clearTimeout(timer));
|
|
1984
|
-
ws$1.once("close", () => clearTimeout(timer));
|
|
1985
|
-
return ws$1;
|
|
1986
|
-
}
|
|
1780
|
+
const MAX_QUEUED_EVENTS = 1024;
|
|
1781
|
+
const RESERVED_EVENTS = 64;
|
|
1782
|
+
const isDiscardable = (event) => event.type === "kubb:files:processing:update" || event.type === "kubb:info" || event.type === "kubb:success";
|
|
1987
1783
|
/** Forwards selected Kubb lifecycle events to a native Cap'n Web stream. */
|
|
1988
1784
|
function createGenerationStream(hooks, jobId, options = {}) {
|
|
1989
1785
|
const unhooks = [];
|
|
1990
1786
|
let root = "";
|
|
1991
|
-
|
|
1992
|
-
const writer = transform.writable.getWriter();
|
|
1993
|
-
let writes = Promise.resolve();
|
|
1787
|
+
let controller;
|
|
1994
1788
|
let closed = false;
|
|
1995
|
-
|
|
1789
|
+
const stream = new ReadableStream({
|
|
1790
|
+
start: (value) => {
|
|
1791
|
+
controller = value;
|
|
1792
|
+
},
|
|
1793
|
+
cancel: () => {
|
|
1794
|
+
closed = true;
|
|
1795
|
+
detach();
|
|
1796
|
+
}
|
|
1797
|
+
}, { highWaterMark: MAX_QUEUED_EVENTS });
|
|
1996
1798
|
/**
|
|
1997
1799
|
* Registers a listener and keeps its remover, so one generation's listeners come off the session
|
|
1998
1800
|
* emitter again when that generation ends.
|
|
@@ -2001,6 +1803,7 @@ function createGenerationStream(hooks, jobId, options = {}) {
|
|
|
2001
1803
|
unhooks.push(hooks.hook(name, handler));
|
|
2002
1804
|
}
|
|
2003
1805
|
function emitEvent(type, data) {
|
|
1806
|
+
if (closed) return;
|
|
2004
1807
|
const event = {
|
|
2005
1808
|
jobId,
|
|
2006
1809
|
type,
|
|
@@ -2008,9 +1811,12 @@ function createGenerationStream(hooks, jobId, options = {}) {
|
|
|
2008
1811
|
version: 1,
|
|
2009
1812
|
timestamp: Date.now()
|
|
2010
1813
|
};
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
1814
|
+
if (controller.desiredSize <= RESERVED_EVENTS && isDiscardable(event)) return;
|
|
1815
|
+
if (controller.desiredSize <= 0) {
|
|
1816
|
+
fail(/* @__PURE__ */ new Error(`Generation event stream exceeded ${MAX_QUEUED_EVENTS} queued events`));
|
|
1817
|
+
return;
|
|
1818
|
+
}
|
|
1819
|
+
controller.enqueue(event);
|
|
2014
1820
|
}
|
|
2015
1821
|
on("kubb:plugin:start", (ctx) => {
|
|
2016
1822
|
emitEvent("kubb:plugin:start", [{ plugin: { name: ctx.plugin.name } }]);
|
|
@@ -2156,25 +1962,38 @@ function createGenerationStream(hooks, jobId, options = {}) {
|
|
|
2156
1962
|
if (closed) return;
|
|
2157
1963
|
closed = true;
|
|
2158
1964
|
detach();
|
|
2159
|
-
|
|
2160
|
-
if (streamError) return;
|
|
2161
|
-
await writer.close().catch(() => void 0);
|
|
1965
|
+
controller.close();
|
|
2162
1966
|
}
|
|
2163
1967
|
function fail(error) {
|
|
2164
1968
|
detach();
|
|
2165
1969
|
if (closed) return;
|
|
2166
1970
|
closed = true;
|
|
2167
|
-
|
|
1971
|
+
controller.error(error);
|
|
2168
1972
|
}
|
|
2169
1973
|
return {
|
|
2170
|
-
stream
|
|
1974
|
+
stream,
|
|
2171
1975
|
close,
|
|
2172
1976
|
dispose: () => fail(),
|
|
2173
1977
|
fail
|
|
2174
1978
|
};
|
|
2175
1979
|
}
|
|
2176
1980
|
//#endregion
|
|
2177
|
-
//#region src/
|
|
1981
|
+
//#region src/operations/websocket.ts
|
|
1982
|
+
const CONNECT_TIMEOUT_MS = 5e3;
|
|
1983
|
+
/**
|
|
1984
|
+
* Opens a Studio WebSocket connection and closes it when the initial handshake exceeds the configured timeout.
|
|
1985
|
+
*/
|
|
1986
|
+
function createWebsocket(url, options) {
|
|
1987
|
+
const ws$1 = new ws.default(url, options);
|
|
1988
|
+
const timer = setTimeout(() => {
|
|
1989
|
+
if (ws$1.readyState === ws.default.CONNECTING) ws$1.close(3008, "Connection timeout");
|
|
1990
|
+
}, CONNECT_TIMEOUT_MS);
|
|
1991
|
+
ws$1.once("open", () => clearTimeout(timer));
|
|
1992
|
+
ws$1.once("close", () => clearTimeout(timer));
|
|
1993
|
+
return ws$1;
|
|
1994
|
+
}
|
|
1995
|
+
//#endregion
|
|
1996
|
+
//#region src/operations/rpc.ts
|
|
2178
1997
|
/**
|
|
2179
1998
|
* The only methods Studio may call on an agent. A `StudioSession` carries far more than
|
|
2180
1999
|
* {@link AgentApi}, so it is wrapped rather than exposed: what Cap'n Web can reach is exactly what
|
|
@@ -2235,16 +2054,253 @@ const connectWebSocketRpc = async ({ url, token, instanceId, local }) => {
|
|
|
2235
2054
|
};
|
|
2236
2055
|
};
|
|
2237
2056
|
//#endregion
|
|
2238
|
-
//#region src/
|
|
2057
|
+
//#region src/operations/hooks.ts
|
|
2239
2058
|
/**
|
|
2240
|
-
*
|
|
2059
|
+
* Register a `kubb:hook:start` listener that spawns the requested command via tinyexec,
|
|
2060
|
+
* streams each stdout line as a `kubb:hook:line` event, and calls `kubb:hook:end` with the result.
|
|
2061
|
+
* Streaming the output lets Kubb Studio render live hook progress over the WebSocket connection.
|
|
2062
|
+
*
|
|
2063
|
+
* Returns a remover, so a session that runs one generation after another on the same emitter does
|
|
2064
|
+
* not stack a listener per run.
|
|
2241
2065
|
*/
|
|
2066
|
+
function setupHookListener(hooks, root, signal) {
|
|
2067
|
+
return hooks.hook("kubb:hook:start", async (ctx) => {
|
|
2068
|
+
const { id, command, args } = ctx;
|
|
2069
|
+
if (!id) return;
|
|
2070
|
+
const commandWithArgs = args?.length ? `${command} ${args.join(" ")}` : command;
|
|
2071
|
+
try {
|
|
2072
|
+
const proc = (0, tinyexec.x)(command, [...args ?? []], {
|
|
2073
|
+
signal,
|
|
2074
|
+
nodeOptions: {
|
|
2075
|
+
cwd: root,
|
|
2076
|
+
detached: true
|
|
2077
|
+
}
|
|
2078
|
+
});
|
|
2079
|
+
for await (const line of proc) await hooks.callHook("kubb:hook:line", {
|
|
2080
|
+
id,
|
|
2081
|
+
line
|
|
2082
|
+
});
|
|
2083
|
+
const { exitCode } = await proc;
|
|
2084
|
+
if (exitCode !== 0) {
|
|
2085
|
+
const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
|
|
2086
|
+
await hooks.callHook("kubb:hook:end", {
|
|
2087
|
+
id,
|
|
2088
|
+
command,
|
|
2089
|
+
args,
|
|
2090
|
+
success: false,
|
|
2091
|
+
error
|
|
2092
|
+
});
|
|
2093
|
+
await hooks.callHook("kubb:error", { error });
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
await hooks.callHook("kubb:hook:end", {
|
|
2097
|
+
id,
|
|
2098
|
+
command,
|
|
2099
|
+
args,
|
|
2100
|
+
success: true,
|
|
2101
|
+
error: null
|
|
2102
|
+
});
|
|
2103
|
+
} catch (caughtError) {
|
|
2104
|
+
const error = /* @__PURE__ */ new Error(`Hook execute failed: ${commandWithArgs}`);
|
|
2105
|
+
error.cause = caughtError;
|
|
2106
|
+
await hooks.callHook("kubb:hook:end", {
|
|
2107
|
+
id,
|
|
2108
|
+
command,
|
|
2109
|
+
args,
|
|
2110
|
+
success: false,
|
|
2111
|
+
error
|
|
2112
|
+
});
|
|
2113
|
+
await hooks.callHook("kubb:error", { error });
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
}
|
|
2117
|
+
/**
|
|
2118
|
+
* Waits for the `kubb:hook:end` matching `hookId`. Register this before calling `kubb:hook:start`:
|
|
2119
|
+
* `callHook` awaits its listeners, and {@link setupHookListener} calls `kubb:hook:end` from inside
|
|
2120
|
+
* that same listener, so a handler added afterward would already have missed it.
|
|
2121
|
+
*/
|
|
2122
|
+
function waitForHookEnd(hooks, hookId) {
|
|
2123
|
+
return new Promise((resolve, reject) => {
|
|
2124
|
+
const handleHookEnd = (ctx) => {
|
|
2125
|
+
if (ctx.id !== hookId) return;
|
|
2126
|
+
hooks.removeHook("kubb:hook:end", handleHookEnd);
|
|
2127
|
+
if (ctx.success) {
|
|
2128
|
+
resolve();
|
|
2129
|
+
return;
|
|
2130
|
+
}
|
|
2131
|
+
reject(ctx.error);
|
|
2132
|
+
};
|
|
2133
|
+
hooks.hook("kubb:hook:end", handleHookEnd);
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
//#endregion
|
|
2137
|
+
//#region src/operations/generate.ts
|
|
2242
2138
|
const DISK_SNAPSHOT_MAX_FILES = 1e4;
|
|
2243
2139
|
/**
|
|
2244
|
-
*
|
|
2245
|
-
*
|
|
2246
|
-
*
|
|
2140
|
+
* `isToolAvailable` spawns a process, and a long-lived connection generates repeatedly, so each
|
|
2141
|
+
* executable is probed once per process. The CLI deliberately does not memoize: a `--watch` build
|
|
2142
|
+
* should keep noticing a tool installed mid-session.
|
|
2143
|
+
*/
|
|
2144
|
+
const detectTool = memoize(/* @__PURE__ */ new Map(), detectTool$1);
|
|
2145
|
+
/**
|
|
2146
|
+
* The two post-build tool steps. Formatting and linting differ only in which tools they look for,
|
|
2147
|
+
* so they run through one loop rather than two near-identical blocks.
|
|
2148
|
+
*
|
|
2149
|
+
* `noun` and `verbing` are spelled out instead of built from `kind`. Concatenating `` `${kind}ter` ``
|
|
2150
|
+
* and `` `${kind}ting` `` works for `format`, but doubles the `t` in `lint`, giving "lintter" and
|
|
2151
|
+
* "lintting" instead of "linter" and "linting".
|
|
2152
|
+
*/
|
|
2153
|
+
const TOOL_STEPS = [{
|
|
2154
|
+
kind: "format",
|
|
2155
|
+
noun: "formatter",
|
|
2156
|
+
verbing: "Formatting",
|
|
2157
|
+
tools: formatters,
|
|
2158
|
+
detect: FORMATTER_PREFERENCE
|
|
2159
|
+
}, {
|
|
2160
|
+
kind: "lint",
|
|
2161
|
+
noun: "linter",
|
|
2162
|
+
verbing: "Linting",
|
|
2163
|
+
tools: linters,
|
|
2164
|
+
detect: LINTER_PREFERENCE
|
|
2165
|
+
}];
|
|
2166
|
+
/**
|
|
2167
|
+
* Absolute path of the directory the formatter and linter are pointed at.
|
|
2168
|
+
*/
|
|
2169
|
+
function outputPath(config) {
|
|
2170
|
+
return node_path.default.isAbsolute(config.output.path) ? config.output.path : node_path.default.resolve(node_process.default.cwd(), config.root, config.output.path);
|
|
2171
|
+
}
|
|
2172
|
+
/**
|
|
2173
|
+
* Emits `kubb:hook:start` and waits for the matching `kubb:hook:end`. The host spawns the process:
|
|
2174
|
+
* this only describes what to run and when it finished.
|
|
2175
|
+
*
|
|
2176
|
+
* @throws whatever the command failed with, so callers can report it their own way.
|
|
2247
2177
|
*/
|
|
2178
|
+
async function runHook({ hooks, id, command, args }) {
|
|
2179
|
+
const hookId = (0, node_crypto.hash)("sha256", id);
|
|
2180
|
+
const hookEnd = waitForHookEnd(hooks, hookId);
|
|
2181
|
+
await hooks.callHook("kubb:hook:start", {
|
|
2182
|
+
id: hookId,
|
|
2183
|
+
command,
|
|
2184
|
+
args: [...args]
|
|
2185
|
+
});
|
|
2186
|
+
await hookEnd;
|
|
2187
|
+
}
|
|
2188
|
+
function isProblemErrorDiagnostic(diagnostic) {
|
|
2189
|
+
return (diagnostic.kind ?? "problem") === "problem" && diagnostic.severity === "error";
|
|
2190
|
+
}
|
|
2191
|
+
/**
|
|
2192
|
+
* Folds error-severity diagnostics into one thrown error so logs name the failing plugin.
|
|
2193
|
+
*/
|
|
2194
|
+
function formatGenerationFailure(diagnostics) {
|
|
2195
|
+
const reasons = diagnostics.filter(isProblemErrorDiagnostic).map((diagnostic) => diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message);
|
|
2196
|
+
if (!reasons.length) return /* @__PURE__ */ new Error("Generation failed");
|
|
2197
|
+
return /* @__PURE__ */ new Error(`Generation failed: ${reasons.length} error${reasons.length === 1 ? "" : "s"}: ${reasons.join("; ")}`);
|
|
2198
|
+
}
|
|
2199
|
+
/**
|
|
2200
|
+
* Runs a full Kubb code-generation cycle for the given config.
|
|
2201
|
+
*
|
|
2202
|
+
* Emits lifecycle events on the provided `hooks` emitter so callers (e.g. the WebSocket stream)
|
|
2203
|
+
* can forward progress to connected clients. After a successful build, auto-formatting and
|
|
2204
|
+
* linting are applied when configured, followed by any user-defined `hooks.done` commands.
|
|
2205
|
+
*/
|
|
2206
|
+
async function generate({ config, hooks, signal }) {
|
|
2207
|
+
signal?.throwIfAborted();
|
|
2208
|
+
const hrStart = node_process.default.hrtime();
|
|
2209
|
+
await hooks.callHook("kubb:generation:start", { config });
|
|
2210
|
+
await hooks.callHook("kubb:info", { message: config.name ? `Setup generation ${config.name}` : "Setup generation" });
|
|
2211
|
+
const kubb = (0, _kubb_core.createKubb)(config, {
|
|
2212
|
+
hooks,
|
|
2213
|
+
signal
|
|
2214
|
+
});
|
|
2215
|
+
await kubb.setup();
|
|
2216
|
+
await hooks.callHook("kubb:info", { message: config.name ? `Build generation ${config.name}` : "Build generation" });
|
|
2217
|
+
const { files, diagnostics, storage } = await kubb.safeBuild();
|
|
2218
|
+
signal?.throwIfAborted();
|
|
2219
|
+
await hooks.callHook("kubb:info", { message: "Load summary" });
|
|
2220
|
+
for (const diagnostic of diagnostics.filter(isProblemErrorDiagnostic)) await hooks.callHook("kubb:error", { error: new Error(diagnostic.plugin ? `${diagnostic.plugin}: ${diagnostic.message}` : diagnostic.message) });
|
|
2221
|
+
const status = _kubb_core.Diagnostics.hasError(diagnostics) ? "failed" : "success";
|
|
2222
|
+
await hooks.callHook("kubb:generation:end", {
|
|
2223
|
+
config,
|
|
2224
|
+
storage: {
|
|
2225
|
+
...storage,
|
|
2226
|
+
readKeys: async () => [...new Set(files.map((file) => file.path))]
|
|
2227
|
+
},
|
|
2228
|
+
diagnostics,
|
|
2229
|
+
status,
|
|
2230
|
+
hrStart,
|
|
2231
|
+
filesCreated: files.length
|
|
2232
|
+
});
|
|
2233
|
+
if (status === "failed") throw formatGenerationFailure(diagnostics);
|
|
2234
|
+
await hooks.callHook("kubb:success", { message: "Generation successfully" });
|
|
2235
|
+
for (const step of TOOL_STEPS) {
|
|
2236
|
+
const setting = config.output[step.kind];
|
|
2237
|
+
if (!setting) continue;
|
|
2238
|
+
await hooks.callHook(`kubb:${step.kind}:start`);
|
|
2239
|
+
const tool = setting === "auto" ? await detectTool(step.detect) : setting;
|
|
2240
|
+
if (!tool) await hooks.callHook("kubb:warn", { message: `No ${step.noun} found (${step.detect.join(", ")}). Skipping ${step.verbing.toLowerCase()}.` });
|
|
2241
|
+
if (tool && setting === "auto") await hooks.callHook("kubb:info", { message: `Auto-detected ${step.noun}: ${(0, node_util.styleText)("dim", tool)}` });
|
|
2242
|
+
const command = tool ? step.tools[tool] : void 0;
|
|
2243
|
+
if (command) try {
|
|
2244
|
+
await runHook({
|
|
2245
|
+
hooks,
|
|
2246
|
+
id: [config.name, tool].filter(Boolean).join("-"),
|
|
2247
|
+
command: command.command,
|
|
2248
|
+
args: command.args(outputPath(config))
|
|
2249
|
+
});
|
|
2250
|
+
await hooks.callHook("kubb:success", { message: `${step.verbing} with ${tool} successfully` });
|
|
2251
|
+
} catch (caughtError) {
|
|
2252
|
+
await hooks.callHook("kubb:error", { error: new Error(command.errorMessage, { cause: caughtError }) });
|
|
2253
|
+
signal?.throwIfAborted();
|
|
2254
|
+
}
|
|
2255
|
+
await hooks.callHook(`kubb:${step.kind}:end`);
|
|
2256
|
+
}
|
|
2257
|
+
if (config.output.postGenerate?.length) {
|
|
2258
|
+
await hooks.callHook("kubb:hooks:start");
|
|
2259
|
+
for (const entry of config.output.postGenerate) {
|
|
2260
|
+
const line = typeof entry === "string" ? entry : entry.command;
|
|
2261
|
+
const [cmd, ...args] = tokenize(line);
|
|
2262
|
+
if (!cmd) continue;
|
|
2263
|
+
await runHook({
|
|
2264
|
+
hooks,
|
|
2265
|
+
id: line,
|
|
2266
|
+
command: cmd,
|
|
2267
|
+
args
|
|
2268
|
+
});
|
|
2269
|
+
await hooks.callHook("kubb:success", { message: `${line} successfully executed` });
|
|
2270
|
+
}
|
|
2271
|
+
await hooks.callHook("kubb:hooks:end");
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
async function runGenerationOperation({ config, hooks, signal, jobId, store, snapshotRoot, maxSnapshotMb }) {
|
|
2275
|
+
const diskFiles = snapshotRoot ? await listDisk({
|
|
2276
|
+
root: snapshotRoot,
|
|
2277
|
+
outputPath: config.output.path,
|
|
2278
|
+
maxFiles: DISK_SNAPSHOT_MAX_FILES
|
|
2279
|
+
}) : void 0;
|
|
2280
|
+
const disk = diskFiles ? await store.keep({
|
|
2281
|
+
jobId,
|
|
2282
|
+
source: "disk",
|
|
2283
|
+
files: diskFiles,
|
|
2284
|
+
maxSetMb: maxSnapshotMb
|
|
2285
|
+
}) : void 0;
|
|
2286
|
+
const removeHookListener = setupHookListener(hooks, config.root, signal);
|
|
2287
|
+
try {
|
|
2288
|
+
await generate({
|
|
2289
|
+
config,
|
|
2290
|
+
hooks,
|
|
2291
|
+
signal
|
|
2292
|
+
});
|
|
2293
|
+
} catch (error) {
|
|
2294
|
+
await store.drop(jobId);
|
|
2295
|
+
throw error;
|
|
2296
|
+
} finally {
|
|
2297
|
+
removeHookListener();
|
|
2298
|
+
}
|
|
2299
|
+
return disk;
|
|
2300
|
+
}
|
|
2301
|
+
//#endregion
|
|
2302
|
+
//#region src/runtime/StudioSession.ts
|
|
2303
|
+
/** A sandbox shares one in-memory generation store across tenants, so old generations expire. */
|
|
2248
2304
|
const SANDBOX_GENERATION_TTL_MS = 9e5;
|
|
2249
2305
|
/**
|
|
2250
2306
|
* A fresh root for one sandbox job. Kubb keys its output manifest cache by root, so tenants that
|
|
@@ -2554,20 +2610,12 @@ var StudioSession = class {
|
|
|
2554
2610
|
clearTimeout(timer);
|
|
2555
2611
|
}
|
|
2556
2612
|
}
|
|
2557
|
-
/**
|
|
2558
|
-
* Whether memory still leaves room for another job. Always, when the host set no budget.
|
|
2559
|
-
*/
|
|
2560
|
-
#isAccepting(memoryMb = rssMb()) {
|
|
2561
|
-
const budget = this.#options.capacity.memoryBudgetMb;
|
|
2562
|
-
return budget === void 0 || memoryMb <= budget * 1.5;
|
|
2563
|
-
}
|
|
2564
2613
|
async #load() {
|
|
2565
|
-
const memoryMb = rssMb();
|
|
2566
2614
|
return {
|
|
2567
2615
|
running: this.#isGenerating ? 1 : 0,
|
|
2568
|
-
rssMb: Math.round(
|
|
2616
|
+
rssMb: Math.round(rssMb()),
|
|
2569
2617
|
storeBytes: await this.#generations.bytes(),
|
|
2570
|
-
accepting:
|
|
2618
|
+
accepting: true
|
|
2571
2619
|
};
|
|
2572
2620
|
}
|
|
2573
2621
|
/**
|
|
@@ -2676,7 +2724,6 @@ var StudioSession = class {
|
|
|
2676
2724
|
}
|
|
2677
2725
|
async #runGeneration(data, controller, cancelRun) {
|
|
2678
2726
|
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");
|
|
2679
|
-
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");
|
|
2680
2727
|
this.#isGenerating = true;
|
|
2681
2728
|
this.#activeJob = {
|
|
2682
2729
|
jobId: data.jobId,
|
|
@@ -2697,43 +2744,28 @@ var StudioSession = class {
|
|
|
2697
2744
|
if (patch?.input && !this.#canUseInput) await this.#warn("Ignored the spec from Studio: generating from a Studio spec was not granted", "allowInput");
|
|
2698
2745
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2699
2746
|
this.#lastGeneration = void 0;
|
|
2700
|
-
const
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
const detach = [setupHookListener(this.#hooks, root, controller.signal)];
|
|
2712
|
-
try {
|
|
2713
|
-
await generate({
|
|
2714
|
-
config: {
|
|
2715
|
-
...config,
|
|
2716
|
-
root,
|
|
2717
|
-
input: inputOverride ?? config.input,
|
|
2718
|
-
storage: this.#canWrite ? (0, _kubb_core.fsStorage)() : (0, _kubb_core.memoryStorage)(),
|
|
2719
|
-
output: permissions.allowExec ? { ...config.output } : {
|
|
2720
|
-
...config.output,
|
|
2721
|
-
format: false,
|
|
2722
|
-
lint: false,
|
|
2723
|
-
postGenerate: []
|
|
2724
|
-
},
|
|
2725
|
-
plugins: resolvedPlugins,
|
|
2726
|
-
adapter
|
|
2747
|
+
const disk = await runGenerationOperation({
|
|
2748
|
+
config: {
|
|
2749
|
+
...config,
|
|
2750
|
+
root,
|
|
2751
|
+
input: inputOverride ?? config.input,
|
|
2752
|
+
storage: this.#canWrite ? (0, _kubb_core.fsStorage)() : (0, _kubb_core.memoryStorage)(),
|
|
2753
|
+
output: permissions.allowExec ? { ...config.output } : {
|
|
2754
|
+
...config.output,
|
|
2755
|
+
format: false,
|
|
2756
|
+
lint: false,
|
|
2757
|
+
postGenerate: []
|
|
2727
2758
|
},
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2759
|
+
plugins: resolvedPlugins,
|
|
2760
|
+
adapter
|
|
2761
|
+
},
|
|
2762
|
+
hooks: this.#hooks,
|
|
2763
|
+
signal: controller.signal,
|
|
2764
|
+
jobId: data.jobId,
|
|
2765
|
+
store: this.#generations,
|
|
2766
|
+
snapshotRoot: this.#hasProjectOnDisk ? root : void 0,
|
|
2767
|
+
maxSnapshotMb: this.#limits.maxSnapshotMb
|
|
2768
|
+
});
|
|
2737
2769
|
await this.#hooks.callHook("studio:command:end", {
|
|
2738
2770
|
command,
|
|
2739
2771
|
info: `${resolvedPlugins.length} plugin${resolvedPlugins.length === 1 ? "" : "s"}, ${this.#canWrite ? "written to disk" : "in memory"}${inputOverride !== void 0 ? ", from a Studio spec" : ""}`
|
|
@@ -2795,8 +2827,10 @@ var StudioSession = class {
|
|
|
2795
2827
|
}
|
|
2796
2828
|
if (this.#isGenerating) return refuse("a generation is in progress");
|
|
2797
2829
|
try {
|
|
2798
|
-
const { source: patched, outcomes, changed } =
|
|
2799
|
-
|
|
2830
|
+
const { source: patched, outcomes, changed } = await writeConfigEdits({
|
|
2831
|
+
filePath: configFile,
|
|
2832
|
+
edits
|
|
2833
|
+
});
|
|
2800
2834
|
const applied = outcomes.filter((outcome) => outcome.applied).length;
|
|
2801
2835
|
await this.#hooks.callHook("studio:command:end", {
|
|
2802
2836
|
command,
|
|
@@ -2834,24 +2868,14 @@ var StudioSession = class {
|
|
|
2834
2868
|
version,
|
|
2835
2869
|
peerDependencies: generation.peerDependencies
|
|
2836
2870
|
});
|
|
2837
|
-
const { token, studioUrl } = this.#options;
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
});
|
|
2845
|
-
const storageUrl = redirect.headers.get("location");
|
|
2846
|
-
if (redirect.status !== 307 || !storageUrl) throw new Error(`Studio did not provide a storage URL (status ${redirect.status})`);
|
|
2847
|
-
const storage = new URL(storageUrl);
|
|
2848
|
-
if (storage.protocol !== "https:" && storage.hostname !== "localhost" && storage.hostname !== "127.0.0.1") throw new Error(`Refusing snapshot upload to ${storage.origin}`);
|
|
2849
|
-
const response = await fetch(storage, {
|
|
2850
|
-
method: "PUT",
|
|
2851
|
-
body: new Uint8Array(bytes),
|
|
2852
|
-
redirect: "error"
|
|
2871
|
+
const { token, studioUrl, signal } = this.#options;
|
|
2872
|
+
await uploadSnapshot({
|
|
2873
|
+
bytes,
|
|
2874
|
+
uploadPath,
|
|
2875
|
+
studioUrl,
|
|
2876
|
+
token,
|
|
2877
|
+
shutdown: signal
|
|
2853
2878
|
});
|
|
2854
|
-
if (!response.ok) throw new Error(`Snapshot upload failed with status ${response.status}`);
|
|
2855
2879
|
await this.#hooks.callHook("studio:command:end", {
|
|
2856
2880
|
command,
|
|
2857
2881
|
info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
|
|
@@ -2865,10 +2889,7 @@ var StudioSession = class {
|
|
|
2865
2889
|
throw error;
|
|
2866
2890
|
}
|
|
2867
2891
|
}
|
|
2868
|
-
/**
|
|
2869
|
-
* An agent with a project on disk can show a run against what its output directory held before.
|
|
2870
|
-
* A sandbox agent has no project.
|
|
2871
|
-
*/
|
|
2892
|
+
/** A sandbox has no project directory to snapshot. */
|
|
2872
2893
|
get #hasProjectOnDisk() {
|
|
2873
2894
|
return !this.#isSandbox && this.#canRead;
|
|
2874
2895
|
}
|
|
@@ -2897,7 +2918,7 @@ var StudioSession = class {
|
|
|
2897
2918
|
}
|
|
2898
2919
|
};
|
|
2899
2920
|
//#endregion
|
|
2900
|
-
//#region src/client.ts
|
|
2921
|
+
//#region src/runtime/client.ts
|
|
2901
2922
|
/**
|
|
2902
2923
|
* Creates the Kubb Studio client: the connection, the command loop, and the generation event
|
|
2903
2924
|
* stream shared by the `kubb studio` CLI command and the Docker agent.
|
|
@@ -2913,7 +2934,7 @@ var StudioSession = class {
|
|
|
2913
2934
|
*/
|
|
2914
2935
|
function createClient({ onAuthRequired, ...options }) {
|
|
2915
2936
|
const controller = new AbortController();
|
|
2916
|
-
const instanceId = (0, node_crypto.randomUUID)();
|
|
2937
|
+
const instanceId = options.instanceId ?? (0, node_crypto.randomUUID)();
|
|
2917
2938
|
function notifyAuthRequired(error) {
|
|
2918
2939
|
if (controller.signal.aborted) return;
|
|
2919
2940
|
controller.abort();
|
|
@@ -2934,7 +2955,7 @@ function createClient({ onAuthRequired, ...options }) {
|
|
|
2934
2955
|
};
|
|
2935
2956
|
}
|
|
2936
2957
|
//#endregion
|
|
2937
|
-
//#region src/runConnection.ts
|
|
2958
|
+
//#region src/runtime/runConnection.ts
|
|
2938
2959
|
/**
|
|
2939
2960
|
* Waits for whichever comes first: the shutdown signal, or Studio rejecting the token during a
|
|
2940
2961
|
* background reconnect. Resolves with the rejection, or nothing when the run is being shut down.
|
|
@@ -3007,7 +3028,7 @@ async function runConnection({ credentials, clientOptions, onTokenRejected, sign
|
|
|
3007
3028
|
}
|
|
3008
3029
|
}
|
|
3009
3030
|
//#endregion
|
|
3010
|
-
//#region src/pair.ts
|
|
3031
|
+
//#region src/operations/pair.ts
|
|
3011
3032
|
/** Labels, not secrets: a person approving the code in the browser is what authorizes a pairing. */
|
|
3012
3033
|
const CLIENT_IDS = {
|
|
3013
3034
|
cli: "kubb-cli",
|