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/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,211 @@ 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: resolve once the import of the new commit
175
+ * finished with every deployment it made, throw {@link UnfazedDeployError}
176
+ * when it failed.
177
+ */
178
+ wait?: boolean;
179
+ /** Timeout/backoff for `wait`; default 10 minutes. */
180
+ waitOptions?: WaitOptions;
181
+ signal?: AbortSignal;
182
+ }
183
+ /** What {@link Unfazed.restoreVersion} did. */
184
+ export type RestoreVersionResult = SdkRestoreVersionMutation["restoreAppVersion"] & {
185
+ /**
186
+ * With `wait` and `deploys`: the deployments the restore's import made (one
187
+ * per alias it deploys), READY. Absent without `wait` or when nothing
188
+ * deploys.
189
+ */
190
+ deployments?: Deployment[];
191
+ };
192
+ /** A service app's error group (MCP `list_errors`). Title, type, culprit and level are untrusted app text. */
193
+ export type ErrorGroup = AppErrorGroupFieldsFragment;
194
+ /** One page of error groups ({@link Unfazed.listErrors}). */
195
+ export type ErrorGroupPage = SdkAppErrorsQuery["appErrors"];
196
+ /** One error group with its latest occurrence ({@link Unfazed.getErrorGroup}). */
197
+ export type ErrorGroupDetail = SdkAppErrorGroupQuery["appErrorGroup"];
198
+ /** Names one error group of one app. */
199
+ export interface ErrorGroupRef extends AppRef {
200
+ /** The group's id (from listErrors, or `group_id` of an error.new / error.regressed event). */
201
+ groupId: string;
202
+ }
203
+ /** Filter/paging for {@link Unfazed.listErrors}. */
204
+ export interface ListErrorsOptions extends AppRef {
205
+ status?: "open" | "resolved" | "muted";
206
+ /** 1 to 100; default 20. */
207
+ limit?: number;
208
+ /** The previous page's `nextCursor`. */
209
+ cursor?: string;
210
+ }
211
+ /** An event type {@link Unfazed.pollEvents} reads (MCP `list_event_types`). */
212
+ export type EventType = SdkEventTypesQuery["platformEventTypes"][number];
213
+ /** A page of events (MCP `poll_events` / `wait_for_events`). */
214
+ export type EventPage = SdkPollEventsQuery["pollEvents"];
215
+ /** One event: `name` (deployment.finished, service.crashed, error.new, git.conflict, …), and minimal `data`. */
216
+ export type PlatformEvent = EventPage["events"][number];
217
+ /** Which events to read (MCP `poll_events`' input). */
218
+ export interface EventsInput {
219
+ /** Event types; omit for every type you may receive. */
220
+ names?: string[];
221
+ /** Filters per type, keyed by its name: `{ "deployment.finished": { alias: "shop", status: "FAILED" } }`. */
222
+ arguments?: Record<string, Record<string, unknown>>;
223
+ /** Where to continue; omit to start from now (nothing is replayed). */
224
+ cursor?: string;
225
+ /** At most this many events. */
226
+ max?: number;
227
+ }
228
+ /**
229
+ * A deployment event. `type` is the event's own name — `"service.vm_started"`,
230
+ * `"deployment.build_finished"` — exactly as MCP `list_deployment_events` and
231
+ * `wait_for_deployment` report it.
232
+ */
233
+ export interface DeploymentEvent {
234
+ id: string;
235
+ deploymentID: string;
236
+ /** The event's name as the platform writes it, e.g. `"deployment.phases"`. */
237
+ type: string;
238
+ /**
239
+ * The GraphQL enum (`SERVICE_VM_STARTED`, …), `UNKNOWN` for every name the
240
+ * enum does not list (`deployment.build_finished`, `deployment.phases`, …).
241
+ * Match on `type` instead.
242
+ */
243
+ typeEnum: DeploymentEventType;
244
+ /** @deprecated Same as `type` (before 0.8.0 `type` was the enum). */
245
+ rawType: string;
246
+ severity: SdkDeploymentEventsQuery["deploymentEvents"][number]["severity"];
247
+ message: string;
248
+ vmID: string | null;
249
+ /** The event's fields; timing events carry their phases as `*_ms` strings ({@link parseDeploymentTimings}). */
250
+ metadata: Record<string, string> | null;
251
+ createdAt: string;
252
+ }
253
+ /**
254
+ * A deploy's phases as the platform measured them: from `deployment.phases`
255
+ * (recorded when the deploy goes READY) and `deployment.build_finished`.
256
+ * Milliseconds.
257
+ */
258
+ export interface PlatformTimings {
259
+ /** Deployment created → READY, on the platform's clock. Null until `deployment.phases` is recorded. */
260
+ totalMs: number | null;
261
+ /** The build, as the build VM's host measured it. */
262
+ buildMs: number | null;
263
+ /** Promotion: health probe and alias switch (and a worker's pickup and boot). */
264
+ promoteMs: number | null;
265
+ /**
266
+ * Every other measured phase by name: the build VM's (`vm_install`,
267
+ * `vm_build`, `vm_bundle`, `vm_collect`, `image`, `vm`, `return`,
268
+ * `extract`) and the promotion's (`admission`, `worker_pickup`, `boot`, …).
269
+ */
270
+ phases: Record<string, number>;
271
+ /** `local` or `worker`. */
272
+ buildMode: string | null;
273
+ artifactTransport: string | null;
274
+ buildMemoryMiB: number | null;
275
+ }
276
+ /** Wall time this client spent on a deploy with `wait: true`. Milliseconds. */
277
+ export interface ClientTimings {
278
+ /** Sending createDeployment until its answer. */
279
+ createMs: number;
280
+ /** Its answer until the client saw the final status. */
281
+ waitMs: number;
282
+ /** `createMs + waitMs`: what the caller waited. Compare with `platform.totalMs`. */
283
+ totalMs: number;
284
+ /** Requests the wait made: one per server-side wait (up to 45 s each), or one per poll. */
285
+ waitRequests: number;
286
+ /** `"server"`: the platform's server-side wait; `"poll"`: polling (platforms without it). */
287
+ waitMode: "server" | "poll";
288
+ }
289
+ /** Both sides of a deploy's time ({@link DeployResult.timings}). */
290
+ export interface DeployTimings {
291
+ client: ClientTimings;
292
+ /** Null when the platform recorded none (yet), or with `timings: false`. */
293
+ platform: PlatformTimings | null;
294
+ }
295
+ /** A deploy's answer: the deployment, and with `wait: true` its timings. */
296
+ export type DeployResult = Deployment & {
297
+ /** With `wait: true`: the platform's phases and this client's wall time. */
298
+ timings?: DeployTimings;
299
+ };
53
300
  /**
54
301
  * A file's contents for {@link Unfazed.writeFiles}.
55
302
  *
@@ -68,16 +315,31 @@ export type FileContent = string | {
68
315
  export type FilesMap = Record<string, FileContent>;
69
316
  export interface WriteFilesOptions {
70
317
  /**
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.
318
+ * On an app with platform git: the message of the call's commit. Default
319
+ * names the files. Ignored on other apps.
320
+ */
321
+ commitMessage?: string;
322
+ /**
323
+ * When the commit waits for the git server (`commit.waiting`), resolve only
324
+ * once it landed ({@link Unfazed.waitForCommit}); `true` or the wait's
325
+ * options. The result's `landed` names the branch head then.
326
+ */
327
+ waitForCommit?: boolean | WaitOptions;
328
+ /**
329
+ * Resolve with the result even when some files failed, instead of throwing
330
+ * {@link UnfazedWriteError}. Read `failed` and `files[].error` then.
331
+ */
332
+ allowPartial?: boolean;
333
+ /**
334
+ * Max files per request. Default 100, the platform's per-call limit. A
335
+ * request is ONE commit on an app with platform git, so a map that needs
336
+ * several requests is several commits.
76
337
  */
77
338
  batchSize?: number;
78
339
  /**
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.
340
+ * Max payload bytes per request (text, or base64 as sent). Default 32 MiB,
341
+ * the platform's per-call total. Whichever limit trips first closes a
342
+ * request; a single file over it is still sent, and refused on its own.
81
343
  */
82
344
  maxBatchBytes?: number;
83
345
  signal?: AbortSignal;
@@ -97,6 +359,12 @@ export interface DeployOptions {
97
359
  wait?: boolean;
98
360
  /** Backoff/timeout knobs used when `wait` is true. */
99
361
  waitOptions?: WaitOptions;
362
+ /**
363
+ * With `wait: true`, read the platform's phase timings once the deploy is
364
+ * READY (one more request) into `result.timings.platform`. Default true;
365
+ * `false` skips the read (`timings.platform` is null).
366
+ */
367
+ timings?: boolean;
100
368
  signal?: AbortSignal;
101
369
  }
102
370
  export interface DeployServiceOptions extends DeployOptions {
@@ -115,7 +383,14 @@ export interface DeployStaticOptions extends DeployOptions {
115
383
  spaMode?: boolean;
116
384
  }
117
385
  export interface DeployBoxOptions extends DeployOptions {
118
- /** VM size. Omit for platform defaults (2048 MiB / 2 vCPU / 8 GiB disk). */
386
+ /**
387
+ * VM size. Omit a field for the platform default; the server's
388
+ * `BoxResourcesInput` is authoritative. Today: 4096 MiB RAM (min 512, max
389
+ * 16384), 4 vCPU (max 16) and a 32 GiB disk (max 128), unless the platform
390
+ * operator raised the box's limit; a node may clamp lower. `diskGiB` sizes
391
+ * the disk when it is first created; a later redeploy asking for more grows
392
+ * it before the box boots, and it never shrinks.
393
+ */
119
394
  resources?: {
120
395
  memMiB?: number;
121
396
  vcpus?: number;
@@ -319,33 +594,98 @@ export declare class Unfazed {
319
594
  updatedAt: string;
320
595
  }[]>;
321
596
  /**
322
- * Deletes a workspace and everything anchored to it — deployments, env, and
323
- * a BOX's persistent data disk. Not recoverable.
324
- *
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.
597
+ * Deletes a workspace — its files, env and secrets, and a BOX's persistent
598
+ * data disk — and, with `withDeployments`, every app it serves.
332
599
  *
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:
600
+ * A workspace can serve several apps (each `unfazed.json` `apps[]` entry, or
601
+ * several aliases deployed into it), so deleting one with its deployments is
602
+ * never implicit: **name exactly the apps it takes offline** in
603
+ * `confirmAliases`. The server compares them with the workspace's live apps
604
+ * and refuses, deleting nothing, on a missing or extra alias; the refusal
605
+ * ({@link UnfazedUnconfirmedAppsError}) lists every live app with its
606
+ * domains. The SDK never fills the list in: look it up deliberately with
607
+ * {@link liveApps} and decide. To delete one app and keep the rest, use
608
+ * {@link deleteApp}.
336
609
  *
337
610
  * ```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
611
+ * await unfazed.deleteWorkspace(ws.id); // only when no deployment is left
612
+ * await unfazed.deleteWorkspace(ws.id, { withDeployments: true, confirmAliases: ["web", "api"] });
613
+ * await unfazed.deleteWorkspace(ws.id, { withDeployments: true, confirmAliases: [] }); // serves no app
614
+ * await unfazed.deleteWorkspace(id, { ignoreNotFound: true }); // false, not a throw
341
615
  * ```
342
616
  *
617
+ * Without `withDeployments` the server refuses while ANY deployment record
618
+ * is left — FAILED ones included — with `still has deployments; delete them
619
+ * first`. A workspace whose deploys all failed serves no app: delete it with
620
+ * `withDeployments: true, confirmAliases: []`.
621
+ *
622
+ * What is deleted disappears at once — gone from your views, its names and
623
+ * aliases free, its VMs stopped — but the platform keeps it for 30 days:
624
+ * within that time support (the platform operator) can restore it exactly
625
+ * as it was. You cannot restore it yourself; ask support. After 30 days it
626
+ * is erased for good.
627
+ *
343
628
  * With `ignoreNotFound` a workspace that is absent (or belongs to someone
344
629
  * else — the server returns one indistinguishable sentinel for both, on
345
630
  * purpose) resolves to `false` instead of throwing, which makes teardown in
346
631
  * a `finally` block idempotent.
347
632
  */
348
633
  deleteWorkspace(workspaceId: string, options?: DeleteWorkspaceOptions): Promise<boolean>;
634
+ /**
635
+ * The apps a workspace serves right now, by alias, with the deployment
636
+ * serving each and its custom domains: everything
637
+ * `deleteWorkspace(id, { withDeployments: true, … })` takes offline, and the
638
+ * exact list its `confirmAliases` must name.
639
+ *
640
+ * It is a lookup for you to read, not something to pipe into a delete
641
+ * unseen: check that the apps are the ones you mean to take down.
642
+ *
643
+ * ```ts
644
+ * const live = await unfazed.liveApps(ws.id); // [{ alias: "web", deploymentID, domains: [] }, …]
645
+ * ```
646
+ *
647
+ * Throws {@link UnfazedNotFoundError} when the workspace is absent or not
648
+ * visible to this token.
649
+ */
650
+ liveApps(workspaceId: string, options?: RequestOptions): Promise<LiveApp[]>;
651
+ /**
652
+ * Deletes ONE app of a workspace and nothing else: every deployment of the
653
+ * alias there (live release, retained releases, failed and building
654
+ * attempts — its VM stops, its URL and custom domains stop serving), its App
655
+ * State, its app-scoped env and secrets, and its integration bindings. The
656
+ * workspace, its files and defaults, its other apps (`otherApps` in the
657
+ * result) and a box's data disk stay.
658
+ *
659
+ * ```ts
660
+ * const gone = await unfazed.deleteApp({ workspaceId: ws.id, alias: "docs" });
661
+ * gone.deployments; // records deleted
662
+ * gone.otherApps; // the workspace's apps still serving, untouched
663
+ * ```
664
+ *
665
+ * In a git-linked workspace pushes and syncs skip the alias while
666
+ * `unfazed.json` `apps[]` still declares it (`skippedOnSync: true`): remove
667
+ * it from `apps[]`, or bring it back with the GraphQL `syncRepo` mutation's
668
+ * `restoreApps` (through {@link client}). The server refuses while a git
669
+ * import of the workspace runs. Owner only.
670
+ *
671
+ * What is deleted disappears at once, but the platform keeps the app for 30
672
+ * days: within that time support (the platform operator) can restore it
673
+ * exactly as it was. You cannot restore it yourself; ask support. After 30
674
+ * days it is erased for good. A custom domain's record stays with the alias,
675
+ * so a restored app serves it again.
676
+ *
677
+ * A delete that stops part-way throws with the server's message, which says
678
+ * so; deleting again finishes it. With `ignoreNotFound`, an app that is not
679
+ * in the workspace (already deleted) or a workspace that is not visible
680
+ * resolves to `null` instead of throwing.
681
+ */
682
+ deleteApp(app: AppRef, options: DeleteAppOptions & {
683
+ ignoreNotFound: true;
684
+ }): Promise<DeleteAppResult | null>;
685
+ deleteApp(app: AppRef, options?: DeleteAppOptions & {
686
+ ignoreNotFound?: false;
687
+ }): Promise<DeleteAppResult>;
688
+ deleteApp(app: AppRef, options?: DeleteAppOptions): Promise<DeleteAppResult | null>;
349
689
  /**
350
690
  * Returns the workspace named `name`, creating it only if it does not exist.
351
691
  *
@@ -371,33 +711,58 @@ export declare class Unfazed {
371
711
  updatedAt: string;
372
712
  }>;
373
713
  /**
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.
714
+ * Writes many files in ONE request — and, on an app with platform git, ONE
715
+ * commit — exactly as MCP `write_workspace_files` does.
380
716
  *
381
717
  * ```ts
382
- * await unfazed.writeFiles(ws.id, {
718
+ * const out = await unfazed.writeFiles(ws.id, {
383
719
  * "server.ts": serverCode,
384
- * "unfazed.json": JSON.stringify({ entry: "server.ts" }),
720
+ * "package.json": JSON.stringify(pkg),
385
721
  * "public/logo.png": { bytes: pngBytes },
386
- * });
722
+ * }, { commitMessage: "Add the server" });
723
+ * out.commit; // { sha, committed, waiting, conflicts, writeSHA, … } or null
387
724
  * ```
388
725
  *
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.
726
+ * A call is not transactional: files are written in order, each atomically,
727
+ * and a file's failure does not undo the others. When any file fails this
728
+ * throws {@link UnfazedWriteError} (its `result` lists what was written and
729
+ * `failed` what was not; retry only those), unless `allowPartial`. A
730
+ * repeated path keeps its last content.
731
+ *
732
+ * On an app with platform git the commit is made before any file is
733
+ * written: if it fails, the request fails and nothing is written. `commit`
734
+ * may come back `waiting` — the files are saved and the commit lands in
735
+ * order; pass `waitForCommit: true` (or call {@link waitForCommit}) to
736
+ * resolve only once it has. See {@link FileCommit} for saved, committed and
737
+ * deployed.
738
+ *
739
+ * One request carries up to 100 files and 32 MiB; a bigger map is split
740
+ * into several requests, each its own commit (`commits`). On a platform
741
+ * older than the batch write the SDK falls back to one mutation per file
742
+ * (one commit each) and reports `commit: null`.
393
743
  */
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. */
744
+ writeFiles(workspaceId: string, files: FilesMap, options?: WriteFilesOptions): Promise<WriteFilesResult>;
745
+ /**
746
+ * Writes one text file: {@link writeFiles} with one entry, so it is one
747
+ * commit with `commitMessage` on an app with platform git, and the answer
748
+ * carries that commit. Throws {@link UnfazedWriteError} when it is refused.
749
+ */
750
+ writeFile(workspaceId: string, path: string, text: string, options?: Omit<WriteFilesOptions, "allowPartial" | "batchSize" | "maxBatchBytes">): Promise<WrittenFile>;
751
+ /** Writes one binary file (raw bytes or base64), like {@link writeFile}. */
398
752
  writeBinaryFile(workspaceId: string, path: string, content: Uint8Array | ArrayBuffer | {
399
753
  base64: string;
400
- }, options?: RequestOptions): Promise<WorkspaceFile>;
754
+ }, options?: Omit<WriteFilesOptions, "allowPartial" | "batchSize" | "maxBatchBytes">): Promise<WrittenFile>;
755
+ /**
756
+ * Waits until no file write of the app waits for its commit: platform git
757
+ * commits a write at once, but when the git server is slow the write is
758
+ * saved and its commit `waiting`, landing in order. Resolves with the
759
+ * branch head then — every write made before the call is in its history.
760
+ * An app without platform git resolves at once (`head: null`).
761
+ *
762
+ * Default timeout 2 minutes; throws {@link UnfazedTimeoutError} past it
763
+ * (the writes stay saved, and still land).
764
+ */
765
+ waitForCommit(workspaceId: string, waitOptions?: WaitOptions): Promise<CommitsLanded>;
401
766
  /** Lists workspace files (paginated). */
402
767
  listFiles(workspaceId: string, input?: {
403
768
  limit?: number;
@@ -453,9 +818,9 @@ export declare class Unfazed {
453
818
  * takes over the alias when it goes READY. Omitting `alias` lets the server
454
819
  * generate one, which means a different app on every call.
455
820
  */
456
- deployService(options: DeployServiceOptions): Promise<Deployment>;
821
+ deployService(options: DeployServiceOptions): Promise<DeployResult>;
457
822
  /** Deploys the workspace as a STATIC site. */
458
- deployStatic(options: DeployStaticOptions): Promise<Deployment>;
823
+ deployStatic(options: DeployStaticOptions): Promise<DeployResult>;
459
824
  /**
460
825
  * Deploys a BOX: a persistent always-on Linux VM with a data disk that
461
826
  * survives stops and redeploys. A dev machine, not an HTTP service. The
@@ -480,19 +845,31 @@ export declare class Unfazed {
480
845
  * await unfazed.deployBox({ workspaceId: ws.id, alias: "dev" }); // re-runnable
481
846
  * ```
482
847
  */
483
- deployBox(options: DeployBoxOptions): Promise<Deployment>;
848
+ deployBox(options: DeployBoxOptions): Promise<DeployResult>;
484
849
  /** Escape hatch: deploy with a raw CreateDeploymentInput. */
485
- deploy(input: CreateDeploymentInput, options?: Omit<DeployOptions, "workspaceId">): Promise<Deployment>;
850
+ deploy(input: CreateDeploymentInput, options?: Omit<DeployOptions, "workspaceId">): Promise<DeployResult>;
486
851
  /**
487
- * Polls a deployment until it is READY, throwing on FAILED or timeout.
852
+ * Waits until a deployment is READY, throwing {@link UnfazedDeployError} on
853
+ * FAILED or timeout (default 10 minutes).
488
854
  *
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`.
855
+ * It waits SERVER-SIDE (MCP `wait_for_deployment`): each request blocks on
856
+ * the control plane until the deployment leaves BUILDING — woken by the
857
+ * deploy pipeline itself — or 45 s pass, and comes back with the full
858
+ * deployment; so READY reaches you one round trip after it happens, not at
859
+ * the next poll. A NEW app that starts directly on its server is READY
860
+ * before it serves there; the wait covers that landing too. On a platform
861
+ * without the server-side wait, or when the platform refuses one because
862
+ * the account already holds its cap of waits in progress (16), it polls
863
+ * instead (`waitOptions.mode: "poll"` forces that), every 0.5 s backing
864
+ * off to 3 s by default.
494
865
  */
495
866
  waitForDeployment(deploymentId: string, waitOptions?: WaitOptions): Promise<Deployment>;
867
+ /**
868
+ * The platform's measured phases of a deploy ({@link PlatformTimings}),
869
+ * from its timing events. Null when it recorded none (a deploy that is
870
+ * still building, or one from before timings existed).
871
+ */
872
+ getDeploymentTimings(deploymentId: string, options?: RequestOptions): Promise<PlatformTimings | null>;
496
873
  /** Reads one deployment by id. Null when absent. */
497
874
  getDeployment(deploymentId: string, options?: RequestOptions): Promise<Deployment | null>;
498
875
  /** Reads the deployment currently serving an alias. Null when absent. */
@@ -566,17 +943,23 @@ export declare class Unfazed {
566
943
  stderrBytes: number;
567
944
  tailBytes: number;
568
945
  }>;
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
- }[]>;
946
+ /**
947
+ * Deployment lifecycle events — build, boot, probe, cron, restarts — the
948
+ * most recent `limit` (default 20), oldest first.
949
+ *
950
+ * `type` is the event's own name, as MCP reports it:
951
+ * `"deployment.build_finished"`, `"deployment.phases"`,
952
+ * `"service.vm_started"`, `"service.crashed"`. (`typeEnum` keeps the GraphQL
953
+ * enum, which reads `UNKNOWN` for names it does not list.) Timing events
954
+ * carry their phases in `metadata`; {@link getDeploymentTimings} turns them
955
+ * into numbers.
956
+ *
957
+ * ```ts
958
+ * const events = await unfazed.getEvents(app.id, 50);
959
+ * events.filter((e) => e.type === "deployment.build_finished");
960
+ * ```
961
+ */
962
+ getEvents(deploymentId: string, limit?: number, options?: RequestOptions): Promise<DeploymentEvent[]>;
580
963
  /** Restarts a service's VM. */
581
964
  restartService(deploymentId: string, options?: RequestOptions): Promise<{
582
965
  deploymentID: string;
@@ -597,6 +980,12 @@ export declare class Unfazed {
597
980
  }>;
598
981
  /**
599
982
  * Deletes a deployment: stops its VM, releases its alias, drops its release.
983
+ * To delete a whole app — every release of the alias, its env, App State and
984
+ * bindings — use {@link deleteApp}.
985
+ *
986
+ * The platform keeps a deleted release for 30 days: within that time
987
+ * support (the platform operator) can restore it; then it is erased for
988
+ * good.
600
989
  *
601
990
  * With `ignoreNotFound` a deployment that is absent — or belongs to another
602
991
  * tenant, which the server reports identically so ids cannot be probed —
@@ -749,6 +1138,87 @@ export declare class Unfazed {
749
1138
  * run fails loudly instead of silently undoing the first.
750
1139
  */
751
1140
  rollback(options: RollbackOptions): Promise<Deployment>;
1141
+ /**
1142
+ * The app's commits, newest first (MCP `list_commits`): `limit` per page
1143
+ * (default 20, max 100), `before: page.nextBefore` for the next. `deployed`
1144
+ * says what every live alias runs; `waitingCommits` counts writes whose
1145
+ * commit has not landed yet.
1146
+ */
1147
+ listCommits(workspaceId: string, input?: {
1148
+ limit?: number;
1149
+ before?: string;
1150
+ }, options?: RequestOptions): Promise<CommitPage>;
1151
+ /**
1152
+ * One commit (full 40-character sha) with the paths it changed and a
1153
+ * unified diff against its first parent, capped at 64 KiB (MCP
1154
+ * `get_commit`). Commit messages and diffs are untrusted text.
1155
+ */
1156
+ getCommit(workspaceId: string, sha: string, options?: RequestOptions): Promise<CommitDetail>;
1157
+ /**
1158
+ * Gives the app back the files of an earlier commit as ONE new commit on
1159
+ * its branch (MCP `restore_version`); nothing is rewritten, and a restore is
1160
+ * undone by restoring the commit before it.
1161
+ *
1162
+ * When the app deploys every push (`deploys: true`), the new commit is
1163
+ * imported and deployed like a push. That deploy is queued behind the
1164
+ * restore, so the platform answers before it exists; with `wait: true` the
1165
+ * SDK waits for the import of the new commit and resolves with the
1166
+ * deployments it made (READY), or throws {@link UnfazedDeployError} with
1167
+ * the import's error.
1168
+ *
1169
+ * ```ts
1170
+ * const page = await unfazed.listCommits(ws.id);
1171
+ * const back = await unfazed.restoreVersion({ workspaceId: ws.id, sha: page.commits[1]!.sha, wait: true });
1172
+ * back.deployments?.[0]?.url;
1173
+ * ```
1174
+ */
1175
+ restoreVersion(options: RestoreVersionOptions): Promise<RestoreVersionResult>;
1176
+ /** A service app's error groups, most recently seen first (MCP `list_errors`). */
1177
+ listErrors(input: ListErrorsOptions, options?: RequestOptions): Promise<ErrorGroupPage>;
1178
+ /**
1179
+ * One error group with its latest occurrence whole (a Sentry event: stack,
1180
+ * request), its recent occurrences and the deployments it was seen in (MCP
1181
+ * `get_error_group`).
1182
+ */
1183
+ getErrorGroup(ref: ErrorGroupRef, options?: RequestOptions): Promise<ErrorGroupDetail>;
1184
+ /**
1185
+ * Marks a group resolved in the app's current deployment, once its fix is
1186
+ * deployed (MCP `resolve_error_group`); another deployment reporting it
1187
+ * opens it again. Only on your human's say-so or a fix you verified.
1188
+ */
1189
+ resolveErrorGroup(ref: ErrorGroupRef, options?: RequestOptions): Promise<ErrorGroup>;
1190
+ /** Mutes an expected group: still counted, never regresses or notifies (MCP `mute_error_group`). */
1191
+ muteErrorGroup(ref: ErrorGroupRef, options?: RequestOptions): Promise<ErrorGroup>;
1192
+ /** Opens a resolved or muted group again (MCP `reopen_error_group`). */
1193
+ reopenErrorGroup(ref: ErrorGroupRef, options?: RequestOptions): Promise<ErrorGroup>;
1194
+ /** The event types, with each one's filters and payload schema (MCP `list_event_types`). */
1195
+ listEventTypes(options?: RequestOptions): Promise<EventType[]>;
1196
+ /**
1197
+ * Events since `cursor` (MCP `poll_events`). Omit `cursor` to start from now
1198
+ * — nothing is replayed — and pass the returned `cursor` back: at once while
1199
+ * `hasMore`, otherwise after `nextPollMs`, or to {@link waitForEvents}.
1200
+ * `truncated` means events may be missing: re-read state.
1201
+ */
1202
+ pollEvents(input?: EventsInput, options?: RequestOptions): Promise<EventPage>;
1203
+ /**
1204
+ * {@link pollEvents} that waits server-side until an event arrives (MCP
1205
+ * `wait_for_events`), for up to `timeoutMs` (default 45 s; longer waits are
1206
+ * several server waits of up to 45 s, continuing from each cursor). A
1207
+ * timeout is not an error: the page has `timedOut: true`, no events and a
1208
+ * cursor to wait again with. The platform caps an account's waits in
1209
+ * progress (16, deploy waits included); past it this throws an error that
1210
+ * `isTooManyWaits` recognizes: poll then.
1211
+ *
1212
+ * ```ts
1213
+ * let page = await unfazed.pollEvents({ names: ["deployment.finished"] }); // from now
1214
+ * page = await unfazed.waitForEvents({ names: ["deployment.finished"], cursor: page.cursor, timeoutMs: 120_000 });
1215
+ * ```
1216
+ */
1217
+ waitForEvents(input?: EventsInput & {
1218
+ timeoutMs?: number;
1219
+ }, options?: RequestOptions & {
1220
+ now?: () => number;
1221
+ }): Promise<EventPage>;
752
1222
  /**
753
1223
  * Starts an agent run.
754
1224
  *
@@ -936,3 +1406,10 @@ export declare class Unfazed {
936
1406
  truncated: boolean;
937
1407
  }>;
938
1408
  }
1409
+ /**
1410
+ * A deploy's phases from its events: `deployment.phases` (written when it went
1411
+ * READY: build, promotion and its split, total) over `deployment.build_finished`
1412
+ * (the build VM's own phases). Every `*_ms` field becomes a number. Null when
1413
+ * neither event is there.
1414
+ */
1415
+ export declare function parseDeploymentTimings(events: readonly Pick<DeploymentEvent, "type" | "metadata">[]): PlatformTimings | null;