@kubb/studio 5.3.15 → 5.3.17

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
@@ -40,32 +40,67 @@ npm install @kubb/studio
40
40
  ## Usage
41
41
 
42
42
  ```typescript
43
- import { createClient, createFileStorage } from '@kubb/studio'
44
-
45
- const client = createClient({
46
- token: process.env.KUBB_AGENT_TOKEN!,
47
- configPath: 'kubb.config.ts',
48
- version: '1.0.0',
49
- loadConfig: () => loadMyKubbConfig(),
50
- storage: createFileStorage('./.kubb-cache'),
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,
51
64
  })
52
-
53
- await client.connect()
54
65
  ```
55
66
 
56
67
  The runtime discovers nothing on its own: the host injects the config loader, the storage, and its
57
68
  own version. That is what lets one runtime serve a CLI running in a developer's project and a
58
69
  container running a fixed plugin set.
59
70
 
71
+ ## Hosts and the runtime
72
+
73
+ Three hosts run this package: `kubb studio`, the `kubblabs/kubb-agent` Docker image, and
74
+ `kubb studio snapshot` in CI. All three follow the steps below. The runtime owns only the
75
+ connection: it doesn't read CI environment variables or print anything, and behaves the same in
76
+ every host.
77
+
78
+ | Step | `kubb studio` | Docker agent | `kubb studio snapshot` |
79
+ | ---------------- | --------------------------------------- | ---------------------------------------- | ---------------------------------------- |
80
+ | Machine identity | `setStorage` under `~/.kubb` | `setStorage` on the Nitro `kubb` mount | `KUBB_AGENT_SECRET` from the CI identity |
81
+ | Credentials | stored, or `pairAgent({ type: 'cli' })` | stored, or `pairAgent({ type: 'user' })` | `createAgent` with the CI API key |
82
+ | Permissions | flags and a per-project prompt | `KUBB_AGENT_ALLOW_*` | flags only |
83
+ | Connection | `runConnection` | `runConnection` | `runConnection` |
84
+ | Token rejected | pair again once | pair again once | fail the run |
85
+ | Output | renders the `studio:*` hooks | renders the `studio:*` hooks | renders the `studio:*` hooks |
86
+
87
+ Everything the runtime has to say goes through hooks on the emitter `installLogger` receives:
88
+ `studio:connecting`, `studio:connected`, `studio:ready`, `studio:reconnecting`,
89
+ `studio:disconnected`, `studio:command:start`, `studio:command:end`, `studio:warn`, and
90
+ `studio:error`. When a request is refused for a missing permission, `studio:warn` carries it as
91
+ `permission`, and the host adds its own remedy, a CLI flag or an environment variable.
92
+
60
93
  ## Permissions
61
94
 
62
95
  Every permission is off by default, and each covers one trust boundary:
63
96
 
64
- | Option | What it grants |
65
- | ------------ | ---------------------------------------------------------------------------------------------- |
66
- | `allowWrite` | Generated files are written to disk. Off means they exist only in memory and stream to Studio. |
67
- | `allowInput` | An OpenAPI spec sent by Studio replaces the one on disk. |
68
- | `allowExec` | The formatter, the linter, and `output.postGenerate` run as child processes. |
97
+ | Option | What it grants |
98
+ | ----------------- | ---------------------------------------------------------------------------------------------- |
99
+ | `allowWrite` | Generated files are written to disk. Off means they exist only in memory and stream to Studio. |
100
+ | `allowInput` | An OpenAPI spec sent by Studio replaces the one on disk. |
101
+ | `allowExec` | The formatter, the linter, and `output.postGenerate` run as child processes. |
102
+ | `allowConfigEdit` | Studio may change plugin options in `kubb.config.ts`. |
103
+ | `allowRead` | Studio may read back the files a generation produced. |
69
104
 
70
105
  ## Connection flow
71
106
 
@@ -74,14 +109,19 @@ the machine-facing surface, singular because the caller is describing itself. Th
74
109
  `/api/agents` is the collection a signed-in user manages in the browser, and the runtime never
75
110
  touches it.
76
111
 
77
- | Step | Call | What it does |
78
- | ---------- | ------------------------------------------------- | ---------------------------------------------------------------------------------- |
79
- | Register | `POST /api/agent/connect` | Binds the token to this machine with a `machineToken`. A failure here is not fatal |
80
- | Session | `POST /api/agent/sessions` | Returns `{ url, sessionId, expiresAt }` |
81
- | Connect | Configured RPC connector on `url` | Attaches the typed `AgentApi`/`StudioApi` RPC session |
82
- | Disconnect | `POST /api/agent/sessions/{sessionId}/disconnect` | Closes the session on a clean shutdown |
112
+ | Step | Call | What it does |
113
+ | -------- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
114
+ | Register | `POST /api/agent/connect` | Binds the token to this machine and process (`machineToken`, `instanceId`), reports `capacity`, and returns `socketUrl` |
115
+ | 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 |
116
+
117
+ Every connection attempt registers first, so a reconnect is also how the agent registers again.
118
+ Each heartbeat carries the agent's load. A process keeps one socket, not one per job: Studio
119
+ schedules jobs onto it up to the advertised `maxConcurrent`. Studio's close codes say whether to
120
+ come back: `4001` registers and reconnects, `4002` (another instance took over) and `4003` (the
121
+ agent is too old or was deleted) stay down. A `426` at registration means the agent is too old for
122
+ that Studio.
83
123
 
84
- The runtime reconnects on its own when a session drops, and keeps retrying while Studio is
124
+ The runtime reconnects on its own when the socket drops, and keeps retrying while Studio is
85
125
  unreachable. Generation progress is a native Cap'n Web `ReadableStream` on the generation
86
126
  capability; durable job status is read through the HTTP job API.
87
127
 
@@ -107,9 +147,23 @@ No host starts with a token, so each one pairs over
107
147
  `POST /api/agent/token` until someone approves in the browser. Studio mints the token once and stores only its hash, so
108
148
  nothing can read it back.
109
149
 
110
- `startPairing` defaults to the `kubb-cli` client, which `kubb studio login` uses and any signed-in
111
- member can approve. A host that pairs a shared or tier-limited agent passes `clientId: 'kubb-agent'`
112
- and an `agentKind`, whose codes only an admin can approve.
150
+ `pairAgent` runs that whole flow and hands each code to the host's `onCode` to show. The `type` is
151
+ what Studio registers the machine as: `cli` is a `kubb studio` machine any signed-in member can
152
+ approve, and `user` or `sandbox` is the Docker image, whose codes only an admin can approve. With
153
+ `maxAttempts` above 1 it asks for a fresh code when one expires unapproved. A denial throws
154
+ `PairingDeniedError`, an expired code `PairingExpiredError`, and an aborted `signal`
155
+ `PairingCanceledError`.
156
+
157
+ ```typescript
158
+ import { pairAgent } from '@kubb/studio'
159
+
160
+ const { token, agent } = await pairAgent({
161
+ type: 'cli',
162
+ name: 'my-project',
163
+ hostname: os.hostname(),
164
+ onCode: (session) => console.log(`Approve ${session.user_code} at ${session.verification_uri}`),
165
+ })
166
+ ```
113
167
 
114
168
  ## Asynchronous jobs
115
169
 
@@ -144,6 +198,19 @@ if (finished.status === 'canceled') throw new Error('Studio job was canceled')
144
198
  const snapshot = finished.snapshot
145
199
  ```
146
200
 
201
+ Pass `commit` with a snapshot job, and the finished snapshot carries `changes`: the files added,
202
+ changed, and removed since the previous snapshot of the same package on the same agent, and which
203
+ snapshot (and commit) that was. `base` is `null` on the first one.
204
+
205
+ Pass `baseId` (the `id` another CI agent's runs register under) and the snapshot also carries
206
+ `branchChanges`: the same comparison against that agent's latest snapshot. On a GitHub pull
207
+ request or a GitLab merge request, `kubb studio snapshot` passes the agent its base branch's runs
208
+ use. Elsewhere, pass `--base-id` with the `--id` those runs use.
209
+
210
+ Runs that share an agent (one pull request, or one branch) must not overlap, since registering it
211
+ again ends the other run's session. Serialize them per ref, such as a GitHub Actions `concurrency`
212
+ group on `github.ref` or a GitLab `resource_group` on `$CI_COMMIT_REF_SLUG`.
213
+
147
214
  A snapshot job packs the tarball on the agent, not on Studio. The agent `PUT`s an empty request to
148
215
  a path Studio provides, gets back a redirect to a short-lived storage URL, and uploads the tarball
149
216
  there. The storage URL never crosses the RPC socket.