@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.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.
|
|
379
|
-
//#endregion
|
|
380
|
-
//#region src/hooks.ts
|
|
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
|
-
}
|
|
378
|
+
var version = "5.3.19";
|
|
459
379
|
//#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.
|
|
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.
|
|
2247
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
|
|
@@ -2306,7 +2362,6 @@ function applyStudioDefaults(options) {
|
|
|
2306
2362
|
permissions: {
|
|
2307
2363
|
allowWrite: false,
|
|
2308
2364
|
allowConfigEdit: false,
|
|
2309
|
-
allowInput: false,
|
|
2310
2365
|
allowExec: false,
|
|
2311
2366
|
allowRead: false,
|
|
2312
2367
|
...options.permissions
|
|
@@ -2442,13 +2497,6 @@ var StudioSession = class {
|
|
|
2442
2497
|
return !this.#isSandbox && this.#options.permissions.allowConfigEdit;
|
|
2443
2498
|
}
|
|
2444
2499
|
/**
|
|
2445
|
-
* A sandbox agent always generates from the spec Studio supplies. A local agent only when the
|
|
2446
|
-
* host opted in.
|
|
2447
|
-
*/
|
|
2448
|
-
get #canUseInput() {
|
|
2449
|
-
return this.#isSandbox || this.#options.permissions.allowInput;
|
|
2450
|
-
}
|
|
2451
|
-
/**
|
|
2452
2500
|
* A sandbox agent always allows reading its output back; a local agent only when opted in.
|
|
2453
2501
|
*/
|
|
2454
2502
|
get #canRead() {
|
|
@@ -2554,20 +2602,12 @@ var StudioSession = class {
|
|
|
2554
2602
|
clearTimeout(timer);
|
|
2555
2603
|
}
|
|
2556
2604
|
}
|
|
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
2605
|
async #load() {
|
|
2565
|
-
const memoryMb = rssMb();
|
|
2566
2606
|
return {
|
|
2567
2607
|
running: this.#isGenerating ? 1 : 0,
|
|
2568
|
-
rssMb: Math.round(
|
|
2608
|
+
rssMb: Math.round(rssMb()),
|
|
2569
2609
|
storeBytes: await this.#generations.bytes(),
|
|
2570
|
-
accepting:
|
|
2610
|
+
accepting: true
|
|
2571
2611
|
};
|
|
2572
2612
|
}
|
|
2573
2613
|
/**
|
|
@@ -2605,7 +2645,6 @@ var StudioSession = class {
|
|
|
2605
2645
|
permissions: {
|
|
2606
2646
|
...permissions,
|
|
2607
2647
|
allowWrite: this.#canWrite,
|
|
2608
|
-
allowInput: this.#canUseInput,
|
|
2609
2648
|
allowConfigEdit: this.#canEditConfig,
|
|
2610
2649
|
allowRead: this.#canRead
|
|
2611
2650
|
}
|
|
@@ -2676,7 +2715,6 @@ var StudioSession = class {
|
|
|
2676
2715
|
}
|
|
2677
2716
|
async #runGeneration(data, controller, cancelRun) {
|
|
2678
2717
|
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
2718
|
this.#isGenerating = true;
|
|
2681
2719
|
this.#activeJob = {
|
|
2682
2720
|
jobId: data.jobId,
|
|
@@ -2692,48 +2730,33 @@ var StudioSession = class {
|
|
|
2692
2730
|
const patch = data.config;
|
|
2693
2731
|
const plugins = await mergePlugins(config.plugins, patch?.plugins);
|
|
2694
2732
|
const adapter = await mergeAdapter(config.adapter, patch?.adapter);
|
|
2695
|
-
const inputOverride = this.#isSandbox ? patch?.input ?? "" :
|
|
2733
|
+
const inputOverride = this.#isSandbox ? patch?.input ?? "" : void 0;
|
|
2696
2734
|
if (permissions.allowWrite && this.#isSandbox) await this.#warn("Running in a sandbox, so writing files is disabled");
|
|
2697
|
-
if (patch?.input && !this.#
|
|
2735
|
+
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");
|
|
2698
2736
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2699
2737
|
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
|
|
2738
|
+
const disk = await runGenerationOperation({
|
|
2739
|
+
config: {
|
|
2740
|
+
...config,
|
|
2741
|
+
root,
|
|
2742
|
+
input: inputOverride ?? config.input,
|
|
2743
|
+
storage: this.#canWrite ? (0, _kubb_core.fsStorage)() : (0, _kubb_core.memoryStorage)(),
|
|
2744
|
+
output: permissions.allowExec ? { ...config.output } : {
|
|
2745
|
+
...config.output,
|
|
2746
|
+
format: false,
|
|
2747
|
+
lint: false,
|
|
2748
|
+
postGenerate: []
|
|
2727
2749
|
},
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2750
|
+
plugins: resolvedPlugins,
|
|
2751
|
+
adapter
|
|
2752
|
+
},
|
|
2753
|
+
hooks: this.#hooks,
|
|
2754
|
+
signal: controller.signal,
|
|
2755
|
+
jobId: data.jobId,
|
|
2756
|
+
store: this.#generations,
|
|
2757
|
+
snapshotRoot: this.#hasProjectOnDisk ? root : void 0,
|
|
2758
|
+
maxSnapshotMb: this.#limits.maxSnapshotMb
|
|
2759
|
+
});
|
|
2737
2760
|
await this.#hooks.callHook("studio:command:end", {
|
|
2738
2761
|
command,
|
|
2739
2762
|
info: `${resolvedPlugins.length} plugin${resolvedPlugins.length === 1 ? "" : "s"}, ${this.#canWrite ? "written to disk" : "in memory"}${inputOverride !== void 0 ? ", from a Studio spec" : ""}`
|
|
@@ -2795,8 +2818,10 @@ var StudioSession = class {
|
|
|
2795
2818
|
}
|
|
2796
2819
|
if (this.#isGenerating) return refuse("a generation is in progress");
|
|
2797
2820
|
try {
|
|
2798
|
-
const { source: patched, outcomes, changed } =
|
|
2799
|
-
|
|
2821
|
+
const { source: patched, outcomes, changed } = await writeConfigEdits({
|
|
2822
|
+
filePath: configFile,
|
|
2823
|
+
edits
|
|
2824
|
+
});
|
|
2800
2825
|
const applied = outcomes.filter((outcome) => outcome.applied).length;
|
|
2801
2826
|
await this.#hooks.callHook("studio:command:end", {
|
|
2802
2827
|
command,
|
|
@@ -2834,24 +2859,14 @@ var StudioSession = class {
|
|
|
2834
2859
|
version,
|
|
2835
2860
|
peerDependencies: generation.peerDependencies
|
|
2836
2861
|
});
|
|
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"
|
|
2862
|
+
const { token, studioUrl, signal } = this.#options;
|
|
2863
|
+
await uploadSnapshot({
|
|
2864
|
+
bytes,
|
|
2865
|
+
uploadPath,
|
|
2866
|
+
studioUrl,
|
|
2867
|
+
token,
|
|
2868
|
+
shutdown: signal
|
|
2853
2869
|
});
|
|
2854
|
-
if (!response.ok) throw new Error(`Snapshot upload failed with status ${response.status}`);
|
|
2855
2870
|
await this.#hooks.callHook("studio:command:end", {
|
|
2856
2871
|
command,
|
|
2857
2872
|
info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
|
|
@@ -2865,10 +2880,7 @@ var StudioSession = class {
|
|
|
2865
2880
|
throw error;
|
|
2866
2881
|
}
|
|
2867
2882
|
}
|
|
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
|
-
*/
|
|
2883
|
+
/** A sandbox has no project directory to snapshot. */
|
|
2872
2884
|
get #hasProjectOnDisk() {
|
|
2873
2885
|
return !this.#isSandbox && this.#canRead;
|
|
2874
2886
|
}
|
|
@@ -2897,7 +2909,7 @@ var StudioSession = class {
|
|
|
2897
2909
|
}
|
|
2898
2910
|
};
|
|
2899
2911
|
//#endregion
|
|
2900
|
-
//#region src/client.ts
|
|
2912
|
+
//#region src/runtime/client.ts
|
|
2901
2913
|
/**
|
|
2902
2914
|
* Creates the Kubb Studio client: the connection, the command loop, and the generation event
|
|
2903
2915
|
* stream shared by the `kubb studio` CLI command and the Docker agent.
|
|
@@ -2913,7 +2925,7 @@ var StudioSession = class {
|
|
|
2913
2925
|
*/
|
|
2914
2926
|
function createClient({ onAuthRequired, ...options }) {
|
|
2915
2927
|
const controller = new AbortController();
|
|
2916
|
-
const instanceId = (0, node_crypto.randomUUID)();
|
|
2928
|
+
const instanceId = options.instanceId ?? (0, node_crypto.randomUUID)();
|
|
2917
2929
|
function notifyAuthRequired(error) {
|
|
2918
2930
|
if (controller.signal.aborted) return;
|
|
2919
2931
|
controller.abort();
|
|
@@ -2934,7 +2946,7 @@ function createClient({ onAuthRequired, ...options }) {
|
|
|
2934
2946
|
};
|
|
2935
2947
|
}
|
|
2936
2948
|
//#endregion
|
|
2937
|
-
//#region src/runConnection.ts
|
|
2949
|
+
//#region src/runtime/runConnection.ts
|
|
2938
2950
|
/**
|
|
2939
2951
|
* Waits for whichever comes first: the shutdown signal, or Studio rejecting the token during a
|
|
2940
2952
|
* background reconnect. Resolves with the rejection, or nothing when the run is being shut down.
|
|
@@ -3007,7 +3019,7 @@ async function runConnection({ credentials, clientOptions, onTokenRejected, sign
|
|
|
3007
3019
|
}
|
|
3008
3020
|
}
|
|
3009
3021
|
//#endregion
|
|
3010
|
-
//#region src/pair.ts
|
|
3022
|
+
//#region src/operations/pair.ts
|
|
3011
3023
|
/** Labels, not secrets: a person approving the code in the browser is what authorizes a pairing. */
|
|
3012
3024
|
const CLIENT_IDS = {
|
|
3013
3025
|
cli: "kubb-cli",
|