@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.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.
|
|
348
|
+
var version = "5.3.18";
|
|
349
349
|
//#endregion
|
|
350
|
-
//#region src/
|
|
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
|
-
}
|
|
429
|
-
//#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.
|
|
2217
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.
|
|
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
|
|
@@ -2524,20 +2580,12 @@ var StudioSession = class {
|
|
|
2524
2580
|
clearTimeout(timer);
|
|
2525
2581
|
}
|
|
2526
2582
|
}
|
|
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
2583
|
async #load() {
|
|
2535
|
-
const memoryMb = rssMb();
|
|
2536
2584
|
return {
|
|
2537
2585
|
running: this.#isGenerating ? 1 : 0,
|
|
2538
|
-
rssMb: Math.round(
|
|
2586
|
+
rssMb: Math.round(rssMb()),
|
|
2539
2587
|
storeBytes: await this.#generations.bytes(),
|
|
2540
|
-
accepting:
|
|
2588
|
+
accepting: true
|
|
2541
2589
|
};
|
|
2542
2590
|
}
|
|
2543
2591
|
/**
|
|
@@ -2646,7 +2694,6 @@ var StudioSession = class {
|
|
|
2646
2694
|
}
|
|
2647
2695
|
async #runGeneration(data, controller, cancelRun) {
|
|
2648
2696
|
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
2697
|
this.#isGenerating = true;
|
|
2651
2698
|
this.#activeJob = {
|
|
2652
2699
|
jobId: data.jobId,
|
|
@@ -2667,43 +2714,28 @@ var StudioSession = class {
|
|
|
2667
2714
|
if (patch?.input && !this.#canUseInput) await this.#warn("Ignored the spec from Studio: generating from a Studio spec was not granted", "allowInput");
|
|
2668
2715
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2669
2716
|
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
|
|
2717
|
+
const disk = await runGenerationOperation({
|
|
2718
|
+
config: {
|
|
2719
|
+
...config,
|
|
2720
|
+
root,
|
|
2721
|
+
input: inputOverride ?? config.input,
|
|
2722
|
+
storage: this.#canWrite ? fsStorage() : memoryStorage(),
|
|
2723
|
+
output: permissions.allowExec ? { ...config.output } : {
|
|
2724
|
+
...config.output,
|
|
2725
|
+
format: false,
|
|
2726
|
+
lint: false,
|
|
2727
|
+
postGenerate: []
|
|
2697
2728
|
},
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
}
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2729
|
+
plugins: resolvedPlugins,
|
|
2730
|
+
adapter
|
|
2731
|
+
},
|
|
2732
|
+
hooks: this.#hooks,
|
|
2733
|
+
signal: controller.signal,
|
|
2734
|
+
jobId: data.jobId,
|
|
2735
|
+
store: this.#generations,
|
|
2736
|
+
snapshotRoot: this.#hasProjectOnDisk ? root : void 0,
|
|
2737
|
+
maxSnapshotMb: this.#limits.maxSnapshotMb
|
|
2738
|
+
});
|
|
2707
2739
|
await this.#hooks.callHook("studio:command:end", {
|
|
2708
2740
|
command,
|
|
2709
2741
|
info: `${resolvedPlugins.length} plugin${resolvedPlugins.length === 1 ? "" : "s"}, ${this.#canWrite ? "written to disk" : "in memory"}${inputOverride !== void 0 ? ", from a Studio spec" : ""}`
|
|
@@ -2765,8 +2797,10 @@ var StudioSession = class {
|
|
|
2765
2797
|
}
|
|
2766
2798
|
if (this.#isGenerating) return refuse("a generation is in progress");
|
|
2767
2799
|
try {
|
|
2768
|
-
const { source: patched, outcomes, changed } =
|
|
2769
|
-
|
|
2800
|
+
const { source: patched, outcomes, changed } = await writeConfigEdits({
|
|
2801
|
+
filePath: configFile,
|
|
2802
|
+
edits
|
|
2803
|
+
});
|
|
2770
2804
|
const applied = outcomes.filter((outcome) => outcome.applied).length;
|
|
2771
2805
|
await this.#hooks.callHook("studio:command:end", {
|
|
2772
2806
|
command,
|
|
@@ -2804,24 +2838,14 @@ var StudioSession = class {
|
|
|
2804
2838
|
version,
|
|
2805
2839
|
peerDependencies: generation.peerDependencies
|
|
2806
2840
|
});
|
|
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"
|
|
2841
|
+
const { token, studioUrl, signal } = this.#options;
|
|
2842
|
+
await uploadSnapshot({
|
|
2843
|
+
bytes,
|
|
2844
|
+
uploadPath,
|
|
2845
|
+
studioUrl,
|
|
2846
|
+
token,
|
|
2847
|
+
shutdown: signal
|
|
2823
2848
|
});
|
|
2824
|
-
if (!response.ok) throw new Error(`Snapshot upload failed with status ${response.status}`);
|
|
2825
2849
|
await this.#hooks.callHook("studio:command:end", {
|
|
2826
2850
|
command,
|
|
2827
2851
|
info: `packed ${Object.keys(files).length} file${Object.keys(files).length === 1 ? "" : "s"}`
|
|
@@ -2835,10 +2859,7 @@ var StudioSession = class {
|
|
|
2835
2859
|
throw error;
|
|
2836
2860
|
}
|
|
2837
2861
|
}
|
|
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
|
-
*/
|
|
2862
|
+
/** A sandbox has no project directory to snapshot. */
|
|
2842
2863
|
get #hasProjectOnDisk() {
|
|
2843
2864
|
return !this.#isSandbox && this.#canRead;
|
|
2844
2865
|
}
|
|
@@ -2867,7 +2888,7 @@ var StudioSession = class {
|
|
|
2867
2888
|
}
|
|
2868
2889
|
};
|
|
2869
2890
|
//#endregion
|
|
2870
|
-
//#region src/client.ts
|
|
2891
|
+
//#region src/runtime/client.ts
|
|
2871
2892
|
/**
|
|
2872
2893
|
* Creates the Kubb Studio client: the connection, the command loop, and the generation event
|
|
2873
2894
|
* stream shared by the `kubb studio` CLI command and the Docker agent.
|
|
@@ -2883,7 +2904,7 @@ var StudioSession = class {
|
|
|
2883
2904
|
*/
|
|
2884
2905
|
function createClient({ onAuthRequired, ...options }) {
|
|
2885
2906
|
const controller = new AbortController();
|
|
2886
|
-
const instanceId = randomUUID();
|
|
2907
|
+
const instanceId = options.instanceId ?? randomUUID();
|
|
2887
2908
|
function notifyAuthRequired(error) {
|
|
2888
2909
|
if (controller.signal.aborted) return;
|
|
2889
2910
|
controller.abort();
|
|
@@ -2904,7 +2925,7 @@ function createClient({ onAuthRequired, ...options }) {
|
|
|
2904
2925
|
};
|
|
2905
2926
|
}
|
|
2906
2927
|
//#endregion
|
|
2907
|
-
//#region src/runConnection.ts
|
|
2928
|
+
//#region src/runtime/runConnection.ts
|
|
2908
2929
|
/**
|
|
2909
2930
|
* Waits for whichever comes first: the shutdown signal, or Studio rejecting the token during a
|
|
2910
2931
|
* background reconnect. Resolves with the rejection, or nothing when the run is being shut down.
|
|
@@ -2977,7 +2998,7 @@ async function runConnection({ credentials, clientOptions, onTokenRejected, sign
|
|
|
2977
2998
|
}
|
|
2978
2999
|
}
|
|
2979
3000
|
//#endregion
|
|
2980
|
-
//#region src/pair.ts
|
|
3001
|
+
//#region src/operations/pair.ts
|
|
2981
3002
|
/** Labels, not secrets: a person approving the code in the browser is what authorizes a pairing. */
|
|
2982
3003
|
const CLIENT_IDS = {
|
|
2983
3004
|
cli: "kubb-cli",
|