@kubb/studio 5.6.0 → 5.6.1

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 CHANGED
@@ -24,9 +24,9 @@
24
24
 
25
25
  Kubb Studio client runtime.
26
26
 
27
- Connects a Kubb project to [Kubb Studio](https://kubb.studio) through typed Cap'n Web RPC over an
28
- authenticated WebSocket. It backs both front ends: the `kubb studio` CLI command and the
29
- `kubblabs/kubb-agent` Docker image. Browser clients use Studio’s HTTP/SSE API, not this socket.
27
+ Connects a Kubb project to [Kubb Studio](https://kubb.studio) through typed Cap'n Web RPC over one
28
+ authenticated WebSocket, and publishes snapshots from CI in one HTTP request. It backs the `kubb studio`
29
+ CLI command and the `kubblabs/kubb-agent` Docker image.
30
30
 
31
31
  Most people never install this directly. Reach for `kubb studio` instead, which pairs your machine
32
32
  and runs this for you.
@@ -40,63 +40,63 @@ npm install @kubb/studio
40
40
  ## Usage
41
41
 
42
42
  ```typescript
43
- import { createFileStorage, runConnection, setStorage } from '@kubb/studio'
44
-
45
- // The machine identity lives in this storage, so install it before anything pairs or connects.
46
- setStorage(createFileStorage('./.kubb-cache'))
47
-
48
- const outcome = await runConnection({
49
- credentials: { token: process.env.KUBB_AGENT_TOKEN! },
50
- clientOptions: () => ({
51
- configPath: 'kubb.config.ts',
52
- version: '1.0.0',
53
- loadConfig: () => loadMyKubbConfig(),
54
- installLogger: (hooks) => {
55
- hooks.hook('studio:connected', ({ url }) => console.log(`Connected to ${url}`))
56
- hooks.hook('studio:reconnecting', ({ delayMs }) => console.log(`Retrying in ${delayMs}ms`))
57
- hooks.hook('studio:warn', ({ message }) => console.warn(message))
58
- hooks.hook('studio:error', ({ error }) => console.error(error.message))
59
- },
60
- }),
61
- signal: shutdown.signal,
62
- // Return a new credential to reconnect with, `null` to stop, or throw to fail the run.
63
- onTokenRejected: async () => null,
64
- })
43
+ import { Hookable, type KubbHooks } from '@kubb/core'
44
+ import { connectAgent, InvalidAgentTokenError, pairAgent } from '@kubb/studio'
45
+
46
+ const hooks = new Hookable<KubbHooks>()
47
+ hooks.hook('studio:connected', ({ agentSlug }) => console.log(`Connected as ${agentSlug}`))
48
+ hooks.hook('studio:warn', ({ message }) => console.warn(message))
49
+
50
+ let token = await readStoredToken()
51
+
52
+ for (;;) {
53
+ token ??= (await pairAgent({ type: 'cli', name: 'my-project', hostname, machineId, onCode })).token
54
+ try {
55
+ // Resolves on shutdown, rejects when Studio refuses the token.
56
+ await connectAgent({ token, configPath: 'kubb.config.ts', loadConfig, version: '1.0.0', hooks, signal })
57
+ break
58
+ } catch (error) {
59
+ if (!(error instanceof InvalidAgentTokenError)) throw error
60
+ token = undefined
61
+ }
62
+ }
65
63
  ```
66
64
 
67
- The runtime discovers nothing on its own: the host injects the config loader, the storage, and its
68
- own version. That is what lets one runtime serve a CLI running in a developer's project and a
69
- container running a fixed plugin set.
65
+ The runtime discovers nothing on its own. The host passes the token, the config loader, its own
66
+ version, and a hook emitter, and keeps its own credentials. That is what lets one runtime serve a CLI
67
+ running in a developer's project and a container running a fixed plugin set.
68
+
69
+ ## Source layout
70
70
 
71
- ## Package layers
71
+ | File | What it owns |
72
+ | ---------------- | -------------------------------------------------------------------------------- |
73
+ | `protocol.ts` | The wire contract Studio compiles against, published as `@kubb/studio/protocol`. |
74
+ | `connect.ts` | `connectAgent`: the socket loop and the `Agent` that serves Studio's calls. |
75
+ | `pair.ts` | `pairAgent`: RFC 8628 device pairing. |
76
+ | `snapshot.ts` | `createSnapshot` for CI, and packing a generation into an npm tarball. |
77
+ | `generations.ts` | Running a generation and keeping its files for `readFiles`. |
78
+ | `configFile.ts` | Reading and patching plugin options in `kubb.config.ts`. |
72
79
 
73
- `src/protocol` defines the shared wire contracts and imports no Studio implementation. `src/operations`
74
- contains focused work such as pairing, generation, config edits, snapshots, and transport; it may
75
- import the protocol. `src/runtime` owns connection state and coordinates operations; it may import
76
- both lower layers. Keep new code in the lowest layer that can own it, with direct internal imports.
77
- Only `@kubb/studio` and `@kubb/studio/protocol` are public entry points.
80
+ ## Connection
78
81
 
79
- ## Hosts and the runtime
82
+ `connectAgent` opens `wss://<studio>/api/agent/socket` with the agent's bearer token and an
83
+ `x-kubb-instance-id` header. Studio calls `connect(info)` as the first RPC call. `info` says whether
84
+ the agent is a sandbox and carries its slug and Studio's version. From then on the agent pings every
85
+ 30 seconds, and Studio schedules one job at a time on the socket.
80
86
 
81
- Three hosts run this package: `kubb studio`, the `kubblabs/kubb-agent` Docker image, and
82
- `kubb studio snapshot` in CI. All three follow the steps below. The runtime owns only the
83
- connection: it doesn't read CI environment variables or print anything, and behaves the same in
84
- every host.
87
+ Studio ends a socket on purpose with one of these close codes:
85
88
 
86
- | Step | `kubb studio` | Docker agent | `kubb studio snapshot` |
87
- | ---------------- | --------------------------------------- | ---------------------------------------- | ---------------------------------------- |
88
- | Machine identity | `setStorage` under project Kubb home | `setStorage` on the cache volume | `KUBB_AGENT_SECRET` from the CI identity |
89
- | Credentials | stored, or `pairAgent({ type: 'cli' })` | stored, or `pairAgent({ type: 'user' })` | `createAgent` with the CI API key |
90
- | Permissions | flags and a per-project prompt | `KUBB_AGENT_ALLOW_*` | flags only |
91
- | Connection | `runConnection` | `runConnection` | `runConnection` |
92
- | Token rejected | pair again once | pair again once | fail the run |
93
- | Output | renders the `studio:*` hooks | renders the `studio:*` hooks | renders the `studio:*` hooks |
89
+ | Code | Meaning | `connectAgent` |
90
+ | ------ | --------------------------------------------- | ------------------------------------- |
91
+ | `4001` | The token is invalid, expired, or revoked | Rejects with `InvalidAgentTokenError` |
92
+ | `4002` | Another process of the same agent took over | Resolves |
93
+ | `4003` | The agent is older than Studio accepts | Rejects with `IncompatibleAgentError` |
94
+ | other | An ordinary drop, `1011` for a Studio failure | Reconnects with jittered backoff |
94
95
 
95
- Everything the runtime has to say goes through hooks on the emitter `installLogger` receives:
96
- `studio:connecting`, `studio:connected`, `studio:ready`, `studio:reconnecting`,
97
- `studio:disconnected`, `studio:command:start`, `studio:command:end`, `studio:warn`, and
98
- `studio:error`. When a request is refused for a missing permission, `studio:warn` carries it as
99
- `permission`, and the host adds its own remedy, a CLI flag or an environment variable.
96
+ Everything the runtime has to say goes through the `hooks` emitter: `studio:connected`,
97
+ `studio:disconnected`, `studio:reconnecting`, `studio:command`, `studio:warn`, and `studio:error`,
98
+ next to the `kubb:*` hooks of each generation. When a request is refused for a missing permission,
99
+ `studio:warn` names it as `permission`, so the host can add its own remedy.
100
100
 
101
101
  ## Permissions
102
102
 
@@ -109,135 +109,46 @@ Every permission is off by default, and each covers one trust boundary:
109
109
  | `allowConfigEdit` | Studio may change plugin options in `kubb.config.ts`. |
110
110
  | `allowRead` | Studio may read back the files a generation produced. |
111
111
 
112
- ## Connection flow
113
-
114
- Both hosts follow the same steps and send the agent's bearer token with every call. `/api/agent` is
115
- the machine-facing surface, singular because the caller is describing itself. The plural
116
- `/api/agents` is the collection a signed-in user manages in the browser, and the runtime never
117
- touches it.
118
-
119
- | Step | Call | What it does |
120
- | -------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
121
- | Register | `POST /api/agent/connect` | Binds the token to this machine and process (`machineToken`, `instanceId`), reports `capacity`, and returns `socketUrl` |
122
- | Connect | Configured RPC connector on `socketUrl` | Opens the process's one socket with the bearer token and `x-kubb-instance-id`, then attaches the typed `AgentApi`/`StudioApi` RPC session |
123
-
124
- Every connection attempt registers first, so a reconnect is also how the agent registers again.
125
- Each heartbeat carries the agent's load. A process keeps one socket, not one per job: Studio
126
- schedules jobs onto it up to the advertised `maxConcurrent`. Studio's close codes say whether to
127
- come back: `4001` registers and reconnects, `4002` (another instance took over) and `4003` (the
128
- agent is too old or was deleted) stay down. A `426` at registration means the agent is too old for
129
- that Studio.
130
-
131
- `runConnection` owns registration retries, capped reconnect backoff, credential replacement, and
132
- shutdown. `StudioSession` serves one attempt and waits for canceled generation work to settle before
133
- the loop reconnects. `createClient` remains a thin compatibility facade over that loop.
134
-
135
- The runtime reconnects when the socket drops and keeps retrying while Studio is unreachable. Generation progress is a native Cap'n Web `ReadableStream` on the generation
136
- capability; durable job status is read through the HTTP job API.
112
+ A sandbox never writes or edits config, and always allows reads.
137
113
 
138
- ## Studio job events
139
-
140
- `@kubb/studio` exposes a deliberately small public event API. Every live event uses the envelope
141
- `{ version: 1, jobId, type, data, timestamp }`. The native stream preserves order and applies
142
- backpressure; it is not a replay cursor. Job status and the terminal result remain authoritative
143
- after a reconnect.
144
-
145
- The stable catalog is `generationEventTypes`: generation and build progress, file processing,
146
- plugin progress, log levels, diagnostics, command-hook output, and the terminal generation summary.
147
- Each `type` is a lifecycle name registered by `@kubb/core`, while `data` is the JSON-safe projection
148
- defined by `GenerationEventPayloads`. AST traversal, live config and adapter objects, storage, and
149
- plugin implementations never leave the agent. New core hooks stay private until Studio explicitly
150
- adds their name and serializer projection.
151
-
152
- ### Pairing
114
+ ## Pairing
153
115
 
154
116
  No host starts with a token, so each one pairs over
155
- [RFC 8628](https://www.rfc-editor.org/rfc/rfc8628.html) device authorization: it asks
156
- `POST /api/auth/device/code` for a `device_code` and a short `user_code`, shows the code, and polls
157
- `POST /api/agent/token` until someone approves in the browser. Studio mints the token once and stores only its hash, so
158
- nothing can read it back.
159
-
160
- `pairAgent` runs that whole flow and hands each code to the host's `onCode` to show. The `type` is
161
- what Studio registers the machine as: `cli` is a `kubb studio` machine any signed-in member can
162
- approve, and `user` or `sandbox` is the Docker image, whose codes only an admin can approve. With
163
- `maxAttempts` above 1 it asks for a fresh code when one expires unapproved. A denial throws
164
- `PairingDeniedError`, an expired code `PairingExpiredError`, and an aborted `signal`
117
+ [RFC 8628](https://www.rfc-editor.org/rfc/rfc8628.html) device authorization. `pairAgent` asks Studio
118
+ for a code, hands it to `onCode` to show, and polls until someone approves it in the browser.
119
+
120
+ `machineId` is a secret the host keeps for the machine. Studio stores its hash, so the same machine
121
+ pairing again rotates one agent's token instead of creating a second agent. Studio records the
122
+ `type` with the agent. `cli` is `kubb studio`, which any member can approve. `user` and `sandbox`
123
+ are the Docker image, run normally or as a sandbox. Only an admin can approve them. A denial
124
+ throws `PairingDeniedError`, an expired code `PairingExpiredError`, and an aborted `signal`
165
125
  `PairingCanceledError`.
166
126
 
167
- ```typescript
168
- import { pairAgent } from '@kubb/studio'
169
-
170
- const { token, agent } = await pairAgent({
171
- type: 'cli',
172
- name: 'my-project',
173
- hostname: os.hostname(),
174
- onCode: (session) => console.log(`Approve ${session.user_code} at ${session.verification_uri}`),
175
- })
176
- ```
177
-
178
- ## Asynchronous jobs
179
-
180
- CI and automation queue work with `createJob` and poll with `waitForJob`. Both send the
181
- organization CI API key as `x-api-key`. They do not open a WebSocket.
127
+ ## CI snapshots
182
128
 
183
- | Step | Call | What it does |
184
- | ------ | -------------------- | --------------------------------------------------------------------- |
185
- | Queue | `POST /api/jobs` | Accepts a `generation` or `snapshot` job and returns `202` with an id |
186
- | Status | `GET /api/jobs/{id}` | Returns the job until `success`, `failed`, or `canceled` |
129
+ `createSnapshot` generates the project like `kubb generate`, with the formatter, the linter, and
130
+ `output.postGenerate`. It then packs the output as an npm tarball and sends one
131
+ `POST /api/snapshots` with the organization CI API key as `x-api-key`.
187
132
 
188
133
  ```typescript
189
- import { createJob, waitForJob } from '@kubb/studio'
190
-
191
- const job = await createJob({
192
- studioUrl: 'https://kubb.studio',
193
- token: process.env.KUBB_TOKEN!,
194
- type: 'snapshot',
195
- agentId: agent.id,
196
- name: '@scope/package',
134
+ import { createSnapshot } from '@kubb/studio'
135
+
136
+ const snapshot = await createSnapshot({
137
+ apiKey: process.env.KUBB_TOKEN!,
138
+ config,
139
+ id: 'gh:123:pr-4',
140
+ name: 'acme/api#4',
141
+ packageName: '@acme/api',
197
142
  version: '1.0.0',
143
+ commit: process.env.GITHUB_SHA,
144
+ baseId: 'gh:123:refs/heads/main',
198
145
  })
199
-
200
- const finished = await waitForJob({
201
- studioUrl: 'https://kubb.studio',
202
- token: process.env.KUBB_TOKEN!,
203
- id: job.id,
204
- })
205
-
206
- if (finished.status === 'failed') throw new Error(finished.error)
207
- if (finished.status === 'canceled') throw new Error('Studio job was canceled')
208
- const snapshot = finished.snapshot
209
- ```
210
-
211
- Pass `commit` with a snapshot job, and the finished snapshot carries `changes`: the files added,
212
- changed, and removed since the previous snapshot of the same package on the same agent, and which
213
- snapshot (and commit) that was. `base` is `null` on the first one.
214
-
215
- Pass `baseId` (the `id` another CI agent's runs register under) and the snapshot also carries
216
- `branchChanges`: the same comparison against that agent's latest snapshot. On a GitHub pull
217
- request or a GitLab merge request, `kubb studio snapshot` passes the agent its base branch's runs
218
- use. Elsewhere, pass `--base-id` with the `--id` those runs use.
219
-
220
- Runs that share an agent (one pull request, or one branch) must not overlap, since registering it
221
- again ends the other run's session. Serialize them per ref, such as a GitHub Actions `concurrency`
222
- group on `github.ref` or a GitLab `resource_group` on `$CI_COMMIT_REF_SLUG`.
223
-
224
- A snapshot job packs the tarball on the agent, not on Studio. The agent `PUT`s an empty request to
225
- a path Studio provides, gets back a redirect to a short-lived storage URL, and uploads the tarball
226
- there. The storage URL never crosses the RPC socket.
227
-
228
- ## Protocol
229
-
230
- `@kubb/studio/protocol` holds the RPC contracts both ends share, so the agent and Studio compile
231
- against one definition rather than two hand-maintained copies. Its only import is `KubbHooks` from
232
- `@kubb/core`, which the published event names are checked against.
233
-
234
- ```typescript
235
- import type { AgentApi, GenerationEvent, GenerationRun, RpcConnection, RpcConnector, StudioApi } from '@kubb/studio'
236
146
  ```
237
147
 
238
- Hosts supply an `RpcConnector`, which keeps transport details in the CLI, Docker agent, or Studio
239
- host rather than in the generation runtime. A `GenerationRun.cancel()` call aborts the matching
240
- generation cooperatively, including configured formatter, linter, and `postGenerate` processes.
148
+ Snapshots that share an `id` belong to one CI agent in Studio. `changes` lists the files added,
149
+ changed, and removed since that agent's previous snapshot of the package. With `baseId`,
150
+ `branchChanges` compares against the latest snapshot of the CI agent with that id, such as the base
151
+ branch's.
241
152
 
242
153
  ## Supporting Kubb
243
154