unfazed 0.6.0 → 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 +88 -0
- package/README.md +299 -78
- package/REFERENCE.md +284 -0
- package/dist/cli/login.d.ts +129 -0
- package/dist/cli/login.js +596 -0
- package/dist/cli/login.js.map +1 -0
- package/dist/cli/workflow.d.ts +31 -0
- package/dist/cli/workflow.js +157 -0
- package/dist/cli/workflow.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +173 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +26 -5
- package/dist/client.js +93 -48
- package/dist/client.js.map +1 -1
- package/dist/credentials.d.ts +123 -0
- package/dist/credentials.js +317 -0
- package/dist/credentials.js.map +1 -0
- package/dist/errors.d.ts +64 -0
- package/dist/errors.js +95 -0
- package/dist/errors.js.map +1 -1
- package/dist/generated/gql.d.ts +78 -2
- package/dist/generated/gql.js +21 -2
- package/dist/generated/gql.js.map +1 -1
- package/dist/generated/graphql.d.ts +505 -6
- package/dist/generated/graphql.js +439 -2
- package/dist/generated/graphql.js.map +1 -1
- package/dist/index.d.ts +25 -6
- package/dist/index.js +23 -5
- package/dist/index.js.map +1 -1
- package/dist/operations.d.ts +116 -5
- package/dist/operations.js +379 -7
- package/dist/operations.js.map +1 -1
- package/dist/reference.d.ts +18 -0
- package/dist/reference.generated.d.ts +2 -0
- package/dist/reference.generated.js +5 -0
- package/dist/reference.generated.js.map +1 -0
- package/dist/reference.js +38 -0
- package/dist/reference.js.map +1 -0
- package/dist/remote/app.js +2 -1
- package/dist/remote/app.js.map +1 -1
- package/dist/remote/world.d.ts +2 -2
- package/dist/remote/world.js +32 -16
- package/dist/remote/world.js.map +1 -1
- package/dist/unfazed.d.ts +545 -68
- package/dist/unfazed.js +688 -64
- package/dist/unfazed.js.map +1 -1
- package/dist/wait.d.ts +7 -0
- package/dist/wait.js.map +1 -1
- package/dist/workflow/nitro.js +5 -3
- package/dist/workflow/nitro.js.map +1 -1
- package/package.json +22 -6
- package/schema/SOURCE.json +4 -4
- package/schema/schema.graphqls +1628 -30
package/README.md
CHANGED
|
@@ -20,8 +20,11 @@ SDK: one script. Three requests. You read the four lines you cared about.
|
|
|
20
20
|
- Zero runtime dependencies. Installing `unfazed` installs exactly one package.
|
|
21
21
|
(The optional [`unfazed/workflow`](#durable-workflows-unfazedworkflow) subpath
|
|
22
22
|
uses Workflow SDK packages your app installs; the root entry never loads them.)
|
|
23
|
-
- `npx
|
|
24
|
-
|
|
23
|
+
- `npx unfazed login` signs you in from a terminal in the browser; the SDK then
|
|
24
|
+
acts as you, with no token to copy ([details](#sign-in-from-a-terminal-npx-unfazed)).
|
|
25
|
+
- `npx unfazed workflow inspect runs --app <alias>`: Workflow SDK's CLI and web
|
|
26
|
+
UI read your app's runs through the platform, from any directory
|
|
27
|
+
([details](#inspect-runs-from-your-terminal)).
|
|
25
28
|
- Strict TypeScript, generated from the platform's own GraphQL schema.
|
|
26
29
|
- Bun-first, runs unchanged on Node >= 20 and Deno.
|
|
27
30
|
|
|
@@ -33,27 +36,79 @@ bun add unfazed # or: npm i unfazed
|
|
|
33
36
|
|
|
34
37
|
## Auth
|
|
35
38
|
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
**A person** signs in once from a terminal, in the browser
|
|
40
|
+
([`npx unfazed login`](#sign-in-from-a-terminal-npx-unfazed)); the SDK then acts
|
|
41
|
+
as them, with their apps, teams and claimed agents:
|
|
42
|
+
|
|
43
|
+
```sh
|
|
44
|
+
npx unfazed login
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**An agent or a CI job** uses a bearer token instead: an agent registers itself
|
|
48
|
+
at <https://unfazed.dev/auth.md> (`POST https://unfazed.dev/agents/register`).
|
|
49
|
+
The SDK reads `UNFZ_API_TOKEN`, which wins over a stored login:
|
|
38
50
|
|
|
39
51
|
```sh
|
|
40
52
|
export UNFZ_API_TOKEN="..."
|
|
41
53
|
```
|
|
42
54
|
|
|
43
55
|
```ts
|
|
44
|
-
const unfazed = new Unfazed();
|
|
45
|
-
const unfazed = new Unfazed({ token });
|
|
56
|
+
const unfazed = new Unfazed(); // UNFZ_API_TOKEN, else your `npx unfazed login`
|
|
57
|
+
const unfazed = new Unfazed({ token }); // or pass a token explicitly
|
|
46
58
|
```
|
|
47
59
|
|
|
48
60
|
Both endpoint and token are configurable. `UNFZ_API_URL` overrides the endpoint
|
|
49
|
-
(default `https://unfazed.dev/query
|
|
50
|
-
self-hosted control planes where only the host differs.
|
|
61
|
+
(default `https://unfazed.dev/query`, or the stored login's); `baseUrl` is a
|
|
62
|
+
convenience for self-hosted control planes where only the host differs.
|
|
51
63
|
|
|
52
64
|
```ts
|
|
53
65
|
new Unfazed({ token, baseUrl: "https://staging.example.com" }); // → /query
|
|
54
66
|
new Unfazed({ token, endpoint: "https://example.com/graphql" }); // exact
|
|
55
67
|
```
|
|
56
68
|
|
|
69
|
+
## Sign in from a terminal: `npx unfazed`
|
|
70
|
+
|
|
71
|
+
The package's `unfazed` bin signs a person in, the way `vercel login` does. It
|
|
72
|
+
runs from any directory (`npx` fetches the package when the directory has no
|
|
73
|
+
`unfazed`), on Node >= 20.16 or Bun, with no dependencies.
|
|
74
|
+
|
|
75
|
+
```sh
|
|
76
|
+
npx unfazed login # opens the browser; you sign in and allow access
|
|
77
|
+
npx unfazed whoami # who the SDK acts as here
|
|
78
|
+
npx unfazed logout # revokes the login where the server offers it, and deletes it
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
$ npx unfazed login
|
|
83
|
+
Opening your browser to sign in to https://unfazed.dev.
|
|
84
|
+
…
|
|
85
|
+
Signed in to https://unfazed.dev as ada (user).
|
|
86
|
+
The login is stored in /home/ada/.config/unfazed/credentials.json.
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
- **How:** OAuth 2.1 with PKCE against the platform's own authorization server,
|
|
90
|
+
the one MCP clients sign in through. The CLI registers itself once as a
|
|
91
|
+
public client (cached in `oauth-clients.json` beside the login), listens on
|
|
92
|
+
`http://127.0.0.1:<random port>/callback` for the answer, and asks for
|
|
93
|
+
`unfz:read unfz:deploy` (`--scope` narrows it).
|
|
94
|
+
- **The login** is `${XDG_CONFIG_HOME:-~/.config}/unfazed/credentials.json`,
|
|
95
|
+
mode 0600: the access token (an hour long), the refresh token (30 days, rotated
|
|
96
|
+
on every refresh), the expiry, the endpoint and who signed in. The SDK uses it
|
|
97
|
+
whenever no token is passed and `UNFZ_API_TOKEN` is unset, refreshes the access
|
|
98
|
+
token before it expires (and once when the platform refuses it), and writes
|
|
99
|
+
the rotated pair back for the next process. It is only ever sent to the
|
|
100
|
+
endpoint it was issued by; another `UNFZ_API_URL` needs its own
|
|
101
|
+
`UNFZ_API_URL=… npx unfazed login`. `new UnfazedClient({ login: false })`
|
|
102
|
+
ignores it.
|
|
103
|
+
- **A browser on another machine** (an SSH session, a box): the URL is printed
|
|
104
|
+
instead of opened. Sign in there, then paste the address the browser ends up
|
|
105
|
+
on (`http://127.0.0.1:…/callback?code=…`, which does not load there) into the
|
|
106
|
+
terminal. `--no-browser` prints the URL on your own machine too.
|
|
107
|
+
- **Another control plane:** `--endpoint https://staging.example.com` (or
|
|
108
|
+
`UNFZ_API_URL`).
|
|
109
|
+
- `whoami --json` prints the same as JSON; nothing the CLI prints ever carries
|
|
110
|
+
a token.
|
|
111
|
+
|
|
57
112
|
## Code mode: deploy, wait, tail logs, in one script
|
|
58
113
|
|
|
59
114
|
```ts
|
|
@@ -64,8 +119,8 @@ const unfazed = new Unfazed();
|
|
|
64
119
|
|
|
65
120
|
const ws = await unfazed.createWorkspace("greeter");
|
|
66
121
|
|
|
67
|
-
// One request, not three —
|
|
68
|
-
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, {
|
|
69
124
|
"server.ts": `
|
|
70
125
|
export default {
|
|
71
126
|
fetch(req: Request) {
|
|
@@ -76,7 +131,8 @@ await unfazed.writeFiles(ws.id, {
|
|
|
76
131
|
`,
|
|
77
132
|
"unfazed.json": JSON.stringify({ entry: "server.ts" }),
|
|
78
133
|
"README.md": "# greeter",
|
|
79
|
-
});
|
|
134
|
+
}, { commitMessage: "First version" });
|
|
135
|
+
console.log(written.commit?.sha ?? "no platform git");
|
|
80
136
|
|
|
81
137
|
try {
|
|
82
138
|
// Blocks until the build, VM boot and health probe all pass.
|
|
@@ -86,6 +142,8 @@ try {
|
|
|
86
142
|
wait: true,
|
|
87
143
|
});
|
|
88
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);
|
|
89
147
|
|
|
90
148
|
// Verify it for real before trusting the alias.
|
|
91
149
|
const res = await fetch(`${app.url}?name=agent`);
|
|
@@ -109,6 +167,17 @@ try {
|
|
|
109
167
|
}
|
|
110
168
|
```
|
|
111
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
|
+
|
|
112
181
|
## Two layers
|
|
113
182
|
|
|
114
183
|
### 1. Typed GraphQL core — `UnfazedClient`
|
|
@@ -133,7 +202,8 @@ const data = await client.query<{ workspaces: { id: string }[] }, never>(
|
|
|
133
202
|
Errors are typed and specific: `UnfazedAuthError`, `UnfazedHTTPError` (carries
|
|
134
203
|
`status`/`body`), `UnfazedGraphQLError` (carries `errors` **and any partial
|
|
135
204
|
`data`**), `UnfazedTimeoutError`, `UnfazedDeployError`, `UnfazedConflictError`,
|
|
136
|
-
`UnfazedNotFoundError
|
|
205
|
+
`UnfazedNotFoundError`, `UnfazedWriteError` (carries the write's `result` and
|
|
206
|
+
the `failed` paths). All extend `UnfazedError`.
|
|
137
207
|
|
|
138
208
|
### 2. Workflow layer — `Unfazed`
|
|
139
209
|
|
|
@@ -151,14 +221,17 @@ The chains an agent actually performs. Reach `client` from it any time:
|
|
|
151
221
|
| `createWorkspace(name)` | names are unique per owner, case-insensitively |
|
|
152
222
|
| `ensureWorkspace(name)` | get-or-create; **use this in re-runnable scripts** |
|
|
153
223
|
| `listWorkspaces()` | |
|
|
154
|
-
| `
|
|
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 |
|
|
155
227
|
|
|
156
228
|
**Files**
|
|
157
229
|
| Method | Notes |
|
|
158
230
|
| --- | --- |
|
|
159
|
-
| `writeFiles(wsId, files, opts?)` | **
|
|
160
|
-
| `writeFile(wsId, path, text)` | |
|
|
161
|
-
| `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") |
|
|
162
235
|
| `listFiles(wsId, page?)` | |
|
|
163
236
|
| `readFile(wsId, path, range?)` | |
|
|
164
237
|
| `deleteFile(wsId, path)` | |
|
|
@@ -166,11 +239,11 @@ The chains an agent actually performs. Reach `client` from it any time:
|
|
|
166
239
|
**Deploy**
|
|
167
240
|
| Method | Notes |
|
|
168
241
|
| --- | --- |
|
|
169
|
-
| `deployService(opts)` | `wait: true`
|
|
242
|
+
| `deployService(opts)` | `wait: true` waits server-side to READY; the result carries `timings` |
|
|
170
243
|
| `deployStatic(opts)` | `spaMode` for single-page apps |
|
|
171
244
|
| `deployBox(opts)` | persistent Linux VM; result carries `sshCommand` |
|
|
172
245
|
| `deploy(input, opts?)` | raw `CreateDeploymentInput` escape hatch |
|
|
173
|
-
| `waitForDeployment(id, opts?)` |
|
|
246
|
+
| `waitForDeployment(id, opts?)` | the server-side wait (MCP `wait_for_deployment`); polls on older platforms |
|
|
174
247
|
| `getDeployment(id)` / `getDeploymentByAlias(alias)` | |
|
|
175
248
|
| `listDeployments(filter?)` | |
|
|
176
249
|
|
|
@@ -179,7 +252,8 @@ The chains an agent actually performs. Reach `client` from it any time:
|
|
|
179
252
|
| --- | --- |
|
|
180
253
|
| `getLogs(id, { tailBytes })` | stdout + stderr tail |
|
|
181
254
|
| `getStatus(id)` | health, in-flight, restarts, `memSizeMiB` |
|
|
182
|
-
| `getEvents(id, limit?)` |
|
|
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 |
|
|
183
257
|
| `restartService(id)` / `stopService(id)` | |
|
|
184
258
|
| `deleteDeployment(id, opts?)` | `ignoreNotFound` for idempotent cleanup |
|
|
185
259
|
|
|
@@ -198,6 +272,27 @@ The chains an agent actually performs. Reach `client` from it any time:
|
|
|
198
272
|
| `listReleases(alias)` | newest first; one entry has `aliasHeld: true` |
|
|
199
273
|
| `rollback({ alias, to?, expectedCurrentId?, retries? })` | **whole compare-and-swap in one call** |
|
|
200
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
|
+
|
|
201
296
|
**Agent runs**
|
|
202
297
|
| Method | Notes |
|
|
203
298
|
| --- | --- |
|
|
@@ -214,25 +309,53 @@ The chains an agent actually performs. Reach `client` from it any time:
|
|
|
214
309
|
|
|
215
310
|
## The two things worth knowing
|
|
216
311
|
|
|
217
|
-
### `writeFiles`
|
|
312
|
+
### `writeFiles` is one request and one commit
|
|
218
313
|
|
|
219
|
-
|
|
220
|
-
|
|
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:
|
|
221
318
|
|
|
222
|
-
```
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
f1: writeWorkspaceBase64File(input: { workspaceID: $workspaceID, path: $p1, contentBase64: $c1 }) { file { ... } }
|
|
226
|
-
}
|
|
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 }
|
|
227
322
|
```
|
|
228
323
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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.
|
|
236
359
|
|
|
237
360
|
### `rollback` does the compare-and-swap for you
|
|
238
361
|
|
|
@@ -306,26 +429,79 @@ what the first one did:
|
|
|
306
429
|
| `deployBox({ workspaceId })` | `deployBox({ workspaceId, alias })` | no alias ⇒ a brand-new box each time |
|
|
307
430
|
| `rollback({ alias })` | `rollback({ alias, to })` or `expectedCurrentId` | bare rollback is a toggle |
|
|
308
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` |
|
|
309
433
|
| `deleteDeployment(id)` throwing | `deleteDeployment(id, { ignoreNotFound: true })` | same |
|
|
310
434
|
|
|
311
|
-
`ignoreNotFound` matches the server's
|
|
312
|
-
found
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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:
|
|
323
471
|
|
|
324
472
|
```ts
|
|
325
|
-
await unfazed.
|
|
326
|
-
|
|
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.
|
|
327
489
|
```
|
|
328
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`.
|
|
504
|
+
|
|
329
505
|
## Env & secrets: three states, not two
|
|
330
506
|
|
|
331
507
|
A name in a given scope is exactly one of **plain**, **secret**, or **masked**,
|
|
@@ -446,25 +622,38 @@ to the app as well.
|
|
|
446
622
|
|
|
447
623
|
### With Nitro (the recipe)
|
|
448
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
|
+
|
|
449
637
|
```ts
|
|
450
638
|
// nitro.config.ts
|
|
451
639
|
import { defineConfig } from "nitro";
|
|
452
640
|
|
|
453
641
|
export default defineConfig({
|
|
454
642
|
modules: ["workflow/nitro", "unfazed/workflow/nitro"],
|
|
455
|
-
routes: { "/**": "./src/app.ts" }, // a Hono app; not named server.ts (Nitro claims that name)
|
|
456
643
|
});
|
|
457
644
|
```
|
|
458
645
|
|
|
459
646
|
```json
|
|
460
|
-
// unfazed.json —
|
|
461
|
-
{ "kind": "SERVICE", "
|
|
647
|
+
// unfazed.json — no "entry"
|
|
648
|
+
{ "kind": "SERVICE", "build": "nitro build" }
|
|
462
649
|
```
|
|
463
650
|
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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.
|
|
468
657
|
|
|
469
658
|
Then bind a Postgres or Neon integration to the app and deploy. In a
|
|
470
659
|
production build the module:
|
|
@@ -481,14 +670,16 @@ production build the module:
|
|
|
481
670
|
- mounts the **inspection route** the unfazed dashboard and MCP read runs
|
|
482
671
|
through (below), with the build's workflow list and Workflow SDK version;
|
|
483
672
|
- sets `noExternals: true` and builds Nitro's `standard` output (`export
|
|
484
|
-
default { fetch }
|
|
673
|
+
default { fetch }`, recorded as `"preset": "standard"` in
|
|
674
|
+
`.output/nitro.json`), which is what unfazed serves, whenever no preset was
|
|
485
675
|
chosen, whatever Nitro detected on the build machine: `bun` under Bun (as on
|
|
486
676
|
the unfazed build VM), `node-server` under Node, `deno-server` under Deno.
|
|
487
677
|
Their own outputs start a server of their own (under Bun with a 10 s idle
|
|
488
678
|
timeout that cuts every quiet workflow stream). A preset chosen explicitly
|
|
489
679
|
(`preset` in nitro.config.ts or .nitrorc, `NITRO_PRESET`, `SERVER_PRESET`)
|
|
490
680
|
other than `standard` **fails the build** with a message saying so; leave it
|
|
491
|
-
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 })`.
|
|
492
683
|
- serves public assets only when `serveStatic: "inline"` embeds them in the
|
|
493
684
|
bundle (a service VM has nothing next to it to read from disk). That is how
|
|
494
685
|
one service ships an API and its UI (`publicAssets: [{ dir: "dist" }]`
|
|
@@ -499,8 +690,12 @@ production build the module:
|
|
|
499
690
|
A complete app — steps, a retried step, `sleep`, a webhook (plain
|
|
500
691
|
`createWebhook()` and `webhook.url`), a progress stream — is in
|
|
501
692
|
[`examples/workflow-nitro`](./examples/workflow-nitro). Once it is deployed,
|
|
502
|
-
`
|
|
693
|
+
`npx unfazed workflow inspect runs --app <alias>` lists its runs
|
|
503
694
|
([Inspect runs from your terminal](#inspect-runs-from-your-terminal)).
|
|
695
|
+
[`examples/birthday-card-generator`](./examples/birthday-card-generator) is
|
|
696
|
+
Vercel's Workflow launch demo on unfazed: a React UI built by Vite and served
|
|
697
|
+
from the same bundle, AI steps, webhooks resumed from emails, `sleep` until a
|
|
698
|
+
date, and a stream the UI reconnects to; it runs without any provider keys.
|
|
504
699
|
|
|
505
700
|
### Without Nitro
|
|
506
701
|
|
|
@@ -569,12 +764,11 @@ database connection afterwards; `wake` and `cancel` are Workflow SDK's own
|
|
|
569
764
|
### Inspect runs from your terminal
|
|
570
765
|
|
|
571
766
|
Workflow SDK's own CLI reads an app's runs through the platform, the way
|
|
572
|
-
`--backend vercel` reads a Vercel project. From
|
|
573
|
-
`unfazed` is installed; the CLI loads `createWorld()` from it):
|
|
767
|
+
`--backend vercel` reads a Vercel project. From any directory:
|
|
574
768
|
|
|
575
769
|
```sh
|
|
576
|
-
|
|
577
|
-
|
|
770
|
+
npx unfazed login # once
|
|
771
|
+
npx unfazed workflow inspect runs --app birthday-card
|
|
578
772
|
```
|
|
579
773
|
|
|
580
774
|
```
|
|
@@ -586,25 +780,38 @@ wrun_01KPBDAY00000000000000RN03 generateBirthdayCard failed 2026-10-15 09:
|
|
|
586
780
|
```
|
|
587
781
|
|
|
588
782
|
```sh
|
|
589
|
-
npx workflow inspect run <run-id> --
|
|
590
|
-
npx workflow inspect steps --runId <run-id> --
|
|
591
|
-
npx workflow inspect step <step-id> --runId <run-id> --
|
|
592
|
-
npx workflow inspect stream <stream-id> --runId <run-id> --
|
|
593
|
-
npx workflow cancel <run-id> --
|
|
594
|
-
npx workflow web --
|
|
595
|
-
|
|
783
|
+
npx unfazed workflow inspect run <run-id> --app birthday-card # input, output, error
|
|
784
|
+
npx unfazed workflow inspect steps --runId <run-id> --app birthday-card # also: events, hooks, sleeps, streams
|
|
785
|
+
npx unfazed workflow inspect step <step-id> --runId <run-id> --app birthday-card
|
|
786
|
+
npx unfazed workflow inspect stream <stream-id> --runId <run-id> --app birthday-card # follows it while the run writes
|
|
787
|
+
npx unfazed workflow cancel <run-id> --app birthday-card
|
|
788
|
+
npx unfazed workflow web --app birthday-card # the local web UI: trace, events, streams,
|
|
789
|
+
# Wake Up Sleep and Cancel
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
`npx unfazed workflow …` is Workflow SDK's CLI with this package as its
|
|
793
|
+
backend: everything after `workflow` but `--app` is passed to it, and its
|
|
794
|
+
output and exit code come back unchanged. It runs the project's own
|
|
795
|
+
`workflow` (major 5) when the directory has one, else `npx -y workflow@5`.
|
|
796
|
+
|
|
797
|
+
**Inside a project** that has `unfazed` installed, the CLI's native form works
|
|
798
|
+
too, since the CLI resolves `--backend unfazed` from the project:
|
|
799
|
+
|
|
800
|
+
```sh
|
|
801
|
+
UNFZ_APP=birthday-card npx workflow inspect runs --backend unfazed
|
|
596
802
|
```
|
|
597
803
|
|
|
598
|
-
|
|
804
|
+
`export WORKFLOW_TARGET_WORLD=unfazed` saves typing `--backend` there. The
|
|
805
|
+
usual options work (`--json`, `--limit`, `--cursor`, `--all`, `--status`,
|
|
599
806
|
`--workflowName`, `--interactive`); the exceptions are below.
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
- **The token**
|
|
606
|
-
|
|
607
|
-
operator).
|
|
807
|
+
|
|
808
|
+
- **Which app:** `--app` (`npx unfazed workflow` only), else `UNFZ_APP` (its
|
|
809
|
+
alias, the `<alias>` of `https://<alias>.unfz.to`), else the `alias` in
|
|
810
|
+
`./unfazed.json`, or its only `apps[]` entry. `UNFZ_API_URL` points at
|
|
811
|
+
another control plane.
|
|
812
|
+
- **The token** is your `npx unfazed login`, or `UNFZ_API_TOKEN` when set. It
|
|
813
|
+
needs `unfz:read`; cancel and Wake Up Sleep need `unfz:deploy` too, and the
|
|
814
|
+
caller must be allowed to restart the app (owner, team member, operator).
|
|
608
815
|
- **Every command is one platform call relayed to the app**, through the same
|
|
609
816
|
inspection route the dashboard reads; an `on_demand` app cold-starts for it,
|
|
610
817
|
as for a visitor.
|
|
@@ -726,6 +933,7 @@ WORKFLOW_POSTGRES_URL=$UNFAZED_TEST_DATABASE_URL bun run test:conformance
|
|
|
726
933
|
# delivery cold-started in a fresh process
|
|
727
934
|
bun run test:example # the example, bundled as unfazed builds it,
|
|
728
935
|
# one fresh process per delivery and request
|
|
936
|
+
bun run test:birthday # the same for examples/birthday-card-generator
|
|
729
937
|
bun run test:cli # Workflow SDK's CLI with --backend unfazed (and --web)
|
|
730
938
|
# against a fake of the platform's inspection API
|
|
731
939
|
```
|
|
@@ -745,6 +953,11 @@ After bumping the `@workflow/world-postgres` devDependency, run
|
|
|
745
953
|
`bun run embed-migrations`: `bun run check` fails while
|
|
746
954
|
`src/workflow/migrations.generated.ts` is stale.
|
|
747
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
|
+
|
|
748
961
|
### Regenerating from the schema
|
|
749
962
|
|
|
750
963
|
`schema/schema.graphqls` is a **vendored copy** of `graph/schema.graphqls` from
|
|
@@ -783,6 +996,14 @@ bun run check # typecheck + tests + build + dist smoke
|
|
|
783
996
|
npm publish # or: bun publish --access public
|
|
784
997
|
```
|
|
785
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
|
+
|
|
786
1007
|
## License
|
|
787
1008
|
|
|
788
1009
|
MIT — see [LICENSE](./LICENSE).
|