@kubb/studio 5.3.15 → 5.3.16
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 +71 -18
- package/dist/index.cjs +106 -64
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +77 -39
- package/dist/index.js +104 -65
- package/dist/index.js.map +1 -1
- package/dist/protocol.cjs.map +1 -1
- package/dist/protocol.d.ts +0 -12
- package/dist/protocol.js.map +1 -1
- package/package.json +4 -4
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
|
|
2
|
-
import { AgentApi, AgentPermissions,
|
|
3
|
-
import { Config, Hookable, KubbHooks } from "@kubb/core";
|
|
2
|
+
import { AgentApi, AgentPermissions, ConfigEdit, ConnectMessagePayload, GenerateInput, GenerateResult, GenerationEvent, GenerationEventPayloads, GenerationEventType, GenerationRun, PublishSnapshotInput, PublishSnapshotResult, RpcConnection, RpcConnector, StudioApi, generationEventTypes } from "./protocol.js";
|
|
4
3
|
import { Storage } from "unstorage";
|
|
4
|
+
import { Config, Hookable, KubbHooks } from "@kubb/core";
|
|
5
5
|
//#region src/api.d.ts
|
|
6
6
|
/**
|
|
7
7
|
* Thrown when Studio rejects the agent token itself (401). Retrying cannot help: the token was
|
|
@@ -15,6 +15,20 @@ export declare class InvalidAgentTokenError extends Error {
|
|
|
15
15
|
* Status values returned by Studio's jobs API.
|
|
16
16
|
*/
|
|
17
17
|
type StudioJobStatus = 'queued' | 'running' | 'success' | 'failed' | 'canceled';
|
|
18
|
+
/** How a snapshot's files differ from the previous one of the same package and agent, relative to `output.path`. */
|
|
19
|
+
type StudioSnapshotChanges = {
|
|
20
|
+
/** The snapshot these changes are measured against, `null` for the first one. */
|
|
21
|
+
base: {
|
|
22
|
+
id: string;
|
|
23
|
+
version: string | null;
|
|
24
|
+
/** The commit the base snapshot was built from, when reported. */
|
|
25
|
+
commit?: string;
|
|
26
|
+
createdAt: string;
|
|
27
|
+
} | null;
|
|
28
|
+
added: Array<string>;
|
|
29
|
+
changed: Array<string>;
|
|
30
|
+
removed: Array<string>;
|
|
31
|
+
};
|
|
18
32
|
/**
|
|
19
33
|
* Package view returned on a successful snapshot job from Studio.
|
|
20
34
|
*/
|
|
@@ -47,6 +61,8 @@ type StudioSnapshot = {
|
|
|
47
61
|
* ISO timestamp after which Studio may delete the tarball.
|
|
48
62
|
*/
|
|
49
63
|
expiresAt: string;
|
|
64
|
+
/** What changed since the previous snapshot. Absent when Studio or the agent predates it. */
|
|
65
|
+
changes?: StudioSnapshotChanges;
|
|
50
66
|
};
|
|
51
67
|
/**
|
|
52
68
|
* Job record from `POST /api/jobs` and `GET /api/jobs/{id}`.
|
|
@@ -88,13 +104,15 @@ type StudioJob = {
|
|
|
88
104
|
* const finished = await waitForJob({ studioUrl, token, id: job.id })
|
|
89
105
|
* ```
|
|
90
106
|
*/
|
|
91
|
-
export declare function createJob({ studioUrl, token, type, agentId, name, version, config }: {
|
|
107
|
+
export declare function createJob({ studioUrl, token, type, agentId, name, version, commit, config }: {
|
|
92
108
|
studioUrl: string;
|
|
93
109
|
token: string;
|
|
94
110
|
type: 'generation' | 'snapshot';
|
|
95
111
|
agentId: string;
|
|
96
112
|
name?: string;
|
|
97
113
|
version?: string;
|
|
114
|
+
/** The commit this snapshot is built from, so the next one can diff against it. */
|
|
115
|
+
commit?: string;
|
|
98
116
|
config?: Record<string, unknown>;
|
|
99
117
|
}): Promise<StudioJob>;
|
|
100
118
|
/**
|
|
@@ -165,10 +183,6 @@ type StudioSessionOptions = {
|
|
|
165
183
|
* The runtime's own version, reported to Studio next to the `kubb` version.
|
|
166
184
|
*/
|
|
167
185
|
version: string;
|
|
168
|
-
/**
|
|
169
|
-
* Identifies the host to Studio, so the UI can badge a CLI connection and show the real project.
|
|
170
|
-
*/
|
|
171
|
-
client?: ClientInfo;
|
|
172
186
|
/**
|
|
173
187
|
* What Studio may do in this project, off unless the host grants it. A sandbox session narrows
|
|
174
188
|
* them further: it never writes to disk and never edits a config file, and it always generates
|
|
@@ -200,13 +214,6 @@ type StudioSessionOptions = {
|
|
|
200
214
|
* default to.
|
|
201
215
|
*/
|
|
202
216
|
installLogger?: (hooks: Hookable<KubbHooks>) => void | Promise<void>;
|
|
203
|
-
/**
|
|
204
|
-
* Threshold for the reconnect loop's own `console.error` lines, using the numeric constants
|
|
205
|
-
* `@kubb/core` exports as `logLevel`. Left out, those lines never print, the same silent default
|
|
206
|
-
* as an unset `installLogger` — a reconnect happens outside any one session's hooks, so it has no
|
|
207
|
-
* other way to ask a host how loud to be.
|
|
208
|
-
*/
|
|
209
|
-
logLevel?: number;
|
|
210
217
|
/**
|
|
211
218
|
* Called when this session's background reconnect is rejected with an invalid token. Unlike
|
|
212
219
|
* `ClientOptions.onAuthRequired`, this fires once per session rather than once per pool:
|
|
@@ -214,15 +221,16 @@ type StudioSessionOptions = {
|
|
|
214
221
|
* directly by a host.
|
|
215
222
|
*/
|
|
216
223
|
onTokenRejected?: (error: InvalidAgentTokenError) => void;
|
|
224
|
+
/**
|
|
225
|
+
* Sent as `studio:warn` once the host's logger is installed. `createClient` uses it to report a
|
|
226
|
+
* failed registration, which happens before any session has hooks to report through. Not meant
|
|
227
|
+
* to be set directly by a host, and dropped on reconnect so it is reported once.
|
|
228
|
+
*/
|
|
229
|
+
startupWarning?: string;
|
|
217
230
|
};
|
|
218
231
|
//#endregion
|
|
219
232
|
//#region src/client.d.ts
|
|
220
|
-
type ClientOptions = Omit<StudioSessionOptions, 'signal' | 'onTokenRejected'> & {
|
|
221
|
-
/**
|
|
222
|
-
* Where the machine secret and the last Studio config are persisted. Defaults to in-memory,
|
|
223
|
-
* which gives up a stable machine identity across restarts.
|
|
224
|
-
*/
|
|
225
|
-
storage?: Storage;
|
|
233
|
+
type ClientOptions = Omit<StudioSessionOptions, 'signal' | 'onTokenRejected' | 'startupWarning'> & {
|
|
226
234
|
/**
|
|
227
235
|
* Called once when a live pool's token is rejected during background reconnect (401: revoked, or
|
|
228
236
|
* the agent was deleted). The whole pool is already stopped by the time this fires, so a host
|
|
@@ -248,7 +256,8 @@ type Client = {
|
|
|
248
256
|
* Creates the Kubb Studio client: the connection, the command loop, and the generation event
|
|
249
257
|
* stream shared by the `kubb studio` CLI command and the Docker agent.
|
|
250
258
|
*
|
|
251
|
-
* Every permission is off by default. A host that wants more grants it explicitly.
|
|
259
|
+
* Every permission is off by default. A host that wants more grants it explicitly. The machine
|
|
260
|
+
* identity comes from the storage the host installed with `setStorage`, before connecting.
|
|
252
261
|
*
|
|
253
262
|
* @example
|
|
254
263
|
* ```ts
|
|
@@ -256,7 +265,7 @@ type Client = {
|
|
|
256
265
|
* await studio.connect()
|
|
257
266
|
* ```
|
|
258
267
|
*/
|
|
259
|
-
export declare function createClient({
|
|
268
|
+
export declare function createClient({ onAuthRequired, ...options }: ClientOptions): Client;
|
|
260
269
|
//#endregion
|
|
261
270
|
//#region src/hooks.d.ts
|
|
262
271
|
/**
|
|
@@ -313,6 +322,10 @@ type StudioDisconnectedContext = {
|
|
|
313
322
|
*/
|
|
314
323
|
reason: string;
|
|
315
324
|
};
|
|
325
|
+
type StudioReconnectingContext = {
|
|
326
|
+
/** Milliseconds until the next connection attempt. */
|
|
327
|
+
delayMs: number;
|
|
328
|
+
};
|
|
316
329
|
type StudioCommandStartContext = {
|
|
317
330
|
/**
|
|
318
331
|
* The command Studio sent, without its `studio:` prefix: `generate`, `connect` or `save`.
|
|
@@ -331,9 +344,11 @@ type StudioCommandEndContext = {
|
|
|
331
344
|
};
|
|
332
345
|
type StudioWarnContext = {
|
|
333
346
|
/**
|
|
334
|
-
* What was refused or ignored
|
|
347
|
+
* What was refused or ignored.
|
|
335
348
|
*/
|
|
336
349
|
message: string;
|
|
350
|
+
/** The missing permission, if that is why, so the host can append its own remedy. */
|
|
351
|
+
permission?: keyof AgentPermissions;
|
|
337
352
|
};
|
|
338
353
|
type StudioErrorContext = {
|
|
339
354
|
/**
|
|
@@ -349,6 +364,7 @@ declare global {
|
|
|
349
364
|
'studio:connected': [ctx: StudioConnectedContext];
|
|
350
365
|
'studio:ready': [ctx: StudioReadyContext];
|
|
351
366
|
'studio:disconnected': [ctx: StudioDisconnectedContext];
|
|
367
|
+
'studio:reconnecting': [ctx: StudioReconnectingContext];
|
|
352
368
|
'studio:command:start': [ctx: StudioCommandStartContext];
|
|
353
369
|
'studio:command:end': [ctx: StudioCommandEndContext];
|
|
354
370
|
'studio:warn': [ctx: StudioWarnContext];
|
|
@@ -485,6 +501,11 @@ type PairingResult = {
|
|
|
485
501
|
organizationSlug?: string;
|
|
486
502
|
};
|
|
487
503
|
};
|
|
504
|
+
/**
|
|
505
|
+
* What this machine registers as. Any member may approve `cli` (`kubb studio`); only an admin may
|
|
506
|
+
* approve `user` and `sandbox` (the Docker image). A `ci` agent never pairs.
|
|
507
|
+
*/
|
|
508
|
+
type PairingAgentType = 'cli' | 'user' | 'sandbox';
|
|
488
509
|
/**
|
|
489
510
|
* Thrown when a caller aborts `startPairing` or `pollForPairingToken` through their `signal`, such
|
|
490
511
|
* as a `kubb studio` shutdown mid-pairing. Distinct from a denial or an expired code, so a host can
|
|
@@ -493,23 +514,22 @@ type PairingResult = {
|
|
|
493
514
|
export declare class PairingCanceledError extends Error {
|
|
494
515
|
constructor();
|
|
495
516
|
}
|
|
517
|
+
/** Thrown when the code expired unapproved. A fresh code can still succeed. */
|
|
518
|
+
export declare class PairingExpiredError extends Error {
|
|
519
|
+
constructor(message?: string);
|
|
520
|
+
}
|
|
521
|
+
/** Thrown when the pairing was denied in the browser, including by the organization's agent limit. */
|
|
522
|
+
export declare class PairingDeniedError extends Error {
|
|
523
|
+
constructor(message?: string);
|
|
524
|
+
}
|
|
496
525
|
type StartPairingOptions = {
|
|
497
526
|
studioUrl?: string;
|
|
527
|
+
type: PairingAgentType;
|
|
498
528
|
/**
|
|
499
529
|
* Display name for the agent, usually the project or machine name.
|
|
500
530
|
*/
|
|
501
531
|
name: string;
|
|
502
532
|
hostname: string;
|
|
503
|
-
/**
|
|
504
|
-
* Which client is pairing. Defaults to the CLI, where any signed-in member may approve their own
|
|
505
|
-
* machine. The Docker image passes `kubb-agent`, whose codes only an admin can approve.
|
|
506
|
-
*/
|
|
507
|
-
clientId?: string;
|
|
508
|
-
/**
|
|
509
|
-
* What a `kubb-agent` pairing asks to be registered as. Studio rejects the request without it,
|
|
510
|
-
* and ignores it for the CLI.
|
|
511
|
-
*/
|
|
512
|
-
agentKind?: 'user' | 'sandbox';
|
|
513
533
|
/**
|
|
514
534
|
* Aborting this cancels the request in flight and rejects with {@link PairingCanceledError}.
|
|
515
535
|
*/
|
|
@@ -520,7 +540,7 @@ type StartPairingOptions = {
|
|
|
520
540
|
* the code, so approval knows which machine it is pairing: the same machine pairing twice rotates
|
|
521
541
|
* one agent's token instead of creating a second agent.
|
|
522
542
|
*/
|
|
523
|
-
export declare function startPairing({ studioUrl, name, hostname,
|
|
543
|
+
export declare function startPairing({ studioUrl, type, name, hostname, signal }: StartPairingOptions): Promise<PairingSession>;
|
|
524
544
|
type PollOptions = {
|
|
525
545
|
studioUrl?: string;
|
|
526
546
|
session: PairingSession;
|
|
@@ -529,17 +549,35 @@ type PollOptions = {
|
|
|
529
549
|
* lands between polls or during the wait for the next one.
|
|
530
550
|
*/
|
|
531
551
|
signal?: AbortSignal;
|
|
552
|
+
/** Called when a poll could not reach Studio. Polling carries on. */
|
|
553
|
+
onRetry?: (error: Error) => void;
|
|
532
554
|
};
|
|
533
555
|
/**
|
|
534
|
-
* Polls until the user approves or denies, honoring the server's `slow_down` back-off.
|
|
535
|
-
* cannot reach Studio is warned about and retried, since the code stays valid either way.
|
|
556
|
+
* Polls until the user approves or denies, honoring the server's `slow_down` back-off.
|
|
536
557
|
*
|
|
537
558
|
* Studio's own endpoint is used rather than the auth layer's `/device/token`, because an approved
|
|
538
559
|
* Kubb pairing is worth an agent bearer token, not a user session.
|
|
539
560
|
*
|
|
540
|
-
* @throws when the code expires
|
|
561
|
+
* @throws {PairingExpiredError} when the code expires before anyone approves it.
|
|
562
|
+
* @throws {PairingDeniedError} when the pairing is denied in the browser.
|
|
563
|
+
* @throws {PairingCanceledError} when `signal` aborts.
|
|
564
|
+
*/
|
|
565
|
+
export declare function pollForPairingToken({ studioUrl, session, signal, onRetry }: PollOptions): Promise<PairingResult>;
|
|
566
|
+
type PairAgentOptions = StartPairingOptions & {
|
|
567
|
+
/** Shows the code to whoever approves it. Called again for each fresh code. */
|
|
568
|
+
onCode: (session: PairingSession, attempt: number) => void | Promise<void>;
|
|
569
|
+
/** Called when a poll could not reach Studio. Polling carries on. */
|
|
570
|
+
onRetry?: (error: Error) => void;
|
|
571
|
+
/**
|
|
572
|
+
* Codes to ask for in total when one expires unapproved. A denial or abort ends it at once.
|
|
573
|
+
* @default 1
|
|
574
|
+
*/
|
|
575
|
+
maxAttempts?: number;
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
578
|
+
* Pairs this machine with Studio: asks for a code, hands it to the host to show, and waits for approval.
|
|
541
579
|
*/
|
|
542
|
-
export declare function
|
|
580
|
+
export declare function pairAgent({ onCode, onRetry, maxAttempts, ...options }: PairAgentOptions): Promise<PairingResult>;
|
|
543
581
|
//#endregion
|
|
544
582
|
//#region src/rpc.d.ts
|
|
545
583
|
/**
|
|
@@ -554,5 +592,5 @@ export declare function pollForPairingToken({ studioUrl, session, signal }: Poll
|
|
|
554
592
|
*/
|
|
555
593
|
export declare const connectWebSocketRpc: RpcConnector;
|
|
556
594
|
//#endregion
|
|
557
|
-
export { type AgentApi, type Client, type ClientOptions, type ConfigEdit, type ConnectMessagePayload, type ConnectionOptions, type GenerateInput, type GenerateResult, type GenerationEvent, type GenerationEventPayloads, type GenerationEventType, type GenerationRun, type PublishSnapshotInput, type PublishSnapshotResult, type RpcConnection, type RpcConnector, type StudioAgent, type StudioApi, type StudioConnectedContext, type StudioJob, type StudioJobStatus, type StudioSnapshot, generationEventTypes };
|
|
595
|
+
export { type AgentApi, type Client, type ClientOptions, type ConfigEdit, type ConnectMessagePayload, type ConnectionOptions, type GenerateInput, type GenerateResult, type GenerationEvent, type GenerationEventPayloads, type GenerationEventType, type GenerationRun, type PairingAgentType, type PairingResult, type PairingSession, type PublishSnapshotInput, type PublishSnapshotResult, type RpcConnection, type RpcConnector, type StudioAgent, type StudioApi, type StudioConnectedContext, type StudioJob, type StudioJobStatus, type StudioSnapshot, type StudioSnapshotChanges, generationEventTypes };
|
|
558
596
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
|
|
2
2
|
import { GENERATION_GONE_MESSAGE, generationEventTypes } from "./protocol.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { promisify, styleText } from "node:util";
|
|
5
4
|
import process$1 from "node:process";
|
|
6
5
|
import { spawn } from "node:child_process";
|
|
7
6
|
import { glob, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
8
7
|
import path, { isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
9
|
-
import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, logLevel, memoryStorage } from "@kubb/core";
|
|
10
8
|
import { FetchError, ofetch } from "ofetch";
|
|
11
9
|
import { createHash, hash, randomBytes } from "node:crypto";
|
|
10
|
+
import { promisify, styleText } from "node:util";
|
|
12
11
|
import { createStorage } from "unstorage";
|
|
13
12
|
import fsDriver from "unstorage/drivers/fs";
|
|
13
|
+
import { Diagnostics, Hookable, cacheStorage, createKubb, fsStorage, memoryStorage } from "@kubb/core";
|
|
14
14
|
import { x } from "tinyexec";
|
|
15
15
|
import { builders, detectCodeFormat, generateCode, parseModule } from "magicast";
|
|
16
16
|
import { existsSync } from "node:fs";
|
|
@@ -572,29 +572,27 @@ async function runRegistration({ token, studioUrl, poolSize }) {
|
|
|
572
572
|
return true;
|
|
573
573
|
} catch (error) {
|
|
574
574
|
if (rejectedWith(error, 401)) throw new InvalidAgentTokenError(studioUrl, { cause: error });
|
|
575
|
-
console.error(styleText("red", `Failed to register agent with Studio after 4 attempts`));
|
|
576
575
|
return false;
|
|
577
576
|
}
|
|
578
577
|
}
|
|
579
578
|
/**
|
|
580
579
|
* Notify Kubb Studio that this agent is disconnecting.
|
|
581
|
-
* Called on process termination or server close.
|
|
582
|
-
*
|
|
580
|
+
* Called on process termination or server close. Never throws: the local socket is already gone,
|
|
581
|
+
* and failing teardown must not block shutdown or reconnect.
|
|
582
|
+
*
|
|
583
|
+
* @returns `false` when Studio could not be reached or rate limited the call. Any other 4xx
|
|
584
|
+
* counts as notified, since it means Studio already dropped the session.
|
|
583
585
|
*/
|
|
584
|
-
async function disconnect({ sessionId, token, studioUrl
|
|
585
|
-
const url = `${studioUrl}/api/agent/sessions/${sessionId}/disconnect`;
|
|
586
|
-
const tag = slug ?? "agent";
|
|
587
|
-
const canLog = logLevel$2 !== void 0 && logLevel$2 > logLevel.silent;
|
|
586
|
+
async function disconnect({ sessionId, token, studioUrl }) {
|
|
588
587
|
try {
|
|
589
|
-
await ofetch(
|
|
588
|
+
await ofetch(`${studioUrl}/api/agent/sessions/${sessionId}/disconnect`, {
|
|
590
589
|
method: "POST",
|
|
591
590
|
headers: { Authorization: `Bearer ${token}` }
|
|
592
591
|
});
|
|
593
|
-
|
|
592
|
+
return true;
|
|
594
593
|
} catch (error) {
|
|
595
594
|
const statusCode = error?.statusCode;
|
|
596
|
-
|
|
597
|
-
if (canLog) console.warn(styleText("yellow", `[${tag}] Failed to notify Studio of disconnection: ${getErrorMessage(error)}`));
|
|
595
|
+
return statusCode !== void 0 && statusCode !== 429 && statusCode >= 400 && statusCode < 500;
|
|
598
596
|
}
|
|
599
597
|
}
|
|
600
598
|
/**
|
|
@@ -616,7 +614,7 @@ async function disconnect({ sessionId, token, studioUrl, slug, logLevel: logLeve
|
|
|
616
614
|
* const finished = await waitForJob({ studioUrl, token, id: job.id })
|
|
617
615
|
* ```
|
|
618
616
|
*/
|
|
619
|
-
async function createJob({ studioUrl, token, type, agentId, name, version, config }) {
|
|
617
|
+
async function createJob({ studioUrl, token, type, agentId, name, version, commit, config }) {
|
|
620
618
|
const { job } = await ofetch(`${studioUrl}/api/jobs`, {
|
|
621
619
|
method: "POST",
|
|
622
620
|
headers: { "x-api-key": token },
|
|
@@ -625,6 +623,7 @@ async function createJob({ studioUrl, token, type, agentId, name, version, confi
|
|
|
625
623
|
agentId,
|
|
626
624
|
name,
|
|
627
625
|
version,
|
|
626
|
+
commit,
|
|
628
627
|
config
|
|
629
628
|
}
|
|
630
629
|
});
|
|
@@ -695,7 +694,7 @@ async function createAgent({ studioUrl, token, name, machineToken }) {
|
|
|
695
694
|
}
|
|
696
695
|
//#endregion
|
|
697
696
|
//#region package.json
|
|
698
|
-
var version = "5.3.
|
|
697
|
+
var version = "5.3.16";
|
|
699
698
|
//#endregion
|
|
700
699
|
//#region src/hooks.ts
|
|
701
700
|
/**
|
|
@@ -2245,15 +2244,13 @@ function applyStudioDefaults(options) {
|
|
|
2245
2244
|
* socket, its hook emitter, or its session id alive for the length of the retry interval.
|
|
2246
2245
|
*/
|
|
2247
2246
|
function reconnect(options) {
|
|
2248
|
-
const { signal, retryInterval, onTokenRejected
|
|
2247
|
+
const { signal, retryInterval, onTokenRejected } = options;
|
|
2249
2248
|
if (signal?.aborted) return;
|
|
2250
|
-
if (logLevel$1 !== void 0 && logLevel$1 > logLevel.silent) console.error(styleText("dim", `Retrying connection in ${retryInterval}ms to Kubb Studio ...`));
|
|
2251
2249
|
const cancel = () => clearTimeout(timer);
|
|
2252
2250
|
const timer = setTimeout(() => {
|
|
2253
2251
|
signal?.removeEventListener("abort", cancel);
|
|
2254
2252
|
if (signal?.aborted) return;
|
|
2255
2253
|
new StudioSession(options).start().catch((error) => {
|
|
2256
|
-
if (logLevel$1 !== void 0 && logLevel$1 > logLevel.silent) console.error(styleText("red", `Reconnect attempt to Kubb Studio failed: ${getErrorMessage(error)}`));
|
|
2257
2254
|
if (error instanceof InvalidAgentTokenError) {
|
|
2258
2255
|
onTokenRejected?.(error);
|
|
2259
2256
|
return;
|
|
@@ -2293,8 +2290,10 @@ var StudioSession = class {
|
|
|
2293
2290
|
* host does not queue jobs before the agent session is registered.
|
|
2294
2291
|
*/
|
|
2295
2292
|
#connectAck = Promise.withResolvers();
|
|
2296
|
-
|
|
2293
|
+
#startupWarning;
|
|
2294
|
+
constructor({ startupWarning, ...options }) {
|
|
2297
2295
|
this.#options = applyStudioDefaults(options);
|
|
2296
|
+
this.#startupWarning = startupWarning;
|
|
2298
2297
|
this.#connectAck.promise.catch(() => {});
|
|
2299
2298
|
}
|
|
2300
2299
|
/**
|
|
@@ -2337,6 +2336,7 @@ var StudioSession = class {
|
|
|
2337
2336
|
async start() {
|
|
2338
2337
|
const { token, studioUrl, signal, heartbeatInterval, installLogger } = this.#options;
|
|
2339
2338
|
await installLogger?.(this.#hooks);
|
|
2339
|
+
if (this.#startupWarning) await this.#warn(this.#startupWarning);
|
|
2340
2340
|
try {
|
|
2341
2341
|
await this.#hooks.callHook("studio:connecting", { url: studioUrl });
|
|
2342
2342
|
const session = await createAgentSession({
|
|
@@ -2372,18 +2372,30 @@ var StudioSession = class {
|
|
|
2372
2372
|
this.dispose();
|
|
2373
2373
|
await this.#hooks.callHook("studio:error", { error: toError(error) });
|
|
2374
2374
|
if (error instanceof InvalidAgentTokenError) throw error;
|
|
2375
|
-
reconnect(
|
|
2375
|
+
await this.#reconnect();
|
|
2376
2376
|
}
|
|
2377
2377
|
}
|
|
2378
|
-
|
|
2379
|
-
|
|
2378
|
+
/**
|
|
2379
|
+
* Tells the host a retry is coming, then schedules it. The host prints the retry, since the
|
|
2380
|
+
* runtime has no output of its own.
|
|
2381
|
+
*/
|
|
2382
|
+
async #reconnect() {
|
|
2383
|
+
if (this.#options.signal?.aborted) return;
|
|
2384
|
+
await this.#hooks.callHook("studio:reconnecting", { delayMs: this.#options.retryInterval });
|
|
2385
|
+
reconnect(this.#options);
|
|
2386
|
+
}
|
|
2387
|
+
#warn(message, permission) {
|
|
2388
|
+
return this.#hooks.callHook("studio:warn", {
|
|
2389
|
+
message,
|
|
2390
|
+
permission
|
|
2391
|
+
});
|
|
2380
2392
|
}
|
|
2381
2393
|
/**
|
|
2382
2394
|
* Declines a request: logs why locally, then tells Studio. The two wordings differ on purpose,
|
|
2383
2395
|
* since the log names the request that was ignored and the error names what the caller can do.
|
|
2384
2396
|
*/
|
|
2385
|
-
async #refuse(reason, message) {
|
|
2386
|
-
await this.#warn(reason);
|
|
2397
|
+
async #refuse(reason, message, permission) {
|
|
2398
|
+
await this.#warn(reason, permission);
|
|
2387
2399
|
throw new Error(message);
|
|
2388
2400
|
}
|
|
2389
2401
|
#scheduleHeartbeat(interval) {
|
|
@@ -2472,19 +2484,17 @@ var StudioSession = class {
|
|
|
2472
2484
|
* `#disposed` keeps the close event from running this twice, and a shutdown from reconnecting.
|
|
2473
2485
|
*/
|
|
2474
2486
|
async #end({ retry }) {
|
|
2475
|
-
const { studioUrl, token
|
|
2487
|
+
const { studioUrl, token } = this.#options;
|
|
2476
2488
|
if (this.#disposed) return;
|
|
2477
2489
|
this.#disposed = true;
|
|
2478
2490
|
this.dispose();
|
|
2479
2491
|
await this.#hooks.callHook("studio:disconnected", { reason: retry ? "connection closed" : "shutdown" });
|
|
2480
|
-
if (this.#session
|
|
2492
|
+
if (this.#session && !await disconnect({
|
|
2481
2493
|
sessionId: this.#session.sessionId,
|
|
2482
2494
|
studioUrl,
|
|
2483
|
-
token
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
}).catch(() => {});
|
|
2487
|
-
if (retry) reconnect(this.#options);
|
|
2495
|
+
token
|
|
2496
|
+
})) await this.#warn("Could not notify Kubb Studio of the disconnect");
|
|
2497
|
+
if (retry) await this.#reconnect();
|
|
2488
2498
|
}
|
|
2489
2499
|
startGeneration(data) {
|
|
2490
2500
|
const generationStream = createGenerationStream(this.#hooks, data.jobId, { onGenerationEnd: (result) => {
|
|
@@ -2510,7 +2520,7 @@ var StudioSession = class {
|
|
|
2510
2520
|
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");
|
|
2511
2521
|
this.#isGenerating = true;
|
|
2512
2522
|
const command = "generate";
|
|
2513
|
-
const { root, loadConfig, permissions
|
|
2523
|
+
const { root, loadConfig, permissions } = this.#options;
|
|
2514
2524
|
try {
|
|
2515
2525
|
await this.#hooks.callHook("studio:command:start", { command });
|
|
2516
2526
|
const config = await loadConfig();
|
|
@@ -2519,10 +2529,7 @@ var StudioSession = class {
|
|
|
2519
2529
|
const adapter = await mergeAdapter(config.adapter, patch?.adapter);
|
|
2520
2530
|
const inputOverride = this.#isSandbox ? patch?.input ?? "" : permissions.allowInput && patch?.input || void 0;
|
|
2521
2531
|
if (permissions.allowWrite && this.#isSandbox) await this.#warn("Running in a sandbox, so writing files is disabled");
|
|
2522
|
-
if (patch?.input && !this.#canUseInput)
|
|
2523
|
-
const remedy = client?.kind === "cli" ? "--allowInput, or answer yes when kubb studio asks," : "KUBB_AGENT_ALLOW_INPUT=true";
|
|
2524
|
-
await this.#warn(`Ignored the spec from Studio; set ${remedy} to generate from it`);
|
|
2525
|
-
}
|
|
2532
|
+
if (patch?.input && !this.#canUseInput) await this.#warn("Ignored the spec from Studio: generating from a Studio spec was not granted", "allowInput");
|
|
2526
2533
|
const resolvedPlugins = plugins ?? config.plugins;
|
|
2527
2534
|
this.#lastGeneration = void 0;
|
|
2528
2535
|
const diskFiles = this.#hasProjectOnDisk ? await listDisk({
|
|
@@ -2702,12 +2709,7 @@ var StudioSession = class {
|
|
|
2702
2709
|
async readFiles(data) {
|
|
2703
2710
|
const command = "readFiles";
|
|
2704
2711
|
await this.#hooks.callHook("studio:command:start", { command });
|
|
2705
|
-
|
|
2706
|
-
if (!this.#canRead) {
|
|
2707
|
-
await this.#warn("Ignored files: reading generated files was not granted");
|
|
2708
|
-
const remedy = client?.kind === "cli" ? "--allow-read, or answer yes when kubb studio asks," : "KUBB_AGENT_ALLOW_READ=true";
|
|
2709
|
-
throw new Error(`The agent was not granted permission to read generated files; set ${remedy} to allow it`);
|
|
2710
|
-
}
|
|
2712
|
+
if (!this.#canRead) return this.#refuse("Ignored files: reading generated files was not granted", "The agent was not granted permission to read generated files", "allowRead");
|
|
2711
2713
|
if (!Array.isArray(data.paths)) return this.#refuse("Ignored files: the message carried no paths", "The request carried no paths");
|
|
2712
2714
|
const { paths } = data;
|
|
2713
2715
|
if (paths.length > 50) return this.#refuse(`Ignored files: requested ${paths.length} paths, more than the 50 allowed per request`, `At most 50 paths may be requested at once`);
|
|
@@ -2734,7 +2736,8 @@ var StudioSession = class {
|
|
|
2734
2736
|
* Creates the Kubb Studio client: the connection, the command loop, and the generation event
|
|
2735
2737
|
* stream shared by the `kubb studio` CLI command and the Docker agent.
|
|
2736
2738
|
*
|
|
2737
|
-
* Every permission is off by default. A host that wants more grants it explicitly.
|
|
2739
|
+
* Every permission is off by default. A host that wants more grants it explicitly. The machine
|
|
2740
|
+
* identity comes from the storage the host installed with `setStorage`, before connecting.
|
|
2738
2741
|
*
|
|
2739
2742
|
* @example
|
|
2740
2743
|
* ```ts
|
|
@@ -2742,8 +2745,7 @@ var StudioSession = class {
|
|
|
2742
2745
|
* await studio.connect()
|
|
2743
2746
|
* ```
|
|
2744
2747
|
*/
|
|
2745
|
-
function createClient({
|
|
2746
|
-
if (storage) setStorage(storage);
|
|
2748
|
+
function createClient({ onAuthRequired, ...options }) {
|
|
2747
2749
|
const controller = new AbortController();
|
|
2748
2750
|
const poolSize = options.poolSize ?? agentDefaults.poolSize;
|
|
2749
2751
|
function notifyAuthRequired(error) {
|
|
@@ -2753,15 +2755,18 @@ function createClient({ storage, onAuthRequired, ...options }) {
|
|
|
2753
2755
|
}
|
|
2754
2756
|
return {
|
|
2755
2757
|
async connect() {
|
|
2756
|
-
await registerAgent({
|
|
2758
|
+
const registered = await registerAgent({
|
|
2757
2759
|
token: options.token,
|
|
2758
2760
|
studioUrl: options.studioUrl ?? agentDefaults.studioUrl,
|
|
2759
2761
|
poolSize
|
|
2760
2762
|
});
|
|
2761
|
-
|
|
2763
|
+
if (controller.signal.aborted) return;
|
|
2764
|
+
const startupWarning = registered ? void 0 : "Could not register with Kubb Studio, continuing";
|
|
2765
|
+
await Promise.all(Array.from({ length: poolSize }, (_, slot) => new StudioSession({
|
|
2762
2766
|
...options,
|
|
2763
2767
|
signal: controller.signal,
|
|
2764
|
-
onTokenRejected: notifyAuthRequired
|
|
2768
|
+
onTokenRejected: notifyAuthRequired,
|
|
2769
|
+
startupWarning: slot === 0 ? startupWarning : void 0
|
|
2765
2770
|
}).start()));
|
|
2766
2771
|
},
|
|
2767
2772
|
disconnect() {
|
|
@@ -2844,11 +2849,11 @@ async function runConnection({ credentials, clientOptions, onTokenRejected, sign
|
|
|
2844
2849
|
}
|
|
2845
2850
|
//#endregion
|
|
2846
2851
|
//#region src/pair.ts
|
|
2847
|
-
/**
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
+
/** Labels, not secrets: a person approving the code in the browser is what authorizes a pairing. */
|
|
2853
|
+
const CLIENT_IDS = {
|
|
2854
|
+
cli: "kubb-cli",
|
|
2855
|
+
agent: "kubb-agent"
|
|
2856
|
+
};
|
|
2852
2857
|
/**
|
|
2853
2858
|
* Thrown when a caller aborts `startPairing` or `pollForPairingToken` through their `signal`, such
|
|
2854
2859
|
* as a `kubb studio` shutdown mid-pairing. Distinct from a denial or an expired code, so a host can
|
|
@@ -2860,21 +2865,35 @@ var PairingCanceledError = class extends Error {
|
|
|
2860
2865
|
this.name = "PairingCanceledError";
|
|
2861
2866
|
}
|
|
2862
2867
|
};
|
|
2868
|
+
/** Thrown when the code expired unapproved. A fresh code can still succeed. */
|
|
2869
|
+
var PairingExpiredError = class extends Error {
|
|
2870
|
+
constructor(message = "The pairing code expired, pair again") {
|
|
2871
|
+
super(message);
|
|
2872
|
+
this.name = "PairingExpiredError";
|
|
2873
|
+
}
|
|
2874
|
+
};
|
|
2875
|
+
/** Thrown when the pairing was denied in the browser, including by the organization's agent limit. */
|
|
2876
|
+
var PairingDeniedError = class extends Error {
|
|
2877
|
+
constructor(message = "Pairing was denied in the browser") {
|
|
2878
|
+
super(message);
|
|
2879
|
+
this.name = "PairingDeniedError";
|
|
2880
|
+
}
|
|
2881
|
+
};
|
|
2863
2882
|
/**
|
|
2864
2883
|
* Asks Studio for a pairing code. The machine token travels with the request and is stored against
|
|
2865
2884
|
* the code, so approval knows which machine it is pairing: the same machine pairing twice rotates
|
|
2866
2885
|
* one agent's token instead of creating a second agent.
|
|
2867
2886
|
*/
|
|
2868
|
-
async function startPairing({ studioUrl = agentDefaults.studioUrl, name, hostname,
|
|
2887
|
+
async function startPairing({ studioUrl = agentDefaults.studioUrl, type, name, hostname, signal }) {
|
|
2869
2888
|
try {
|
|
2870
2889
|
return await ofetch(`${studioUrl}/api/auth/device/code`, {
|
|
2871
2890
|
method: "POST",
|
|
2872
2891
|
body: {
|
|
2873
|
-
client_id:
|
|
2892
|
+
client_id: type === "cli" ? CLIENT_IDS.cli : CLIENT_IDS.agent,
|
|
2874
2893
|
name,
|
|
2875
2894
|
hostname,
|
|
2876
2895
|
machine_token: await getMachineToken(),
|
|
2877
|
-
agent_kind:
|
|
2896
|
+
agent_kind: type === "cli" ? void 0 : type
|
|
2878
2897
|
},
|
|
2879
2898
|
signal
|
|
2880
2899
|
});
|
|
@@ -2887,15 +2906,16 @@ function isPairingResult(response) {
|
|
|
2887
2906
|
return !!response && typeof response === "object" && "token" in response && typeof response.token === "string";
|
|
2888
2907
|
}
|
|
2889
2908
|
/**
|
|
2890
|
-
* Polls until the user approves or denies, honoring the server's `slow_down` back-off.
|
|
2891
|
-
* cannot reach Studio is warned about and retried, since the code stays valid either way.
|
|
2909
|
+
* Polls until the user approves or denies, honoring the server's `slow_down` back-off.
|
|
2892
2910
|
*
|
|
2893
2911
|
* Studio's own endpoint is used rather than the auth layer's `/device/token`, because an approved
|
|
2894
2912
|
* Kubb pairing is worth an agent bearer token, not a user session.
|
|
2895
2913
|
*
|
|
2896
|
-
* @throws when the code expires
|
|
2914
|
+
* @throws {PairingExpiredError} when the code expires before anyone approves it.
|
|
2915
|
+
* @throws {PairingDeniedError} when the pairing is denied in the browser.
|
|
2916
|
+
* @throws {PairingCanceledError} when `signal` aborts.
|
|
2897
2917
|
*/
|
|
2898
|
-
async function pollForPairingToken({ studioUrl = agentDefaults.studioUrl, session, signal }) {
|
|
2918
|
+
async function pollForPairingToken({ studioUrl = agentDefaults.studioUrl, session, signal, onRetry }) {
|
|
2899
2919
|
const deadline = Date.now() + (session.expires_in > 0 ? session.expires_in : 600) * 1e3;
|
|
2900
2920
|
let intervalMs = (session.interval > 0 ? session.interval : 5) * 1e3;
|
|
2901
2921
|
while (Date.now() < deadline) {
|
|
@@ -2915,7 +2935,7 @@ async function pollForPairingToken({ studioUrl = agentDefaults.studioUrl, sessio
|
|
|
2915
2935
|
});
|
|
2916
2936
|
} catch (error) {
|
|
2917
2937
|
if (signal?.aborted) throw new PairingCanceledError();
|
|
2918
|
-
|
|
2938
|
+
onRetry?.(toError(error));
|
|
2919
2939
|
continue;
|
|
2920
2940
|
}
|
|
2921
2941
|
if (isPairingResult(response)) return response;
|
|
@@ -2925,13 +2945,32 @@ async function pollForPairingToken({ studioUrl = agentDefaults.studioUrl, sessio
|
|
|
2925
2945
|
intervalMs += 5e3;
|
|
2926
2946
|
continue;
|
|
2927
2947
|
}
|
|
2928
|
-
if (response.error === "access_denied") throw new
|
|
2929
|
-
if (response.error === "expired_token" || response.error === "invalid_grant") throw new
|
|
2948
|
+
if (response.error === "access_denied") throw new PairingDeniedError(response.error_description);
|
|
2949
|
+
if (response.error === "expired_token" || response.error === "invalid_grant") throw new PairingExpiredError(response.error_description);
|
|
2930
2950
|
throw new Error(response.error_description ?? `Pairing failed (${response.error})`);
|
|
2931
2951
|
}
|
|
2932
|
-
throw new
|
|
2952
|
+
throw new PairingExpiredError();
|
|
2953
|
+
}
|
|
2954
|
+
/**
|
|
2955
|
+
* Pairs this machine with Studio: asks for a code, hands it to the host to show, and waits for approval.
|
|
2956
|
+
*/
|
|
2957
|
+
async function pairAgent({ onCode, onRetry, maxAttempts = 1, ...options }) {
|
|
2958
|
+
for (let attempt = 1;; attempt++) {
|
|
2959
|
+
const session = await startPairing(options);
|
|
2960
|
+
await onCode(session, attempt);
|
|
2961
|
+
try {
|
|
2962
|
+
return await pollForPairingToken({
|
|
2963
|
+
studioUrl: options.studioUrl,
|
|
2964
|
+
session,
|
|
2965
|
+
signal: options.signal,
|
|
2966
|
+
onRetry
|
|
2967
|
+
});
|
|
2968
|
+
} catch (error) {
|
|
2969
|
+
if (!(error instanceof PairingExpiredError) || attempt >= maxAttempts) throw error;
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2933
2972
|
}
|
|
2934
2973
|
//#endregion
|
|
2935
|
-
export { InvalidAgentTokenError, PairingCanceledError, connectWebSocketRpc, createAgent, createClient, createFileStorage, createJob, defaultStudioUrl, generationEventTypes, machineTokenFrom, pollForPairingToken, runConnection, setStorage, startPairing, waitForJob };
|
|
2974
|
+
export { InvalidAgentTokenError, PairingCanceledError, PairingDeniedError, PairingExpiredError, connectWebSocketRpc, createAgent, createClient, createFileStorage, createJob, defaultStudioUrl, generationEventTypes, machineTokenFrom, pairAgent, pollForPairingToken, runConnection, setStorage, startPairing, waitForJob };
|
|
2936
2975
|
|
|
2937
2976
|
//# sourceMappingURL=index.js.map
|