unfazed 0.6.1 → 0.8.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/dist/unfazed.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { UnfazedClient, type ClientOptions, type RequestOptions } from "./client.js";
2
- import type { CreateDeploymentInput, DeploymentFieldsFragment, ListDeploymentsInput, RunFieldsFragment, RunStatus, ServiceMode, ServiceRuntime, SdkCreateWorkspaceMutation, SdkRunHarnessesQuery } from "./generated/graphql.js";
2
+ import type { AppErrorGroupFieldsFragment, CreateDeploymentInput, DeploymentEventType, DeploymentFieldsFragment, FileCommitFieldsFragment, ListDeploymentsInput, RunFieldsFragment, RunStatus, ServiceMode, ServiceRuntime, SdkAppCommitQuery, SdkAppCommitsQuery, SdkAppErrorGroupQuery, SdkAppErrorsQuery, SdkCreateWorkspaceMutation, SdkDeleteAppMutation, SdkDeploymentEventsQuery, SdkEventTypesQuery, SdkPollEventsQuery, SdkRestoreVersionMutation, SdkRunHarnessesQuery, SdkWorkspaceLiveAppsQuery } from "./generated/graphql.js";
3
3
  import { type WaitOptions } from "./wait.js";
4
4
  /** A deployment as the workflow layer returns it. */
5
5
  export type Deployment = DeploymentFieldsFragment;
@@ -17,16 +17,58 @@ export type RunHarness = SdkRunHarnessesQuery["runHarnesses"][number];
17
17
  * a listed or a freshly created workspace under one type.
18
18
  */
19
19
  export type Workspace = SdkCreateWorkspaceMutation["createWorkspace"]["workspace"];
20
+ /**
21
+ * An app a workspace serves right now, from {@link Unfazed.liveApps}: its
22
+ * alias, the deployment serving it, and the custom domains routing to it
23
+ * (listed for the app's owner only).
24
+ */
25
+ export type LiveApp = NonNullable<SdkWorkspaceLiveAppsQuery["workspace"]>["liveApps"][number];
26
+ /** What {@link Unfazed.deleteApp} removed, as the server reports it. */
27
+ export type DeleteAppResult = SdkDeleteAppMutation["deleteApp"];
20
28
  /** Options for {@link Unfazed.deleteWorkspace}. */
21
- export type DeleteWorkspaceOptions = {
29
+ export type DeleteWorkspaceOptions = ({
30
+ /**
31
+ * Omitted or `false`: delete the workspace only when it has no
32
+ * deployment records left. The server refuses otherwise, and deletes
33
+ * nothing.
34
+ */
35
+ withDeployments?: false;
36
+ /** Only with `withDeployments: true`. */
37
+ confirmAliases?: undefined;
38
+ } | {
22
39
  /**
23
40
  * Also delete every deployment snapshotted from the workspace, stopping
24
- * their service VMs. **Defaults to `true`** — see the method docs for why.
41
+ * its VMs. This takes EVERY app the workspace serves offline, so
42
+ * `confirmAliases` must name them.
25
43
  */
26
- withDeployments?: boolean;
44
+ withDeployments: true;
45
+ /**
46
+ * Exactly the aliases of the apps the workspace serves
47
+ * ({@link Unfazed.liveApps}), in any order: every app this delete takes
48
+ * offline. `[]` when it serves none (only failed or retired records are
49
+ * left). A missing or extra alias makes the server refuse and delete
50
+ * nothing ({@link UnfazedUnconfirmedAppsError}). The SDK never fills
51
+ * this in for you.
52
+ */
53
+ confirmAliases: readonly string[];
54
+ }) & {
27
55
  /** Resolve `false` instead of throwing when the workspace is not visible. */
28
56
  ignoreNotFound?: boolean;
29
57
  } & RequestOptions;
58
+ /** The app {@link Unfazed.deleteApp} deletes. */
59
+ export interface AppRef {
60
+ workspaceId: string;
61
+ /** The app's alias in that workspace. */
62
+ alias: string;
63
+ }
64
+ /** Options for {@link Unfazed.deleteApp}. */
65
+ export type DeleteAppOptions = {
66
+ /**
67
+ * Resolve `null` instead of throwing when the app is not in the workspace
68
+ * (already deleted) or the workspace is not visible.
69
+ */
70
+ ignoreNotFound?: boolean;
71
+ } & RequestOptions;
30
72
  /** Options for {@link Unfazed.deleteDeployment}. */
31
73
  export type DeleteDeploymentOptions = {
32
74
  /** Resolve `false` instead of throwing when the deployment is not visible. */
@@ -50,6 +92,239 @@ export interface EnvVar {
50
92
  masked: boolean;
51
93
  updatedAt: string;
52
94
  }
95
+ /**
96
+ * The commit a write made on an app with platform git, exactly as MCP
97
+ * `write_workspace_files` reports it.
98
+ *
99
+ * The three states of a write: SAVED (the call returned: the app's files
100
+ * have it, and the next deploy uses it); COMMITTED (`committed && !waiting`:
101
+ * it is in the app's history, `listCommits`); DEPLOYED (a deploy of those
102
+ * files went READY: `deployService({ wait: true })`, or — only when
103
+ * `deploys` is true — the import of the branch head the write triggers).
104
+ *
105
+ * - `waiting: true` — saved, and the commit waits for the git server; it
106
+ * lands in order. `sha` is null until then. Nothing to retry; await it with
107
+ * {@link Unfazed.waitForCommit} (or `writeFiles(…, { waitForCommit: true })`).
108
+ * - `committed: false` — git already held exactly these files.
109
+ * - `conflicts` — a push changed the same paths meanwhile: the head (`sha`,
110
+ * a merge) keeps the pushed version; yours is `writeSHA`.
111
+ * - `paused` — platform git is paused for recovery: saved, committed later.
112
+ */
113
+ export type FileCommit = FileCommitFieldsFragment;
114
+ /** One file's outcome in {@link Unfazed.writeFiles}, in the order given. */
115
+ export interface FileWriteOutcome {
116
+ path: string;
117
+ ok: boolean;
118
+ /** Why this path was not written; null when it was. */
119
+ error: string | null;
120
+ /** The written file; null when it was not. */
121
+ file: WorkspaceFile | null;
122
+ }
123
+ /** What {@link Unfazed.writeFiles} wrote (MCP `write_workspace_files`' answer). */
124
+ export interface WriteFilesResult {
125
+ workspaceId: string;
126
+ /** Every file's outcome, in the order of the map. */
127
+ files: FileWriteOutcome[];
128
+ written: number;
129
+ failed: number;
130
+ /** Bytes written (decoded). */
131
+ totalBytes: number;
132
+ /**
133
+ * The commit the call made on an app with platform git; null on any other
134
+ * app, and on a platform too old to report it. A map over one request's
135
+ * limits (100 files, 32 MiB) is several requests and several commits: this
136
+ * is the last, `commits` has them all.
137
+ */
138
+ commit: FileCommit | null;
139
+ /** One commit per request, in order. Usually exactly one. */
140
+ commits: FileCommit[];
141
+ /**
142
+ * With `waitForCommit`, when a commit was waiting: the branch head once
143
+ * every waiting write had landed (this write is in its history).
144
+ */
145
+ landed?: CommitsLanded;
146
+ }
147
+ /** A single-file write's answer: the file, and the commit it made. */
148
+ export type WrittenFile = WorkspaceFile & {
149
+ commit: FileCommit | null;
150
+ };
151
+ /** What {@link Unfazed.waitForCommit} saw once no write was waiting. */
152
+ export interface CommitsLanded {
153
+ branch: string;
154
+ /** The branch head: every write made before the wait is in its history. */
155
+ head: string | null;
156
+ /** How long the wait took, in ms. */
157
+ waitedMs: number;
158
+ }
159
+ /** One page of an app's platform git history ({@link Unfazed.listCommits}). */
160
+ export type CommitPage = SdkAppCommitsQuery["appCommits"];
161
+ /** One commit with its diff ({@link Unfazed.getCommit}). */
162
+ export type CommitDetail = SdkAppCommitQuery["appCommit"];
163
+ /** Options for {@link Unfazed.restoreVersion} (MCP `restore_version`). */
164
+ export interface RestoreVersionOptions {
165
+ workspaceId: string;
166
+ /** Full 40-character sha of the commit whose files the app gets back. */
167
+ sha: string;
168
+ /** Refuse, changing nothing, unless the branch head is still this commit. */
169
+ expectedHead?: string;
170
+ /** Message of the new commit; default `Restore <short sha> (<its message>)`. */
171
+ commitMessage?: string;
172
+ /**
173
+ * When the restore deploys (`deploys: true`, an app that deploys every
174
+ * push), wait for that deploy: the platform's wait by commit
175
+ * ({@link Unfazed.waitForDeployment} with `gitCommit`, `alias` and `after`
176
+ * from the answer) resolves once it is READY; {@link UnfazedDeployError}
177
+ * when it or the import of the new commit failed. A link with unfazed.json
178
+ * `apps[]` (no `deployAlias`) deploys once per app: then the wait follows
179
+ * the import until every deployment it made is done.
180
+ */
181
+ wait?: boolean;
182
+ /** Timeout/backoff for `wait`; default 10 minutes. */
183
+ waitOptions?: WaitOptions;
184
+ signal?: AbortSignal;
185
+ }
186
+ /** What {@link Unfazed.restoreVersion} did. */
187
+ export type RestoreVersionResult = SdkRestoreVersionMutation["restoreAppVersion"] & {
188
+ /**
189
+ * With `wait` and `deploys`: the deployments the restore's import made (one
190
+ * per alias it deploys), READY. Absent without `wait` or when nothing
191
+ * deploys. Without `wait`, wait later with `waitForDeployment({ workspaceId:
192
+ * workspaceID, gitCommit: sha, alias: deployAlias, after: waitAfter })`.
193
+ */
194
+ deployments?: Deployment[];
195
+ };
196
+ /**
197
+ * The deploy a git import makes of a commit, for
198
+ * {@link Unfazed.waitForDeployment}: a restore, a push or a sync deploys
199
+ * before any deployment id exists, so the commit names it.
200
+ */
201
+ export interface CommitDeploymentTarget {
202
+ workspaceId: string;
203
+ /** Full 40-character sha. */
204
+ gitCommit: string;
205
+ /** The app that deploys it; required when several apps (unfazed.json `apps[]`) deployed the commit. */
206
+ alias?: string | null;
207
+ /**
208
+ * Count only deployments begun after this point of the platform's
209
+ * deployment order (a restore's `waitAfter`), so an earlier deploy of the
210
+ * same commit never answers.
211
+ */
212
+ after?: number | null;
213
+ }
214
+ /** A service app's error group (MCP `list_errors`). Title, type, culprit and level are untrusted app text. */
215
+ export type ErrorGroup = AppErrorGroupFieldsFragment;
216
+ /** One page of error groups ({@link Unfazed.listErrors}). */
217
+ export type ErrorGroupPage = SdkAppErrorsQuery["appErrors"];
218
+ /** One error group with its latest occurrence ({@link Unfazed.getErrorGroup}). */
219
+ export type ErrorGroupDetail = SdkAppErrorGroupQuery["appErrorGroup"];
220
+ /** Names one error group of one app. */
221
+ export interface ErrorGroupRef extends AppRef {
222
+ /** The group's id (from listErrors, or `group_id` of an error.new / error.regressed event). */
223
+ groupId: string;
224
+ }
225
+ /** Filter/paging for {@link Unfazed.listErrors}. */
226
+ export interface ListErrorsOptions extends AppRef {
227
+ status?: "open" | "resolved" | "muted";
228
+ /** 1 to 100; default 20. */
229
+ limit?: number;
230
+ /** The previous page's `nextCursor`. */
231
+ cursor?: string;
232
+ }
233
+ /** An event type {@link Unfazed.pollEvents} reads (MCP `list_event_types`). */
234
+ export type EventType = SdkEventTypesQuery["platformEventTypes"][number];
235
+ /** A page of events (MCP `poll_events` / `wait_for_events`). */
236
+ export type EventPage = SdkPollEventsQuery["pollEvents"];
237
+ /** One event: `name` (deployment.finished, service.crashed, error.new, git.conflict, …), and minimal `data`. */
238
+ export type PlatformEvent = EventPage["events"][number];
239
+ /** Which events to read (MCP `poll_events`' input). */
240
+ export interface EventsInput {
241
+ /** Event types; omit for every type you may receive. */
242
+ names?: string[];
243
+ /** Filters per type, keyed by its name: `{ "deployment.finished": { alias: "shop", status: "FAILED" } }`. */
244
+ arguments?: Record<string, Record<string, unknown>>;
245
+ /** Where to continue; omit to start from now (nothing is replayed). */
246
+ cursor?: string;
247
+ /** At most this many events. */
248
+ max?: number;
249
+ }
250
+ /**
251
+ * A deployment event. `type` is the event's own name — `"service.vm_started"`,
252
+ * `"deployment.build_finished"` — exactly as MCP `list_deployment_events` and
253
+ * `wait_for_deployment` report it.
254
+ */
255
+ export interface DeploymentEvent {
256
+ id: string;
257
+ deploymentID: string;
258
+ /** The event's name as the platform writes it, e.g. `"deployment.phases"`. */
259
+ type: string;
260
+ /**
261
+ * The GraphQL enum (`SERVICE_VM_STARTED`, …), `UNKNOWN` for every name the
262
+ * enum does not list (`deployment.build_finished`, `deployment.phases`, …).
263
+ * Match on `type` instead.
264
+ */
265
+ typeEnum: DeploymentEventType;
266
+ /** @deprecated Same as `type` (before 0.8.0 `type` was the enum). */
267
+ rawType: string;
268
+ severity: SdkDeploymentEventsQuery["deploymentEvents"][number]["severity"];
269
+ message: string;
270
+ vmID: string | null;
271
+ /** The event's fields; timing events carry their phases as `*_ms` strings. */
272
+ metadata: Record<string, string> | null;
273
+ /**
274
+ * What a timing event (`deployment.build_finished`, `deployment.phases`)
275
+ * measured, as the platform parsed it; null for any other event.
276
+ * {@link parseDeploymentTimings} prefers it to `metadata`.
277
+ */
278
+ timings: PlatformTimings | null;
279
+ createdAt: string;
280
+ }
281
+ /**
282
+ * A deploy's phases as the platform measured them: from `deployment.phases`
283
+ * (recorded when the deploy goes READY) and `deployment.build_finished`.
284
+ * Milliseconds.
285
+ */
286
+ export interface PlatformTimings {
287
+ /** Deployment created → READY, on the platform's clock. Null until `deployment.phases` is recorded. */
288
+ totalMs: number | null;
289
+ /** The build, as the build VM's host measured it. */
290
+ buildMs: number | null;
291
+ /** Promotion: health probe and alias switch (and a worker's pickup and boot). */
292
+ promoteMs: number | null;
293
+ /**
294
+ * Every other measured phase by name: the build VM's (`vm_install`,
295
+ * `vm_build`, `vm_bundle`, `vm_collect`, `image`, `vm`, `return`,
296
+ * `extract`) and the promotion's (`admission`, `worker_pickup`, `boot`, …).
297
+ */
298
+ phases: Record<string, number>;
299
+ /** `local` or `worker`. */
300
+ buildMode: string | null;
301
+ artifactTransport: string | null;
302
+ buildMemoryMiB: number | null;
303
+ }
304
+ /** Wall time this client spent on a deploy with `wait: true`. Milliseconds. */
305
+ export interface ClientTimings {
306
+ /** Sending createDeployment until its answer. */
307
+ createMs: number;
308
+ /** Its answer until the client saw the final status. */
309
+ waitMs: number;
310
+ /** `createMs + waitMs`: what the caller waited. Compare with `platform.totalMs`. */
311
+ totalMs: number;
312
+ /** Requests the wait made: one per server-side wait (up to 45 s each), or one per poll. */
313
+ waitRequests: number;
314
+ /** `"server"`: the platform's server-side wait; `"poll"`: polling (platforms without it). */
315
+ waitMode: "server" | "poll";
316
+ }
317
+ /** Both sides of a deploy's time ({@link DeployResult.timings}). */
318
+ export interface DeployTimings {
319
+ client: ClientTimings;
320
+ /** Null when the platform recorded none (yet), or with `timings: false`. */
321
+ platform: PlatformTimings | null;
322
+ }
323
+ /** A deploy's answer: the deployment, and with `wait: true` its timings. */
324
+ export type DeployResult = Deployment & {
325
+ /** With `wait: true`: the platform's phases and this client's wall time. */
326
+ timings?: DeployTimings;
327
+ };
53
328
  /**
54
329
  * A file's contents for {@link Unfazed.writeFiles}.
55
330
  *
@@ -68,16 +343,31 @@ export type FileContent = string | {
68
343
  export type FilesMap = Record<string, FileContent>;
69
344
  export interface WriteFilesOptions {
70
345
  /**
71
- * Max files per GraphQL request. Default 20.
72
- *
73
- * The batch is one document with N aliased root fields; too many at once
74
- * makes a single oversized request whose failure is all-or-nothing to
75
- * diagnose. Twenty keeps a typical app to one or two round trips.
346
+ * On an app with platform git: the message of the call's commit. Default
347
+ * names the files. Ignored on other apps.
348
+ */
349
+ commitMessage?: string;
350
+ /**
351
+ * When the commit waits for the git server (`commit.waiting`), resolve only
352
+ * once it landed ({@link Unfazed.waitForCommit}); `true` or the wait's
353
+ * options. The result's `landed` names the branch head then.
354
+ */
355
+ waitForCommit?: boolean | WaitOptions;
356
+ /**
357
+ * Resolve with the result even when some files failed, instead of throwing
358
+ * {@link UnfazedWriteError}. Read `failed` and `files[].error` then.
359
+ */
360
+ allowPartial?: boolean;
361
+ /**
362
+ * Max files per request. Default 100, the platform's per-call limit. A
363
+ * request is ONE commit on an app with platform git, so a map that needs
364
+ * several requests is several commits.
76
365
  */
77
366
  batchSize?: number;
78
367
  /**
79
- * Max total payload bytes per request. Default 4 MiB. Whichever limit trips
80
- * first closes the batch, so one large file cannot blow the request up.
368
+ * Max payload bytes per request (text, or base64 as sent). Default 32 MiB,
369
+ * the platform's per-call total. Whichever limit trips first closes a
370
+ * request; a single file over it is still sent, and refused on its own.
81
371
  */
82
372
  maxBatchBytes?: number;
83
373
  signal?: AbortSignal;
@@ -97,6 +387,12 @@ export interface DeployOptions {
97
387
  wait?: boolean;
98
388
  /** Backoff/timeout knobs used when `wait` is true. */
99
389
  waitOptions?: WaitOptions;
390
+ /**
391
+ * With `wait: true`, read the platform's phase timings once the deploy is
392
+ * READY (one more request) into `result.timings.platform`. Default true;
393
+ * `false` skips the read (`timings.platform` is null).
394
+ */
395
+ timings?: boolean;
100
396
  signal?: AbortSignal;
101
397
  }
102
398
  export interface DeployServiceOptions extends DeployOptions {
@@ -115,7 +411,14 @@ export interface DeployStaticOptions extends DeployOptions {
115
411
  spaMode?: boolean;
116
412
  }
117
413
  export interface DeployBoxOptions extends DeployOptions {
118
- /** VM size. Omit for platform defaults (2048 MiB / 2 vCPU / 8 GiB disk). */
414
+ /**
415
+ * VM size. Omit a field for the platform default; the server's
416
+ * `BoxResourcesInput` is authoritative. Today: 4096 MiB RAM (min 512, max
417
+ * 16384), 4 vCPU (max 16) and a 32 GiB disk (max 128), unless the platform
418
+ * operator raised the box's limit; a node may clamp lower. `diskGiB` sizes
419
+ * the disk when it is first created; a later redeploy asking for more grows
420
+ * it before the box boots, and it never shrinks.
421
+ */
119
422
  resources?: {
120
423
  memMiB?: number;
121
424
  vcpus?: number;
@@ -319,33 +622,98 @@ export declare class Unfazed {
319
622
  updatedAt: string;
320
623
  }[]>;
321
624
  /**
322
- * Deletes a workspace and everything anchored to it — deployments, env, and
323
- * a BOX's persistent data disk. Not recoverable.
625
+ * Deletes a workspace — its files, env and secrets, and a BOX's persistent
626
+ * data disk — and, with `withDeployments`, every app it serves.
324
627
  *
325
- * **The cascade is opt-out, not opt-in.** The server refuses to delete a
326
- * workspace that has ANY deployment history — including FAILED records, and
327
- * including deployments whose alias was long since taken over by something
328
- * else — unless the mutation carries `withDeployments`. Since almost every
329
- * workspace an agent creates gets deployed at least once, defaulting to
330
- * `false` would make this method fail on nearly every real workspace, so it
331
- * defaults to **`true`**: the behaviour this docstring has always promised.
332
- *
333
- * Pass `withDeployments: false` for the strict variant, which refuses rather
334
- * than cascading and is the safer choice when you are deleting a workspace
335
- * you did not create:
628
+ * A workspace can serve several apps (each `unfazed.json` `apps[]` entry, or
629
+ * several aliases deployed into it), so deleting one with its deployments is
630
+ * never implicit: **name exactly the apps it takes offline** in
631
+ * `confirmAliases`. The server compares them with the workspace's live apps
632
+ * and refuses, deleting nothing, on a missing or extra alias; the refusal
633
+ * ({@link UnfazedUnconfirmedAppsError}) lists every live app with its
634
+ * domains. The SDK never fills the list in: look it up deliberately with
635
+ * {@link liveApps} and decide. To delete one app and keep the rest, use
636
+ * {@link deleteApp}.
336
637
  *
337
638
  * ```ts
338
- * await unfazed.deleteWorkspace(ws.id); // cascade
339
- * await unfazed.deleteWorkspace(ws.id, { withDeployments: false }); // refuse if deployed
340
- * await unfazed.deleteWorkspace(id, { ignoreNotFound: true }); // false, not a throw
639
+ * await unfazed.deleteWorkspace(ws.id); // only when no deployment is left
640
+ * await unfazed.deleteWorkspace(ws.id, { withDeployments: true, confirmAliases: ["web", "api"] });
641
+ * await unfazed.deleteWorkspace(ws.id, { withDeployments: true, confirmAliases: [] }); // serves no app
642
+ * await unfazed.deleteWorkspace(id, { ignoreNotFound: true }); // false, not a throw
341
643
  * ```
342
644
  *
645
+ * Without `withDeployments` the server refuses while ANY deployment record
646
+ * is left — FAILED ones included — with `still has deployments; delete them
647
+ * first`. A workspace whose deploys all failed serves no app: delete it with
648
+ * `withDeployments: true, confirmAliases: []`.
649
+ *
650
+ * What is deleted disappears at once — gone from your views, its names and
651
+ * aliases free, its VMs stopped — but the platform keeps it for 30 days:
652
+ * within that time support (the platform operator) can restore it exactly
653
+ * as it was. You cannot restore it yourself; ask support. After 30 days it
654
+ * is erased for good.
655
+ *
343
656
  * With `ignoreNotFound` a workspace that is absent (or belongs to someone
344
657
  * else — the server returns one indistinguishable sentinel for both, on
345
658
  * purpose) resolves to `false` instead of throwing, which makes teardown in
346
659
  * a `finally` block idempotent.
347
660
  */
348
661
  deleteWorkspace(workspaceId: string, options?: DeleteWorkspaceOptions): Promise<boolean>;
662
+ /**
663
+ * The apps a workspace serves right now, by alias, with the deployment
664
+ * serving each and its custom domains: everything
665
+ * `deleteWorkspace(id, { withDeployments: true, … })` takes offline, and the
666
+ * exact list its `confirmAliases` must name.
667
+ *
668
+ * It is a lookup for you to read, not something to pipe into a delete
669
+ * unseen: check that the apps are the ones you mean to take down.
670
+ *
671
+ * ```ts
672
+ * const live = await unfazed.liveApps(ws.id); // [{ alias: "web", deploymentID, domains: [] }, …]
673
+ * ```
674
+ *
675
+ * Throws {@link UnfazedNotFoundError} when the workspace is absent or not
676
+ * visible to this token.
677
+ */
678
+ liveApps(workspaceId: string, options?: RequestOptions): Promise<LiveApp[]>;
679
+ /**
680
+ * Deletes ONE app of a workspace and nothing else: every deployment of the
681
+ * alias there (live release, retained releases, failed and building
682
+ * attempts — its VM stops, its URL and custom domains stop serving), its App
683
+ * State, its app-scoped env and secrets, and its integration bindings. The
684
+ * workspace, its files and defaults, its other apps (`otherApps` in the
685
+ * result) and a box's data disk stay.
686
+ *
687
+ * ```ts
688
+ * const gone = await unfazed.deleteApp({ workspaceId: ws.id, alias: "docs" });
689
+ * gone.deployments; // records deleted
690
+ * gone.otherApps; // the workspace's apps still serving, untouched
691
+ * ```
692
+ *
693
+ * In a git-linked workspace pushes and syncs skip the alias while
694
+ * `unfazed.json` `apps[]` still declares it (`skippedOnSync: true`): remove
695
+ * it from `apps[]`, or bring it back with the GraphQL `syncRepo` mutation's
696
+ * `restoreApps` (through {@link client}). The server refuses while a git
697
+ * import of the workspace runs. Owner only.
698
+ *
699
+ * What is deleted disappears at once, but the platform keeps the app for 30
700
+ * days: within that time support (the platform operator) can restore it
701
+ * exactly as it was. You cannot restore it yourself; ask support. After 30
702
+ * days it is erased for good. A custom domain's record stays with the alias,
703
+ * so a restored app serves it again.
704
+ *
705
+ * A delete that stops part-way throws with the server's message, which says
706
+ * so; deleting again finishes it. With `ignoreNotFound`, an app that is not
707
+ * in the workspace (already deleted) or a workspace that is not visible
708
+ * resolves to `null` instead of throwing.
709
+ */
710
+ deleteApp(app: AppRef, options: DeleteAppOptions & {
711
+ ignoreNotFound: true;
712
+ }): Promise<DeleteAppResult | null>;
713
+ deleteApp(app: AppRef, options?: DeleteAppOptions & {
714
+ ignoreNotFound?: false;
715
+ }): Promise<DeleteAppResult>;
716
+ deleteApp(app: AppRef, options?: DeleteAppOptions): Promise<DeleteAppResult | null>;
349
717
  /**
350
718
  * Returns the workspace named `name`, creating it only if it does not exist.
351
719
  *
@@ -371,33 +739,58 @@ export declare class Unfazed {
371
739
  updatedAt: string;
372
740
  }>;
373
741
  /**
374
- * Writes many files in as few requests as possible.
375
- *
376
- * The API is one-file-per-mutation, so writing a 12-file app naively costs 12
377
- * round trips. This batches them into a single GraphQL document with one
378
- * ALIASED root field per file (`f0: writeWorkspaceTextFile(...)`,
379
- * `f1: ...`), which the server resolves in one request.
742
+ * Writes many files in ONE request — and, on an app with platform git, ONE
743
+ * commit — exactly as MCP `write_workspace_files` does.
380
744
  *
381
745
  * ```ts
382
- * await unfazed.writeFiles(ws.id, {
746
+ * const out = await unfazed.writeFiles(ws.id, {
383
747
  * "server.ts": serverCode,
384
- * "unfazed.json": JSON.stringify({ entry: "server.ts" }),
748
+ * "package.json": JSON.stringify(pkg),
385
749
  * "public/logo.png": { bytes: pngBytes },
386
- * });
750
+ * }, { commitMessage: "Add the server" });
751
+ * out.commit; // { sha, committed, waiting, conflicts, writeSHA, … } or null
387
752
  * ```
388
753
  *
389
- * Batches are NOT atomic: GraphQL resolves each aliased field independently,
390
- * so a partial failure leaves earlier writes committed. The thrown
391
- * {@link UnfazedGraphQLError} carries `data` with whichever aliases
392
- * succeeded, and `error.path` identifies the ones that did not.
754
+ * A call is not transactional: files are written in order, each atomically,
755
+ * and a file's failure does not undo the others. When any file fails this
756
+ * throws {@link UnfazedWriteError} (its `result` lists what was written and
757
+ * `failed` what was not; retry only those), unless `allowPartial`. A
758
+ * repeated path keeps its last content.
759
+ *
760
+ * On an app with platform git the commit is made before any file is
761
+ * written: if it fails, the request fails and nothing is written. `commit`
762
+ * may come back `waiting` — the files are saved and the commit lands in
763
+ * order; pass `waitForCommit: true` (or call {@link waitForCommit}) to
764
+ * resolve only once it has. See {@link FileCommit} for saved, committed and
765
+ * deployed.
766
+ *
767
+ * One request carries up to 100 files and 32 MiB; a bigger map is split
768
+ * into several requests, each its own commit (`commits`). On a platform
769
+ * older than the batch write the SDK falls back to one mutation per file
770
+ * (one commit each) and reports `commit: null`.
771
+ */
772
+ writeFiles(workspaceId: string, files: FilesMap, options?: WriteFilesOptions): Promise<WriteFilesResult>;
773
+ /**
774
+ * Writes one text file: {@link writeFiles} with one entry, so it is one
775
+ * commit with `commitMessage` on an app with platform git, and the answer
776
+ * carries that commit. Throws {@link UnfazedWriteError} when it is refused.
393
777
  */
394
- writeFiles(workspaceId: string, files: FilesMap, options?: WriteFilesOptions): Promise<WorkspaceFile[]>;
395
- /** Writes one text file. Prefer {@link writeFiles} for more than one. */
396
- writeFile(workspaceId: string, path: string, text: string, options?: RequestOptions): Promise<WorkspaceFile>;
397
- /** Writes one binary file. Accepts raw bytes or a base64 string. */
778
+ writeFile(workspaceId: string, path: string, text: string, options?: Omit<WriteFilesOptions, "allowPartial" | "batchSize" | "maxBatchBytes">): Promise<WrittenFile>;
779
+ /** Writes one binary file (raw bytes or base64), like {@link writeFile}. */
398
780
  writeBinaryFile(workspaceId: string, path: string, content: Uint8Array | ArrayBuffer | {
399
781
  base64: string;
400
- }, options?: RequestOptions): Promise<WorkspaceFile>;
782
+ }, options?: Omit<WriteFilesOptions, "allowPartial" | "batchSize" | "maxBatchBytes">): Promise<WrittenFile>;
783
+ /**
784
+ * Waits until no file write of the app waits for its commit: platform git
785
+ * commits a write at once, but when the git server is slow the write is
786
+ * saved and its commit `waiting`, landing in order. Resolves with the
787
+ * branch head then — every write made before the call is in its history.
788
+ * An app without platform git resolves at once (`head: null`).
789
+ *
790
+ * Default timeout 2 minutes; throws {@link UnfazedTimeoutError} past it
791
+ * (the writes stay saved, and still land).
792
+ */
793
+ waitForCommit(workspaceId: string, waitOptions?: WaitOptions): Promise<CommitsLanded>;
401
794
  /** Lists workspace files (paginated). */
402
795
  listFiles(workspaceId: string, input?: {
403
796
  limit?: number;
@@ -453,9 +846,9 @@ export declare class Unfazed {
453
846
  * takes over the alias when it goes READY. Omitting `alias` lets the server
454
847
  * generate one, which means a different app on every call.
455
848
  */
456
- deployService(options: DeployServiceOptions): Promise<Deployment>;
849
+ deployService(options: DeployServiceOptions): Promise<DeployResult>;
457
850
  /** Deploys the workspace as a STATIC site. */
458
- deployStatic(options: DeployStaticOptions): Promise<Deployment>;
851
+ deployStatic(options: DeployStaticOptions): Promise<DeployResult>;
459
852
  /**
460
853
  * Deploys a BOX: a persistent always-on Linux VM with a data disk that
461
854
  * survives stops and redeploys. A dev machine, not an HTTP service. The
@@ -480,19 +873,46 @@ export declare class Unfazed {
480
873
  * await unfazed.deployBox({ workspaceId: ws.id, alias: "dev" }); // re-runnable
481
874
  * ```
482
875
  */
483
- deployBox(options: DeployBoxOptions): Promise<Deployment>;
876
+ deployBox(options: DeployBoxOptions): Promise<DeployResult>;
484
877
  /** Escape hatch: deploy with a raw CreateDeploymentInput. */
485
- deploy(input: CreateDeploymentInput, options?: Omit<DeployOptions, "workspaceId">): Promise<Deployment>;
878
+ deploy(input: CreateDeploymentInput, options?: Omit<DeployOptions, "workspaceId">): Promise<DeployResult>;
486
879
  /**
487
- * Polls a deployment until it is READY, throwing on FAILED or timeout.
880
+ * Waits until a deployment is READY, throwing {@link UnfazedDeployError} on
881
+ * FAILED or timeout (default 10 minutes).
882
+ *
883
+ * It waits SERVER-SIDE (MCP `wait_for_deployment`): each request blocks on
884
+ * the control plane until the deployment leaves BUILDING — woken by the
885
+ * deploy pipeline itself — or 45 s pass, and comes back with the full
886
+ * deployment; so READY reaches you one round trip after it happens, not at
887
+ * the next poll. A NEW app that starts directly on its server is READY
888
+ * before it serves there; the wait covers that landing too. On a platform
889
+ * without the server-side wait, or when the platform refuses one because
890
+ * the account already holds its cap of waits in progress (16), it polls
891
+ * instead (`waitOptions.mode: "poll"` forces that), every 0.5 s backing
892
+ * off to 3 s by default.
488
893
  *
489
- * BUILDING is the only non-terminal status; READY and FAILED are terminal.
490
- * The poll query selects six fields rather than the full deployment, because
491
- * this runs every few seconds for potentially minutes — then the READY
492
- * deployment is re-read in full so callers get the same shape `deployService`
493
- * returns without `wait`.
894
+ * Pass a {@link CommitDeploymentTarget} instead of an id to wait for the
895
+ * deploy a git import makes of a commit — a restore, a push or a sync
896
+ * deploys before any id exists. The wait covers the import too, and
897
+ * {@link UnfazedDeployError} carries the import's error when it failed on
898
+ * the commit.
899
+ *
900
+ * ```ts
901
+ * const back = await unfazed.restoreVersion({ workspaceId: ws.id, sha });
902
+ * await unfazed.waitForDeployment({
903
+ * workspaceId: back.workspaceID, gitCommit: back.sha, alias: back.deployAlias, after: back.waitAfter,
904
+ * });
905
+ * ```
494
906
  */
495
- waitForDeployment(deploymentId: string, waitOptions?: WaitOptions): Promise<Deployment>;
907
+ waitForDeployment(target: string | CommitDeploymentTarget, waitOptions?: WaitOptions): Promise<Deployment>;
908
+ /**
909
+ * The platform's measured phases of a deploy ({@link PlatformTimings}), as
910
+ * it parses them from all of the deployment's timing events
911
+ * (`Deployment.timings`). Null when it recorded none (a deploy that is
912
+ * still building, or one from before timings existed), or for a deployment
913
+ * you cannot see.
914
+ */
915
+ getDeploymentTimings(deploymentId: string, options?: RequestOptions): Promise<PlatformTimings | null>;
496
916
  /** Reads one deployment by id. Null when absent. */
497
917
  getDeployment(deploymentId: string, options?: RequestOptions): Promise<Deployment | null>;
498
918
  /** Reads the deployment currently serving an alias. Null when absent. */
@@ -566,17 +986,23 @@ export declare class Unfazed {
566
986
  stderrBytes: number;
567
987
  tailBytes: number;
568
988
  }>;
569
- /** Deployment lifecycle events — build, boot, probe, cron, restarts. */
570
- getEvents(deploymentId: string, limit?: number, options?: RequestOptions): Promise<{
571
- id: string;
572
- deploymentID: string;
573
- type: import("./index.js").DeploymentEventType;
574
- rawType: string;
575
- severity: import("./index.js").DeploymentEventSeverity;
576
- message: string;
577
- vmID: string | null;
578
- createdAt: string;
579
- }[]>;
989
+ /**
990
+ * Deployment lifecycle events — build, boot, probe, cron, restarts — the
991
+ * most recent `limit` (default 20), oldest first.
992
+ *
993
+ * `type` is the event's own name, as MCP reports it:
994
+ * `"deployment.build_finished"`, `"deployment.phases"`,
995
+ * `"service.vm_started"`, `"service.crashed"`. (`typeEnum` keeps the GraphQL
996
+ * enum, which reads `UNKNOWN` for names it does not list.) Timing events
997
+ * carry their phases in `metadata`; {@link getDeploymentTimings} turns them
998
+ * into numbers.
999
+ *
1000
+ * ```ts
1001
+ * const events = await unfazed.getEvents(app.id, 50);
1002
+ * events.filter((e) => e.type === "deployment.build_finished");
1003
+ * ```
1004
+ */
1005
+ getEvents(deploymentId: string, limit?: number, options?: RequestOptions): Promise<DeploymentEvent[]>;
580
1006
  /** Restarts a service's VM. */
581
1007
  restartService(deploymentId: string, options?: RequestOptions): Promise<{
582
1008
  deploymentID: string;
@@ -597,6 +1023,12 @@ export declare class Unfazed {
597
1023
  }>;
598
1024
  /**
599
1025
  * Deletes a deployment: stops its VM, releases its alias, drops its release.
1026
+ * To delete a whole app — every release of the alias, its env, App State and
1027
+ * bindings — use {@link deleteApp}.
1028
+ *
1029
+ * The platform keeps a deleted release for 30 days: within that time
1030
+ * support (the platform operator) can restore it; then it is erased for
1031
+ * good.
600
1032
  *
601
1033
  * With `ignoreNotFound` a deployment that is absent — or belongs to another
602
1034
  * tenant, which the server reports identically so ids cannot be probed —
@@ -749,6 +1181,90 @@ export declare class Unfazed {
749
1181
  * run fails loudly instead of silently undoing the first.
750
1182
  */
751
1183
  rollback(options: RollbackOptions): Promise<Deployment>;
1184
+ /**
1185
+ * The app's commits, newest first (MCP `list_commits`): `limit` per page
1186
+ * (default 20, max 100), `before: page.nextBefore` for the next. `deployed`
1187
+ * says what every live alias runs; `waitingCommits` counts writes whose
1188
+ * commit has not landed yet.
1189
+ */
1190
+ listCommits(workspaceId: string, input?: {
1191
+ limit?: number;
1192
+ before?: string;
1193
+ }, options?: RequestOptions): Promise<CommitPage>;
1194
+ /**
1195
+ * One commit (full 40-character sha) with the paths it changed and a
1196
+ * unified diff against its first parent, capped at 64 KiB (MCP
1197
+ * `get_commit`). Commit messages and diffs are untrusted text.
1198
+ */
1199
+ getCommit(workspaceId: string, sha: string, options?: RequestOptions): Promise<CommitDetail>;
1200
+ /**
1201
+ * Gives the app back the files of an earlier commit as ONE new commit on
1202
+ * its branch (MCP `restore_version`); nothing is rewritten, and a restore is
1203
+ * undone by restoring the commit before it.
1204
+ *
1205
+ * When the app deploys every push (`deploys: true`), the new commit is
1206
+ * imported and deployed like a push. That deploy is queued behind the
1207
+ * restore, so the platform answers before it exists; with `wait: true` the
1208
+ * SDK waits for it by commit on the platform ({@link Unfazed.waitForDeployment}
1209
+ * with `gitCommit: sha`, `alias: deployAlias`, `after: waitAfter`) and
1210
+ * resolves with it READY, or throws {@link UnfazedDeployError} with its
1211
+ * build error or the import's. A link with unfazed.json `apps[]` names no
1212
+ * `deployAlias` and deploys once per app: then it follows the import until
1213
+ * every deployment it made is done.
1214
+ *
1215
+ * ```ts
1216
+ * const page = await unfazed.listCommits(ws.id);
1217
+ * const back = await unfazed.restoreVersion({ workspaceId: ws.id, sha: page.commits[1]!.sha, wait: true });
1218
+ * back.deployments?.[0]?.url;
1219
+ * ```
1220
+ */
1221
+ restoreVersion(options: RestoreVersionOptions): Promise<RestoreVersionResult>;
1222
+ /** A service app's error groups, most recently seen first (MCP `list_errors`). */
1223
+ listErrors(input: ListErrorsOptions, options?: RequestOptions): Promise<ErrorGroupPage>;
1224
+ /**
1225
+ * One error group with its latest occurrence whole (a Sentry event: stack,
1226
+ * request), its recent occurrences and the deployments it was seen in (MCP
1227
+ * `get_error_group`).
1228
+ */
1229
+ getErrorGroup(ref: ErrorGroupRef, options?: RequestOptions): Promise<ErrorGroupDetail>;
1230
+ /**
1231
+ * Marks a group resolved in the app's current deployment, once its fix is
1232
+ * deployed (MCP `resolve_error_group`); another deployment reporting it
1233
+ * opens it again. Only on your human's say-so or a fix you verified.
1234
+ */
1235
+ resolveErrorGroup(ref: ErrorGroupRef, options?: RequestOptions): Promise<ErrorGroup>;
1236
+ /** Mutes an expected group: still counted, never regresses or notifies (MCP `mute_error_group`). */
1237
+ muteErrorGroup(ref: ErrorGroupRef, options?: RequestOptions): Promise<ErrorGroup>;
1238
+ /** Opens a resolved or muted group again (MCP `reopen_error_group`). */
1239
+ reopenErrorGroup(ref: ErrorGroupRef, options?: RequestOptions): Promise<ErrorGroup>;
1240
+ /** The event types, with each one's filters and payload schema (MCP `list_event_types`). */
1241
+ listEventTypes(options?: RequestOptions): Promise<EventType[]>;
1242
+ /**
1243
+ * Events since `cursor` (MCP `poll_events`). Omit `cursor` to start from now
1244
+ * — nothing is replayed — and pass the returned `cursor` back: at once while
1245
+ * `hasMore`, otherwise after `nextPollMs`, or to {@link waitForEvents}.
1246
+ * `truncated` means events may be missing: re-read state.
1247
+ */
1248
+ pollEvents(input?: EventsInput, options?: RequestOptions): Promise<EventPage>;
1249
+ /**
1250
+ * {@link pollEvents} that waits server-side until an event arrives (MCP
1251
+ * `wait_for_events`), for up to `timeoutMs` (default 45 s; longer waits are
1252
+ * several server waits of up to 45 s, continuing from each cursor). A
1253
+ * timeout is not an error: the page has `timedOut: true`, no events and a
1254
+ * cursor to wait again with. The platform caps an account's waits in
1255
+ * progress (16, deploy waits included); past it this throws an error that
1256
+ * `isTooManyWaits` recognizes: poll then.
1257
+ *
1258
+ * ```ts
1259
+ * let page = await unfazed.pollEvents({ names: ["deployment.finished"] }); // from now
1260
+ * page = await unfazed.waitForEvents({ names: ["deployment.finished"], cursor: page.cursor, timeoutMs: 120_000 });
1261
+ * ```
1262
+ */
1263
+ waitForEvents(input?: EventsInput & {
1264
+ timeoutMs?: number;
1265
+ }, options?: RequestOptions & {
1266
+ now?: () => number;
1267
+ }): Promise<EventPage>;
752
1268
  /**
753
1269
  * Starts an agent run.
754
1270
  *
@@ -936,3 +1452,18 @@ export declare class Unfazed {
936
1452
  truncated: boolean;
937
1453
  }>;
938
1454
  }
1455
+ /** An event {@link parseDeploymentTimings} reads: `timings` when the platform parsed it, else `metadata`. */
1456
+ export type TimingEvent = Pick<DeploymentEvent, "type" | "metadata"> & {
1457
+ timings?: PlatformTimings | null;
1458
+ };
1459
+ /**
1460
+ * A deploy's phases from its events, as the platform reads them: the latest
1461
+ * `deployment.phases` (written when it went READY: build, promotion and its
1462
+ * split, total) over the latest `deployment.build_finished` (the build VM's
1463
+ * own phases). It prefers the `timings` the platform parsed into each event
1464
+ * ({@link Unfazed.getEvents} reads them); events without them (an older
1465
+ * platform, events built by hand) are parsed from their `*_ms` metadata the
1466
+ * same way. Durations are whole, non-negative milliseconds: anything else is
1467
+ * dropped, as the platform drops it. Null when neither event is there.
1468
+ */
1469
+ export declare function parseDeploymentTimings(events: readonly TimingEvent[]): PlatformTimings | null;