unfazed 0.6.1 → 0.8.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,88 @@
1
+ # Changelog
2
+
3
+ ## 0.8.0
4
+
5
+ Fixes from a hands-on Code Mode test (2026-10-22), and the MCP tools the SDK
6
+ lacked. Needs the platform change that adds `writeWorkspaceFiles`,
7
+ `waitForDeployment`, the error and event queries and the new
8
+ `restoreAppVersion` fields; on an older platform `writeFiles` and the deploy
9
+ wait fall back to their 0.7 behaviour.
10
+
11
+ **Breaking**
12
+
13
+ - `writeFiles` returns a `WriteFilesResult` (`{ files, written, failed,
14
+ totalBytes, commit, commits, landed? }`) instead of `WorkspaceFile[]`, and a
15
+ file that fails throws `UnfazedWriteError` (its `result` and `failed`)
16
+ instead of `UnfazedGraphQLError`; `allowPartial: true` returns the result
17
+ instead. Its defaults are now 100 files and 32 MiB per request (were 20 and
18
+ 4 MiB).
19
+ - `getEvents` returns `DeploymentEvent[]` whose `type` is the event's own
20
+ dotted name (`"deployment.build_finished"`, `"service.vm_started"`), as MCP
21
+ shows it. The GraphQL enum moved to `typeEnum`; `rawType` stays, deprecated.
22
+
23
+ **Changed**
24
+
25
+ - `writeFiles` is ONE request and ONE commit on an app with platform git (the
26
+ `writeWorkspaceFiles` mutation MCP's `write_workspace_files` uses), with
27
+ `commitMessage`, and answers the commit: `sha`, `committed`, `waiting`,
28
+ `waitingCount`, `deploys`, `conflicts`, `writeSHA`, `paused`. A map over one
29
+ request's limits is several requests, one commit each. `writeFile` and
30
+ `writeBinaryFile` are one-file batches and answer the file with its
31
+ `commit`.
32
+ - `deployService/deployStatic/deployBox/deploy({ wait: true })` and
33
+ `waitForDeployment` wait server-side (MCP `wait_for_deployment`): READY comes
34
+ back one round trip after it happens, with the full deployment, no poll
35
+ interval and no re-read. The 9.0 s vs 6.8 s gap in the report was the old
36
+ 1 s × 1.5 poll backoff (READY seen at the 8.1 s poll) plus the full re-read.
37
+ The polling fallback starts at 0.5 s and caps at 3 s.
38
+ - A deploy with `wait: true` carries `timings`: `platform` (`totalMs`,
39
+ `buildMs`, `promoteMs`, every measured phase as numbers) and `client`
40
+ (`createMs`, `waitMs`, `totalMs`, `waitRequests`, `waitMode`).
41
+ `timings: false` skips the extra read.
42
+ - The platform holds at most 16 long waits of one account at once (and one
43
+ per GraphQL request). A deploy wait refused past that polls instead;
44
+ `waitForEvents` throws an error `isTooManyWaits` recognizes.
45
+
46
+ **Added**
47
+
48
+ - `help(topic?)` and `REFERENCE`: every method with its signature, in one
49
+ page (REFERENCE.md, shipped in the package).
50
+ - `waitForCommit(workspaceId)` and `writeFiles(…, { waitForCommit })`; the
51
+ saved / committed / deployed states are documented.
52
+ - History: `listCommits`, `getCommit`, `restoreVersion` (with `wait: true` it
53
+ follows the import the restore queues to its READY deployments).
54
+ - Errors: `listErrors`, `getErrorGroup`, `resolveErrorGroup`,
55
+ `muteErrorGroup`, `reopenErrorGroup`.
56
+ - Events: `listEventTypes`, `pollEvents`, `waitForEvents`.
57
+ - `getDeploymentTimings(id)` and `parseDeploymentTimings(events)`.
58
+
59
+ ## 0.7.0
60
+
61
+ `deleteApp`, and `deleteWorkspace` names every app it takes offline
62
+ (`confirmAliases`, `liveApps`).
63
+
64
+ ## 0.6.1
65
+
66
+ `npx unfazed login`, and the SDK uses the stored login.
67
+
68
+ ## 0.6.0
69
+
70
+ `unfazed/workflow`: the unfazed World for Workflow SDK; `--backend unfazed`
71
+ for Workflow SDK's CLI.
72
+
73
+ ## 0.5.0
74
+
75
+ Runs: access mode, app alias, integrations on a run, `listRunHarnesses`.
76
+
77
+ ## 0.4.0
78
+
79
+ Runs: `verify` commands and the harness result.
80
+
81
+ ## 0.3.0
82
+
83
+ Agent runs.
84
+
85
+ ## 0.2.0
86
+
87
+ `ensureWorkspace`, `ignoreNotFound`; `deleteWorkspace` fixed; a harder
88
+ `rollback`.
package/README.md CHANGED
@@ -119,8 +119,8 @@ const unfazed = new Unfazed();
119
119
 
120
120
  const ws = await unfazed.createWorkspace("greeter");
121
121
 
122
- // One request, not three — batched as aliased mutations.
123
- await unfazed.writeFiles(ws.id, {
122
+ // One request, not three — and ONE commit on an app with platform git.
123
+ const written = await unfazed.writeFiles(ws.id, {
124
124
  "server.ts": `
125
125
  export default {
126
126
  fetch(req: Request) {
@@ -131,7 +131,8 @@ await unfazed.writeFiles(ws.id, {
131
131
  `,
132
132
  "unfazed.json": JSON.stringify({ entry: "server.ts" }),
133
133
  "README.md": "# greeter",
134
- });
134
+ }, { commitMessage: "First version" });
135
+ console.log(written.commit?.sha ?? "no platform git");
135
136
 
136
137
  try {
137
138
  // Blocks until the build, VM boot and health probe all pass.
@@ -141,6 +142,8 @@ try {
141
142
  wait: true,
142
143
  });
143
144
  console.log(`live: ${app.url}`);
145
+ // What the platform measured, and what this script waited.
146
+ console.log(app.timings?.platform?.totalMs, app.timings?.client.totalMs);
144
147
 
145
148
  // Verify it for real before trusting the alias.
146
149
  const res = await fetch(`${app.url}?name=agent`);
@@ -164,6 +167,17 @@ try {
164
167
  }
165
168
  ```
166
169
 
170
+ ## Every signature, from a script: `help()`
171
+
172
+ `help()` returns [REFERENCE.md](REFERENCE.md): every method with its
173
+ signature, result and an example, in one page. `help("writeFiles")` is one
174
+ entry, `help("Events")` one section. A Code Mode script (MCP `run_script`) can
175
+ `export default () => help("restoreVersion")` to read it without leaving the
176
+ tool. The platform serves the reference of the version it vendors at
177
+ https://unfazed.dev/sdk.md; a run_script result's `sdk_version` (or
178
+ `VERSION` in the script) names the version a script actually ran, which lags
179
+ for a while after a release (it ships with the platform's worker releases).
180
+
167
181
  ## Two layers
168
182
 
169
183
  ### 1. Typed GraphQL core — `UnfazedClient`
@@ -188,7 +202,8 @@ const data = await client.query<{ workspaces: { id: string }[] }, never>(
188
202
  Errors are typed and specific: `UnfazedAuthError`, `UnfazedHTTPError` (carries
189
203
  `status`/`body`), `UnfazedGraphQLError` (carries `errors` **and any partial
190
204
  `data`**), `UnfazedTimeoutError`, `UnfazedDeployError`, `UnfazedConflictError`,
191
- `UnfazedNotFoundError`. All extend `UnfazedError`.
205
+ `UnfazedNotFoundError`, `UnfazedWriteError` (carries the write's `result` and
206
+ the `failed` paths). All extend `UnfazedError`.
192
207
 
193
208
  ### 2. Workflow layer — `Unfazed`
194
209
 
@@ -206,14 +221,17 @@ The chains an agent actually performs. Reach `client` from it any time:
206
221
  | `createWorkspace(name)` | names are unique per owner, case-insensitively |
207
222
  | `ensureWorkspace(name)` | get-or-create; **use this in re-runnable scripts** |
208
223
  | `listWorkspaces()` | |
209
- | `deleteWorkspace(id, opts?)` | cascades to deployments by default; also destroys a BOX's data disk |
224
+ | `liveApps(id)` | the apps the workspace serves now: what a cascading delete takes offline |
225
+ | `deleteApp({ workspaceId, alias }, opts?)` | deletes ONE app; the workspace and its other apps keep serving |
226
+ | `deleteWorkspace(id, opts?)` | with deployments only via `{ withDeployments: true, confirmAliases }` naming every live app; also deletes a BOX's data disk |
210
227
 
211
228
  **Files**
212
229
  | Method | Notes |
213
230
  | --- | --- |
214
- | `writeFiles(wsId, files, opts?)` | **batched** — many files per request |
215
- | `writeFile(wsId, path, text)` | |
216
- | `writeBinaryFile(wsId, path, bytes)` | base64 under the hood |
231
+ | `writeFiles(wsId, files, opts?)` | **one request, one commit** (MCP `write_workspace_files`); `commitMessage`; answers the `commit` |
232
+ | `writeFile(wsId, path, text, opts?)` | a one-file `writeFiles`; answers the file and its `commit` |
233
+ | `writeBinaryFile(wsId, path, bytes, opts?)` | base64 under the hood |
234
+ | `waitForCommit(wsId)` | resolves once no write waits for its commit (see "Saved, committed, deployed") |
217
235
  | `listFiles(wsId, page?)` | |
218
236
  | `readFile(wsId, path, range?)` | |
219
237
  | `deleteFile(wsId, path)` | |
@@ -221,11 +239,11 @@ The chains an agent actually performs. Reach `client` from it any time:
221
239
  **Deploy**
222
240
  | Method | Notes |
223
241
  | --- | --- |
224
- | `deployService(opts)` | `wait: true` polls to READY |
242
+ | `deployService(opts)` | `wait: true` waits server-side to READY; the result carries `timings` |
225
243
  | `deployStatic(opts)` | `spaMode` for single-page apps |
226
244
  | `deployBox(opts)` | persistent Linux VM; result carries `sshCommand` |
227
245
  | `deploy(input, opts?)` | raw `CreateDeploymentInput` escape hatch |
228
- | `waitForDeployment(id, opts?)` | standalone poller |
246
+ | `waitForDeployment(id, opts?)` | the server-side wait (MCP `wait_for_deployment`); polls on older platforms |
229
247
  | `getDeployment(id)` / `getDeploymentByAlias(alias)` | |
230
248
  | `listDeployments(filter?)` | |
231
249
 
@@ -234,7 +252,8 @@ The chains an agent actually performs. Reach `client` from it any time:
234
252
  | --- | --- |
235
253
  | `getLogs(id, { tailBytes })` | stdout + stderr tail |
236
254
  | `getStatus(id)` | health, in-flight, restarts, `memSizeMiB` |
237
- | `getEvents(id, limit?)` | build/boot/probe/cron lifecycle events |
255
+ | `getEvents(id, limit?)` | lifecycle events; `type` is the dotted name MCP shows (`deployment.build_finished`), `typeEnum` the GraphQL enum |
256
+ | `getDeploymentTimings(id)` | the platform's measured phases, as numbers |
238
257
  | `restartService(id)` / `stopService(id)` | |
239
258
  | `deleteDeployment(id, opts?)` | `ignoreNotFound` for idempotent cleanup |
240
259
 
@@ -253,6 +272,27 @@ The chains an agent actually performs. Reach `client` from it any time:
253
272
  | `listReleases(alias)` | newest first; one entry has `aliasHeld: true` |
254
273
  | `rollback({ alias, to?, expectedCurrentId?, retries? })` | **whole compare-and-swap in one call** |
255
274
 
275
+ **History (platform git)**
276
+ | Method | Notes |
277
+ | --- | --- |
278
+ | `listCommits(wsId, { limit?, before? })` | newest first; `deployed` says what each alias runs (MCP `list_commits`) |
279
+ | `getCommit(wsId, sha)` | the changed paths and a unified diff (MCP `get_commit`) |
280
+ | `restoreVersion({ workspaceId, sha, wait? })` | one new commit with that commit's files; `wait: true` follows its deploy to READY (MCP `restore_version`) |
281
+
282
+ **App errors**
283
+ | Method | Notes |
284
+ | --- | --- |
285
+ | `listErrors({ workspaceId, alias, status? })` | a service app's error groups (MCP `list_errors`) |
286
+ | `getErrorGroup({ workspaceId, alias, groupId })` | the latest occurrence whole, and where it was seen |
287
+ | `resolveErrorGroup` / `muteErrorGroup` / `reopenErrorGroup` | same ref; only on your human's say-so |
288
+
289
+ **Events**
290
+ | Method | Notes |
291
+ | --- | --- |
292
+ | `listEventTypes()` | every type with its filters and payload |
293
+ | `pollEvents({ names?, arguments?, cursor? })` | MCP `poll_events`; the same cursors |
294
+ | `waitForEvents({ …, timeoutMs? })` | blocks server-side until an event arrives (MCP `wait_for_events`) |
295
+
256
296
  **Agent runs**
257
297
  | Method | Notes |
258
298
  | --- | --- |
@@ -269,25 +309,53 @@ The chains an agent actually performs. Reach `client` from it any time:
269
309
 
270
310
  ## The two things worth knowing
271
311
 
272
- ### `writeFiles` batches, because the API is one-file-per-mutation
312
+ ### `writeFiles` is one request and one commit
273
313
 
274
- Writing a 12-file app the obvious way costs 12 round trips. `writeFiles` puts
275
- one **aliased** mutation per file into a single GraphQL document:
276
-
277
- ```graphql
278
- mutation SdkWriteFilesBatch($workspaceID: ID!, $p0: String!, $c0: String!, ...) {
279
- f0: writeWorkspaceTextFile(input: { workspaceID: $workspaceID, path: $p0, text: $c0 }) { file { ... } }
280
- f1: writeWorkspaceBase64File(input: { workspaceID: $workspaceID, path: $p1, contentBase64: $c1 }) { file { ... } }
281
- }
282
- ```
314
+ `writeFiles` sends every file in one `writeWorkspaceFiles` mutation, the one
315
+ MCP's `write_workspace_files` uses. Text and binary mix freely. On an app with
316
+ platform git the whole call is ONE commit, made before any file is written,
317
+ with your `commitMessage`; the answer carries it:
283
318
 
284
- Text and binary mix freely in one batch. It splits automatically at 20 files or
285
- 4 MiB per request (`batchSize`, `maxBatchBytes`).
286
-
287
- Batches are **not atomic** — GraphQL resolves each field independently. If one
288
- path fails, the others still commit; the thrown `UnfazedGraphQLError` carries
289
- `data` with the aliases that succeeded and `errors[].path` naming the ones that
290
- didn't.
319
+ ```ts
320
+ const out = await unfazed.writeFiles(ws.id, { "server.ts": code, "package.json": pkg }, { commitMessage: "Add the API" });
321
+ out.commit; // { sha, branch, message, committed, waiting, waitingCount, deploys, conflicts, writeSHA, paused }
322
+ ```
323
+
324
+ A call is **not transactional**, exactly as MCP's: files are written in order,
325
+ each atomically, and one failing does not undo the others. Then `writeFiles`
326
+ throws `UnfazedWriteError`, whose `result` lists what landed and `failed` what
327
+ did not — retry only those, or pass `allowPartial: true` to get the result
328
+ instead. A request carries up to 100 files and 32 MiB (the platform's limits);
329
+ a bigger map is split, one commit per request (`commits`). On a platform older
330
+ than the batch mutation the SDK falls back to one aliased mutation per file in
331
+ one request (one commit each, `commit: null`).
332
+
333
+ ### Saved, committed, deployed
334
+
335
+ - **Saved** — `writeFiles` returned: the app's files have it; the next deploy
336
+ uses it.
337
+ - **Committed** — `commit.committed && !commit.waiting`: it is in the app's
338
+ history (`listCommits`). When the git server is slow the commit comes back
339
+ `waiting` (`sha: null`): saved, landing in order, nothing to retry.
340
+ `writeFiles(…, { waitForCommit: true })` or `waitForCommit(wsId)` resolves
341
+ once it has landed, with the branch head.
342
+ - **Deployed** — a deploy of those files went READY: `deployService({ wait:
343
+ true })`. A write deploys by itself only when `commit.deploys` is true (the
344
+ branch head is imported like a push).
345
+
346
+ ### Waiting is server-side, and you get both clocks
347
+
348
+ `deployService({ wait: true })` and `waitForDeployment` wait on the platform
349
+ (MCP `wait_for_deployment`): each request blocks until the deployment leaves
350
+ BUILDING — woken by the deploy pipeline — or 45 s pass, and answers with the
351
+ full deployment. READY reaches the script one round trip after it happens.
352
+ Before 0.8.0 the SDK polled at 1 s × 1.5 backoff and re-read the deployment in
353
+ full: a deploy READY after 6.8 s was seen at about 8.1 s and returned at 9.0 s.
354
+
355
+ The result carries `timings`: `platform` is what the platform measured
356
+ (`totalMs` from creation to READY, `buildMs`, `promoteMs`, and every phase in
357
+ `phases`), `client` what the script waited (`createMs`, `waitMs`, `totalMs`,
358
+ `waitRequests`, `waitMode`). `timings: false` skips the one extra read.
291
359
 
292
360
  ### `rollback` does the compare-and-swap for you
293
361
 
@@ -361,25 +429,78 @@ what the first one did:
361
429
  | `deployBox({ workspaceId })` | `deployBox({ workspaceId, alias })` | no alias ⇒ a brand-new box each time |
362
430
  | `rollback({ alias })` | `rollback({ alias, to })` or `expectedCurrentId` | bare rollback is a toggle |
363
431
  | `deleteWorkspace(id)` throwing | `deleteWorkspace(id, { ignoreNotFound: true })` | already-deleted is not a failure |
432
+ | `deleteApp({ workspaceId, alias })` throwing | `deleteApp({ workspaceId, alias }, { ignoreNotFound: true })` | same; resolves `null` |
364
433
  | `deleteDeployment(id)` throwing | `deleteDeployment(id, { ignoreNotFound: true })` | same |
365
434
 
366
- `ignoreNotFound` matches the server's two exact sentinels — `workspace was not
367
- found` and `deployment was not found` — and nothing else. A message that merely
368
- *contains* "not found" still throws, so a real failure can't be mistaken for a
369
- successful teardown. (Both sentinels also cover "belongs to another tenant": the
370
- server reports the two identically so ids can't be probed.)
371
-
372
- ### Deleting a workspace cascades by default
373
-
374
- `deleteWorkspace` sends `withDeployments: true` unless you say otherwise. It has
375
- to: the server **refuses** to delete a workspace with any deployment history at
376
- all — including FAILED-only history — without that flag, so the strict form
377
- fails on nearly every workspace that was ever deployed.
435
+ `ignoreNotFound` matches the server's exact refusals — `workspace was not
436
+ found`, `deployment was not found`, and for `deleteApp` `app "<alias>" was not
437
+ found in workspace "…"` naming that alias — and nothing else. A message that
438
+ merely *contains* "not found" still throws, so a real failure can't be mistaken
439
+ for a successful teardown. (The two sentinels also cover "belongs to another
440
+ tenant": the server reports the two identically so ids can't be probed.)
441
+
442
+ ### Deleting names what goes
443
+
444
+ A workspace is a source, and one source can serve several apps: each
445
+ `unfazed.json` `apps[]` entry, or several aliases deployed into it. A delete is
446
+ never wider than what you name:
447
+
448
+ - **`deleteApp({ workspaceId, alias })` deletes one app**: every deployment of
449
+ the alias in that workspace (its VM stops, its URL and custom domains stop
450
+ serving), its App State, its app-scoped env and secrets, and its integration
451
+ bindings. The workspace, its files and defaults, its other apps and a box's
452
+ data disk stay. The result says what went (`deployments`, `domains`,
453
+ `envEntries`) and what still serves (`otherApps`). In a git-linked workspace
454
+ pushes and syncs skip the alias while `unfazed.json` `apps[]` still declares
455
+ it (`skippedOnSync: true`); remove it from `apps[]`, or bring it back with
456
+ the GraphQL `syncRepo` mutation's `restoreApps`. A delete that stops
457
+ part-way throws saying so; deleting again finishes it.
458
+ - **`deleteWorkspace(id, { withDeployments: true, confirmAliases })` deletes the
459
+ workspace with every app it serves**, so `confirmAliases` must name exactly
460
+ those apps, in any order. A missing or extra alias, or an app that went live
461
+ after the check, makes the server delete nothing and refuse with
462
+ `UnfazedUnconfirmedAppsError` (still an `UnfazedGraphQLError`), whose message
463
+ is the server's: it lists every live app with its custom domains. A
464
+ workspace whose deploys all failed serves no app: `confirmAliases: []`.
465
+ - **`deleteWorkspace(id)`** deletes only a workspace with no deployment records
466
+ left; otherwise the server refuses with `still has deployments; delete them
467
+ first`.
468
+
469
+ The SDK never fills `confirmAliases` in for you. `liveApps(id)` is there to look
470
+ the list up deliberately — read it, then name what you mean to take down:
378
471
 
379
472
  ```ts
380
- await unfazed.deleteWorkspace(ws.id); // cascade (default)
381
- await unfazed.deleteWorkspace(ws.id, { withDeployments: false }); // refuse if deployed
382
- ```
473
+ await unfazed.liveApps(ws.id);
474
+ // [{ alias: "api", deploymentID: "…", domains: ["api.example.com"] },
475
+ // { alias: "web", deploymentID: "…", domains: [] }]
476
+
477
+ // Either one app — "web" keeps serving:
478
+ await unfazed.deleteApp({ workspaceId: ws.id, alias: "api" });
479
+
480
+ // or the workspace with every app it serves, each named:
481
+ await unfazed.deleteWorkspace(ws.id, { withDeployments: true, confirmAliases: ["api", "web"] });
482
+
483
+ // Naming fewer (or more) deletes nothing and throws:
484
+ await unfazed.deleteWorkspace(ws.id, { withDeployments: true, confirmAliases: ["web"] });
485
+ // UnfazedUnconfirmedAppsError: unfazed: nothing was deleted: workspace "shop" (…) serves 2 apps:
486
+ // api (api.example.com); web. Deleting it takes every one of them offline. To delete one app,
487
+ // use deleteApp (MCP delete_app). To delete the workspace with all of them, name exactly these
488
+ // aliases in confirmAliases.
489
+ ```
490
+
491
+ **Deleted is kept for 30 days.** A delete takes effect at once: the app,
492
+ release or workspace disappears from your views, its name and alias are free
493
+ for anyone, and it stops serving. The platform keeps what you deleted — its
494
+ files, releases with their env and secrets, App State, box disk, integration
495
+ resources and custom domains — for 30 days. Within that time support (the
496
+ platform operator) can restore it exactly as it was; you cannot restore it
497
+ yourself, so ask support. After 30 days it is erased for good. This holds for
498
+ `deleteApp`, `deleteWorkspace` and `deleteDeployment` alike.
499
+
500
+ **Upgrading from 0.6.** `deleteWorkspace(id)` used to send `withDeployments:
501
+ true` with no apps named; the server now refuses that whenever the workspace
502
+ serves an app. From 0.7 the bare call is the strict delete, and a cascade takes
503
+ `{ withDeployments: true, confirmAliases }`. To remove one app, use `deleteApp`.
383
504
 
384
505
  ## Env & secrets: three states, not two
385
506
 
@@ -501,25 +622,38 @@ to the app as well.
501
622
 
502
623
  ### With Nitro (the recipe)
503
624
 
625
+ The app deploys in Nitro's own layout, as the platform's "Deploying a Nitro
626
+ app" describes, with no re-export file:
627
+
628
+ ```ts
629
+ // server.ts — the app itself; Nitro takes a root server.ts as its server entry
630
+ import { Hono } from "hono";
631
+
632
+ const app = new Hono();
633
+ // … routes that start() workflows
634
+ export default app;
635
+ ```
636
+
504
637
  ```ts
505
638
  // nitro.config.ts
506
639
  import { defineConfig } from "nitro";
507
640
 
508
641
  export default defineConfig({
509
642
  modules: ["workflow/nitro", "unfazed/workflow/nitro"],
510
- routes: { "/**": "./src/app.ts" }, // a Hono app; not named server.ts (Nitro claims that name)
511
643
  });
512
644
  ```
513
645
 
514
646
  ```json
515
- // unfazed.json — package.json has "build": "nitro build"
516
- { "kind": "SERVICE", "runtime": "BUN", "install": "bun install", "build": "bun run build", "entry": "unfazed-entry.ts" }
647
+ // unfazed.json — no "entry"
648
+ { "kind": "SERVICE", "build": "nitro build" }
517
649
  ```
518
650
 
519
- ```ts
520
- // unfazed-entry.ts
521
- export { default } from "./.output/server/index.mjs";
522
- ```
651
+ With no `entry` in unfazed.json and a `standard` Nitro build, unfazed serves
652
+ `.output/server/index.mjs`; the build log says `using Nitro output
653
+ .output/server/index.mjs as the service entry`. Do not set `entry` or add an
654
+ `unfazed-entry.ts` re-export: an explicit entry replaces the Nitro output.
655
+ Install and build commands find the app's `node_modules/.bin` CLIs on PATH, so
656
+ `"build": "bun run build"` over a package.json script works as well.
523
657
 
524
658
  Then bind a Postgres or Neon integration to the app and deploy. In a
525
659
  production build the module:
@@ -536,14 +670,16 @@ production build the module:
536
670
  - mounts the **inspection route** the unfazed dashboard and MCP read runs
537
671
  through (below), with the build's workflow list and Workflow SDK version;
538
672
  - sets `noExternals: true` and builds Nitro's `standard` output (`export
539
- default { fetch }`), which is what unfazed serves, whenever no preset was
673
+ default { fetch }`, recorded as `"preset": "standard"` in
674
+ `.output/nitro.json`), which is what unfazed serves, whenever no preset was
540
675
  chosen, whatever Nitro detected on the build machine: `bun` under Bun (as on
541
676
  the unfazed build VM), `node-server` under Node, `deno-server` under Deno.
542
677
  Their own outputs start a server of their own (under Bun with a 10 s idle
543
678
  timeout that cuts every quiet workflow stream). A preset chosen explicitly
544
679
  (`preset` in nitro.config.ts or .nitrorc, `NITRO_PRESET`, `SERVER_PRESET`)
545
680
  other than `standard` **fails the build** with a message saying so; leave it
546
- out, or set `preset: "standard"`.
681
+ out, or set `preset: "standard"`. A Nitro app without this module sets both
682
+ itself: `defineConfig({ preset: "standard", noExternals: true })`.
547
683
  - serves public assets only when `serveStatic: "inline"` embeds them in the
548
684
  bundle (a service VM has nothing next to it to read from disk). That is how
549
685
  one service ships an API and its UI (`publicAssets: [{ dir: "dist" }]`
@@ -817,6 +953,11 @@ After bumping the `@workflow/world-postgres` devDependency, run
817
953
  `bun run embed-migrations`: `bun run check` fails while
818
954
  `src/workflow/migrations.generated.ts` is stale.
819
955
 
956
+ `REFERENCE.md` is what `help()` returns: after editing it run
957
+ `bun run embed-reference` (`bun run check` fails while
958
+ `src/reference.generated.ts` is stale). `test/reference.test.ts` fails when a
959
+ public method of `Unfazed` is missing from it.
960
+
820
961
  ### Regenerating from the schema
821
962
 
822
963
  `schema/schema.graphqls` is a **vendored copy** of `graph/schema.graphqls` from
@@ -855,6 +996,14 @@ bun run check # typecheck + tests + build + dist smoke
855
996
  npm publish # or: bun publish --access public
856
997
  ```
857
998
 
999
+ Then bring the release to Code Mode: MCP `run_script` runs a copy of the SDK
1000
+ vendored in the unfazed repo, not npm's. There, `hack/update-script-sdk.sh
1001
+ <version>` vendors the published tarball (checked against npm's integrity),
1002
+ and a worker release ships it, since scripts run on workers (unfazed
1003
+ `docs/code-mode.md`, "Shipping a new SDK"). Its `script-sdk` check turns red
1004
+ within a day of a publish that was not vendored. A release that needs new
1005
+ platform fields goes out after the platform change that adds them.
1006
+
858
1007
  ## License
859
1008
 
860
1009
  MIT — see [LICENSE](./LICENSE).