@kubb/studio 5.3.14 → 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/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { t as __name } from "./rolldown-runtime-CRm0XQPb.js";
2
- import { AgentApi, AgentPermissions, ClientInfo, ConfigEdit, ConnectMessagePayload, GenerateInput, GenerateResult, GenerationEvent, GenerationEventPayloads, GenerationEventType, GenerationRun, PublishSnapshotInput, PublishSnapshotResult, RpcConnection, RpcConnector, StudioApi, generationEventTypes } from "./protocol.js";
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({ storage, onAuthRequired, ...options }: ClientOptions): Client;
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, and what would change it.
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, clientId, agentKind, signal }: StartPairingOptions): Promise<PairingSession>;
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. A poll that
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, the user denies it, or Studio returns an unexpected error.
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 pollForPairingToken({ studioUrl, session, signal }: PollOptions): Promise<PairingResult>;
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. A failed notify is logged and swallowed: the
582
- * local socket is already gone, and failing teardown must not block shutdown or reconnect.
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, slug, logLevel: logLevel$2 }) {
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(url, {
588
+ await ofetch(`${studioUrl}/api/agent/sessions/${sessionId}/disconnect`, {
590
589
  method: "POST",
591
590
  headers: { Authorization: `Bearer ${token}` }
592
591
  });
593
- if (canLog) console.error(styleText("green", `[${tag}] Disconnected from Studio`));
592
+ return true;
594
593
  } catch (error) {
595
594
  const statusCode = error?.statusCode;
596
- if (statusCode !== void 0 && statusCode >= 400 && statusCode < 500) return;
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.14";
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, logLevel: logLevel$1 } = options;
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
- constructor(options) {
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(this.#options);
2375
+ await this.#reconnect();
2376
2376
  }
2377
2377
  }
2378
- #warn(message) {
2379
- return this.#hooks.callHook("studio:warn", { message });
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, logLevel } = this.#options;
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) await disconnect({
2492
+ if (this.#session && !await disconnect({
2481
2493
  sessionId: this.#session.sessionId,
2482
2494
  studioUrl,
2483
- token,
2484
- slug: this.#session.slug,
2485
- logLevel
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, client } = this.#options;
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
- const { client } = this.#options;
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({ storage, onAuthRequired, ...options }) {
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
- await Promise.all(Array.from({ length: poolSize }, () => new StudioSession({
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
- * Identifies the CLI to Studio's device authorization endpoint. A label, not a secret: what
2849
- * authorizes a pairing is a signed-in person approving the code in the browser.
2850
- */
2851
- const CLIENT_ID = "kubb-cli";
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, clientId = CLIENT_ID, agentKind, signal }) {
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: clientId,
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: agentKind
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. A poll that
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, the user denies it, or Studio returns an unexpected error.
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
- console.warn(styleText("yellow", `Could not reach Kubb Studio while waiting for approval, retrying: ${getErrorMessage(error)}`));
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 Error(response.error_description ?? "Pairing was denied in the browser");
2929
- if (response.error === "expired_token" || response.error === "invalid_grant") throw new Error(response.error_description ?? "The pairing code expired, pair again");
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 Error("The pairing code expired, pair again");
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