@samebase/alchemy-convex 0.2.0 → 0.3.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.
@@ -0,0 +1,409 @@
1
+ // Convex.Deployment: one deployment of a Convex project, with the URLs that a
2
+ // frontend or a Worker needs.
3
+ //
4
+ // - `prod` is the project's default production deployment. Convex creates it
5
+ // with the project, so this resource never creates one. It takes the
6
+ // deployment over without --adopt: it never changes or deletes a production
7
+ // deployment, so the takeover cannot lose data. It follows the default:
8
+ // when another production deployment becomes the default, the next deploy
9
+ // outputs that one. To delete a production deployment, delete its project
10
+ // (Convex.Project with RemovalPolicy.destroy()). Delete reads the live type
11
+ // first, so a dev or preview deployment that became a production
12
+ // deployment is not deleted either.
13
+ // - `dev` and `preview` are created with POST
14
+ // /projects/{project_id}/create_deployment and the `name` prop as the
15
+ // reference. Convex keeps a dev reference as given. For a preview, Convex
16
+ // records `name` as the preview identifier, the name that
17
+ // `npx convex deploy --preview-name <name>` finds, and derives the reference
18
+ // from it (`preview/<slug>`). A create for a preview name that already has
19
+ // a preview deployment deletes that deployment and creates a new one, so
20
+ // reconcile always looks the name up first and never creates over an
21
+ // existing deployment. A dev or preview deployment that exists but is not
22
+ // in state belongs to someone else: the resource takes it over only with
23
+ // --adopt.
24
+ // - `projectId`, `type`, and `name` are the identity. A change fails with
25
+ // DeploymentIdentityChange; the resource is never replaced.
26
+ // - The removal policy is `retain` for every type. A preview deployment
27
+ // expires on Convex's schedule (14 days by default). To delete a dev or
28
+ // preview deployment with the resource, add RemovalPolicy.destroy().
29
+ //
30
+ // Patterns from Confect `packages/alchemy/src/Deployment.ts`
31
+ // (https://github.com/rjdellecese/confect, ISC license).
32
+ import { Resource } from "alchemy";
33
+ import { OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
34
+ import { isResolved } from "alchemy/Diff";
35
+ import * as Provider from "alchemy/Provider";
36
+ import * as Effect from "effect/Effect";
37
+ import * as Schema from "effect/Schema";
38
+ import { shouldAdopt } from "./Adoption.ts";
39
+ import { absentAsUndefined, ManagementApi, retryIdempotentWrite } from "./ManagementApi.ts";
40
+ import type { Providers } from "./Providers.ts";
41
+
42
+ export type DeploymentProps =
43
+ | {
44
+ /** Numeric project id, such as `project.projectId`. Cannot change. */
45
+ readonly projectId: number;
46
+ /** The project's default production deployment. Cannot change. */
47
+ readonly type: "prod";
48
+ }
49
+ | {
50
+ /** Numeric project id, such as `project.projectId`. Cannot change. */
51
+ readonly projectId: number;
52
+ /** A dev deployment or a preview deployment that this resource creates. Cannot change. */
53
+ readonly type: "dev" | "preview";
54
+ /**
55
+ * The dev reference, such as `staging`, or the preview name, such as a
56
+ * pull request or stage name. Unique in the project. Cannot change.
57
+ */
58
+ readonly name: string;
59
+ };
60
+
61
+ export interface DeploymentAttributes {
62
+ readonly projectId: number;
63
+ readonly type: DeploymentProps["type"];
64
+ /** Deployment name, such as `happy-animal-123`. */
65
+ readonly name: string;
66
+ /** The reference that Convex lists, such as `production`, `staging`, or `preview/pr-42`. */
67
+ readonly reference: string;
68
+ /** The preview name of a preview deployment. Undefined for prod and dev. */
69
+ readonly previewName: string | undefined;
70
+ /** Client URL, such as `https://happy-animal-123.convex.cloud`. */
71
+ readonly url: string;
72
+ /** HTTP actions URL, such as `https://happy-animal-123.convex.site`. */
73
+ readonly siteUrl: string;
74
+ }
75
+
76
+ export type Deployment = Resource<
77
+ "Convex.Deployment",
78
+ DeploymentProps,
79
+ DeploymentAttributes,
80
+ never,
81
+ Providers
82
+ >;
83
+ export const Deployment = Resource<Deployment>("Convex.Deployment", {
84
+ defaultRemovalPolicy: "retain",
85
+ });
86
+
87
+ /** The props name another deployment than the one in state. */
88
+ export class DeploymentIdentityChange extends Schema.TaggedError<DeploymentIdentityChange>()(
89
+ "DeploymentIdentityChange",
90
+ {
91
+ /** Name of the deployment in state, such as `happy-animal-123`. */
92
+ deployment: Schema.String,
93
+ field: Schema.Literals(["projectId", "type", "name"]),
94
+ current: Schema.String,
95
+ requested: Schema.String,
96
+ },
97
+ ) {
98
+ override get message() {
99
+ return `Convex.Deployment cannot change the ${this.field} of deployment ${this.deployment} from ${this.current} to ${this.requested}. Set ${this.field} back to ${this.current}. For another deployment, create a new resource with a new logical id.`;
100
+ }
101
+ }
102
+
103
+ /** The project has no default production deployment for a `prod` resource to take over. */
104
+ export class ProductionDeploymentNotFound extends Schema.TaggedError<ProductionDeploymentNotFound>()(
105
+ "ProductionDeploymentNotFound",
106
+ { projectId: Schema.Number },
107
+ ) {
108
+ override get message() {
109
+ return `Project ${this.projectId} has no default production deployment, and Convex.Deployment does not create one. Create it in the Convex dashboard, or with \`npx convex deployment create --type prod --default\` in the project, then run the deploy again.`;
110
+ }
111
+ }
112
+
113
+ /** More than one deployment of the project has the name, so the lookup cannot choose one. */
114
+ export class AmbiguousDeployment extends Schema.TaggedError<AmbiguousDeployment>()(
115
+ "AmbiguousDeployment",
116
+ {
117
+ projectId: Schema.Number,
118
+ type: Schema.Literals(["dev", "preview"]),
119
+ name: Schema.String,
120
+ deployments: Schema.Array(Schema.String),
121
+ },
122
+ ) {
123
+ override get message() {
124
+ return `Project ${this.projectId} has ${this.deployments.length} ${this.type} deployments named "${this.name}" (${this.deployments.join(", ")}). Convex.Deployment does not choose one. Delete the extra deployments in the Convex dashboard, or use another name.`;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * The URL of a Convex cloud deployment: `https://<name>.convex.cloud`, or
130
+ * `https://<name>.<region>.convex.cloud` outside US East.
131
+ */
132
+ const CLOUD_URL =
133
+ /^https:\/\/[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)?\.convex\.cloud$/;
134
+
135
+ /**
136
+ * The HTTP actions URL of a deployment: the client URL with `.convex.site`
137
+ * for `.convex.cloud`, as Convex sets CONVEX_SITE_URL. Custom domains are
138
+ * not covered.
139
+ */
140
+ export const siteUrlOf = (url: string): string => url.replace(/\.convex\.cloud$/, ".convex.site");
141
+
142
+ /** One row of list_deployments, GET /deployments/{name}, and create_deployment. Only the fields the provider uses. */
143
+ export const DeploymentRow = Schema.Struct({
144
+ name: Schema.String,
145
+ projectId: Schema.Number,
146
+ deploymentType: Schema.String,
147
+ isDefault: Schema.Boolean,
148
+ reference: Schema.String,
149
+ /** Set in list_deployments and create_deployment rows only: GET /deployments/{name} answers null. */
150
+ previewIdentifier: Schema.optionalKey(Schema.NullOr(Schema.String)),
151
+ deploymentUrl: Schema.String.check(Schema.isPattern(CLOUD_URL)),
152
+ });
153
+ type DeploymentRow = typeof DeploymentRow.Type;
154
+ const DeploymentRows = Schema.Array(DeploymentRow);
155
+
156
+ const attributesOf = (
157
+ row: DeploymentRow,
158
+ type: DeploymentProps["type"],
159
+ previewName: string | undefined,
160
+ ): DeploymentAttributes => ({
161
+ projectId: row.projectId,
162
+ type,
163
+ name: row.name,
164
+ reference: row.reference,
165
+ previewName: type === "preview" ? previewName : undefined,
166
+ url: row.deploymentUrl,
167
+ siteUrl: siteUrlOf(row.deploymentUrl),
168
+ });
169
+
170
+ /** The `name` prop of the deployment in state: the dev reference or the preview name. */
171
+ const nameOf = (attributes: DeploymentAttributes) =>
172
+ attributes.type === "dev"
173
+ ? attributes.reference
174
+ : attributes.type === "preview"
175
+ ? attributes.previewName
176
+ : undefined;
177
+
178
+ /**
179
+ * Fails with {@link DeploymentIdentityChange} when the props name another
180
+ * deployment than `current`: the attributes in state, or a live row.
181
+ */
182
+ const checkIdentity = (current: DeploymentAttributes, news: DeploymentProps) => {
183
+ const change = (field: "projectId" | "type" | "name", from: string, to: string) =>
184
+ Effect.fail(
185
+ new DeploymentIdentityChange({
186
+ deployment: current.name,
187
+ field,
188
+ current: from,
189
+ requested: to,
190
+ }),
191
+ );
192
+ if (current.projectId !== news.projectId) {
193
+ return change("projectId", String(current.projectId), String(news.projectId));
194
+ }
195
+ if (current.type !== news.type) return change("type", current.type, news.type);
196
+ const currentName = nameOf(current);
197
+ if (news.type !== "prod" && currentName !== news.name) {
198
+ return change("name", currentName ?? "(none)", news.name);
199
+ }
200
+ return Effect.void;
201
+ };
202
+
203
+ export const DeploymentProvider = () =>
204
+ Provider.effect(
205
+ Deployment,
206
+ Effect.gen(function* () {
207
+ const api = yield* ManagementApi;
208
+
209
+ /** The live deployment, or undefined after a 404. GET /deployments/{name} answers no preview identifier. */
210
+ const getByName = (name: string) =>
211
+ absentAsUndefined(
212
+ api.request("get deployment", (client) =>
213
+ client.GET("/deployments/{deployment_name}", {
214
+ params: { path: { deployment_name: name } },
215
+ }),
216
+ ),
217
+ ).pipe(
218
+ Effect.map((body) =>
219
+ body === undefined ? undefined : Schema.decodeUnknownSync(DeploymentRow)(body),
220
+ ),
221
+ );
222
+
223
+ const listDeployments = (
224
+ projectId: number,
225
+ type: DeploymentProps["type"],
226
+ isDefault?: boolean,
227
+ ) =>
228
+ api
229
+ .request("list deployments", (client) =>
230
+ client.GET("/projects/{project_id}/list_deployments", {
231
+ params: {
232
+ path: { project_id: projectId },
233
+ query: { deploymentType: type, ...(isDefault === undefined ? {} : { isDefault }) },
234
+ },
235
+ }),
236
+ )
237
+ .pipe(Effect.map((body) => Schema.decodeUnknownSync(DeploymentRows)(body)));
238
+
239
+ /** The project's default production deployment, or undefined. */
240
+ const production = (projectId: number) =>
241
+ listDeployments(projectId, "prod", true).pipe(
242
+ Effect.map((rows) => {
243
+ const row = rows.find(
244
+ (candidate) => candidate.deploymentType === "prod" && candidate.isDefault,
245
+ );
246
+ return row === undefined ? undefined : attributesOf(row, "prod", undefined);
247
+ }),
248
+ );
249
+
250
+ /**
251
+ * The one dev deployment with this reference, or the one preview
252
+ * deployment with this preview name, or undefined.
253
+ */
254
+ const findByName = (projectId: number, type: "dev" | "preview", name: string) =>
255
+ Effect.gen(function* () {
256
+ const matches = (yield* listDeployments(projectId, type)).filter(
257
+ (row) =>
258
+ row.deploymentType === type &&
259
+ (type === "dev" ? row.reference === name : row.previewIdentifier === name),
260
+ );
261
+ if (matches.length > 1) {
262
+ return yield* new AmbiguousDeployment({
263
+ projectId,
264
+ type,
265
+ name,
266
+ deployments: matches.map((row) => row.name),
267
+ });
268
+ }
269
+ return matches[0];
270
+ });
271
+
272
+ /** The deployment that the props name, for a resource without state. */
273
+ const lookup = (news: DeploymentProps) =>
274
+ news.type === "prod"
275
+ ? production(news.projectId)
276
+ : findByName(news.projectId, news.type, news.name).pipe(
277
+ Effect.map((row) =>
278
+ row === undefined ? undefined : attributesOf(row, news.type, news.name),
279
+ ),
280
+ );
281
+
282
+ return {
283
+ // `name`, `reference`, and the URLs change when an expired preview is
284
+ // created again, or when another production deployment becomes the
285
+ // default.
286
+ stables: ["projectId", "type", "previewName"],
287
+
288
+ // Compares with the attributes: after a refused change, `olds` are
289
+ // the refused props.
290
+ diff: Effect.fn(function* ({ news, output }) {
291
+ if (!isResolved(news) || output === undefined) return undefined;
292
+ yield* checkIdentity(output, news);
293
+ // `prod` follows the project's default production deployment,
294
+ // which can change in Convex. Reconcile then returns the new one.
295
+ if (news.type === "prod") {
296
+ // A 404 for a deleted project plans an update; reconcile then reports it.
297
+ const current = yield* absentAsUndefined(production(news.projectId));
298
+ return current?.name === output.name ? undefined : ({ action: "update" } as const);
299
+ }
300
+ // A preview expires, and anyone can delete a deployment. Reconcile
301
+ // then finds or creates it again, so the stack never hands out the
302
+ // URL of a deployment that is gone.
303
+ const live = yield* getByName(output.name);
304
+ return live === undefined ? ({ action: "update" } as const) : undefined;
305
+ }),
306
+
307
+ read: Effect.fn(function* ({ olds, output }) {
308
+ if (output?.type === "prod") {
309
+ // 404: the project is gone, and so is its production deployment.
310
+ return yield* absentAsUndefined(production(output.projectId));
311
+ }
312
+ if (output !== undefined) {
313
+ const live = yield* getByName(output.name);
314
+ return live === undefined
315
+ ? undefined
316
+ : attributesOf(live, output.type, output.previewName);
317
+ }
318
+ const found = yield* lookup(olds);
319
+ if (found === undefined) return undefined;
320
+ // The production deployment comes with the project, and this
321
+ // resource never changes or deletes it, so it is ours to read.
322
+ // A dev or preview deployment that is not in state is someone
323
+ // else's until the user passes --adopt.
324
+ return olds.type === "prod" ? found : Unowned(found);
325
+ }),
326
+
327
+ reconcile: Effect.fn(function* ({ fqn, news, output }) {
328
+ // Also here: with an unresolved prop in plan, diff could not check.
329
+ if (output !== undefined) yield* checkIdentity(output, news);
330
+ if (news.type === "prod") {
331
+ // Always the current default, also when another production
332
+ // deployment became the default after the last deploy.
333
+ const current = yield* production(news.projectId);
334
+ if (current === undefined) {
335
+ return yield* new ProductionDeploymentNotFound({ projectId: news.projectId });
336
+ }
337
+ return current;
338
+ }
339
+ if (output !== undefined) {
340
+ const live = yield* getByName(output.name);
341
+ if (live !== undefined) {
342
+ if (live.deploymentType !== output.type) {
343
+ return yield* new DeploymentIdentityChange({
344
+ deployment: live.name,
345
+ field: "type",
346
+ current: live.deploymentType,
347
+ requested: news.type,
348
+ });
349
+ }
350
+ const current = attributesOf(live, output.type, output.previewName);
351
+ // The live deployment can have moved to another project.
352
+ yield* checkIdentity(current, news);
353
+ return current;
354
+ }
355
+ // The deployment in state is gone, such as an expired preview.
356
+ // Find or create it below like a resource without state.
357
+ }
358
+ // Never create over an existing deployment: a preview create
359
+ // replaces the preview deployment with the same name. Another
360
+ // writer can still create one between this lookup and the create.
361
+ const existing = yield* lookup(news);
362
+ if (existing !== undefined) {
363
+ if (!(yield* shouldAdopt(fqn))) {
364
+ return yield* new OwnedBySomeoneElse({
365
+ message: `Project ${news.projectId} already has ${news.type} deployment ${existing.name} named "${news.name}". Re-run with --adopt to take it over, or use another name.`,
366
+ resourceType: Deployment.Type,
367
+ physicalName: existing.name,
368
+ });
369
+ }
370
+ return existing;
371
+ }
372
+ const created = yield* api.request("create deployment", (client) =>
373
+ client.POST("/projects/{project_id}/create_deployment", {
374
+ params: { path: { project_id: news.projectId } },
375
+ body: { type: news.type, reference: news.name },
376
+ }),
377
+ );
378
+ return attributesOf(
379
+ Schema.decodeUnknownSync(DeploymentRow)(created),
380
+ news.type,
381
+ news.name,
382
+ );
383
+ }),
384
+
385
+ // Runs only with RemovalPolicy.destroy(): the default policy is `retain`.
386
+ delete: Effect.fn(function* ({ output }) {
387
+ const keep = Effect.logWarning(
388
+ `Convex.Deployment does not delete production deployment ${output.name}: it belongs to project ${output.projectId}. To delete it, delete the project with RemovalPolicy.destroy() on Convex.Project.`,
389
+ );
390
+ if (output.type === "prod") return yield* keep;
391
+ // The type in state can be old: a dev or preview deployment can
392
+ // become a production deployment in Convex. The live type decides.
393
+ const live = yield* getByName(output.name);
394
+ // 404: an earlier attempt, the expiry, or someone else deleted it.
395
+ if (live === undefined) return;
396
+ if (live.deploymentType === "prod") return yield* keep;
397
+ yield* absentAsUndefined(
398
+ retryIdempotentWrite(
399
+ api.requestVoid("delete deployment", (client) =>
400
+ client.POST("/deployments/{deployment_name}/delete", {
401
+ params: { path: { deployment_name: output.name } },
402
+ }),
403
+ ),
404
+ ),
405
+ );
406
+ }),
407
+ };
408
+ }),
409
+ );
@@ -33,6 +33,7 @@ import {
33
33
  ManagementApi,
34
34
  type ManagementApiService,
35
35
  retryIdempotentWrite,
36
+ settle,
36
37
  settleVoid,
37
38
  } from "./ManagementApi.ts";
38
39
  import type { Providers } from "./Providers.ts";
@@ -49,26 +50,40 @@ export const findVariable = (
49
50
  Object.hasOwn(list.environmentVariables, name) ? { deployment, name } : undefined;
50
51
 
51
52
  /**
52
- * One attempt to set one variable, or to remove it when `value` is null. The
53
- * deployment answers 200 with an empty body, so this reads no response data.
53
+ * One attempt to set variables, or to remove those whose `value` is null, in
54
+ * one call. The deployment answers 200 with an empty body, so this reads no
55
+ * response data.
54
56
  */
55
- const writeVariable = (
57
+ export const writeVariables = (
56
58
  deployment: string,
57
59
  deployKey: Redacted.Redacted<string>,
58
- name: string,
59
- value: string | Redacted.Redacted<string> | null,
60
+ changes: ReadonlyArray<{
61
+ readonly name: string;
62
+ readonly value: string | Redacted.Redacted<string> | null;
63
+ }>,
60
64
  ) =>
61
65
  settleVoid("update environment variables", () =>
62
66
  createDeploymentClient(deployment, Redacted.value(deployKey)).POST(
63
67
  "/update_environment_variables",
64
68
  {
65
69
  body: {
66
- changes: [{ name, value: Redacted.isRedacted(value) ? Redacted.value(value) : value }],
70
+ changes: changes.map(({ name, value }) => ({
71
+ name,
72
+ value: Redacted.isRedacted(value) ? Redacted.value(value) : value,
73
+ })),
67
74
  },
68
75
  },
69
76
  ),
70
77
  );
71
78
 
79
+ /** One attempt to set one variable, or to remove it when `value` is null. */
80
+ const writeVariable = (
81
+ deployment: string,
82
+ deployKey: Redacted.Redacted<string>,
83
+ name: string,
84
+ value: string | Redacted.Redacted<string> | null,
85
+ ) => writeVariables(deployment, deployKey, [{ name, value }]);
86
+
72
87
  /**
73
88
  * Sets one variable, or removes it when `value` is null. The call sets a
74
89
  * value, so it runs again after a write conflict or a 5xx answer, for example
@@ -87,6 +102,18 @@ export const EnvironmentVariableList = Schema.Struct({
87
102
  environmentVariables: Schema.Record(Schema.String, Schema.String),
88
103
  });
89
104
 
105
+ /** Every variable of the deployment with its value. Fails with a 404 when the deployment is gone. */
106
+ export const listVariables = (deployment: string, deployKey: Redacted.Redacted<string>) =>
107
+ settle("list environment variables", () =>
108
+ createDeploymentClient(deployment, Redacted.value(deployKey)).GET(
109
+ "/list_environment_variables",
110
+ ),
111
+ ).pipe(
112
+ Effect.map(
113
+ (body) => Schema.decodeUnknownSync(EnvironmentVariableList)(body).environmentVariables,
114
+ ),
115
+ );
116
+
90
117
  /** The variable's identity, or undefined when the variable or the deployment is absent. */
91
118
  const readVariable = (
92
119
  api: ManagementApiService,
package/src/Providers.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  import { CommandExecutorLive } from "alchemy/Command";
4
4
  import * as Provider from "alchemy/Provider";
5
5
  import * as Layer from "effect/Layer";
6
+ import { Code, CodeProvider } from "./Code.ts";
6
7
  import { type Credentials, fromConfig } from "./Credentials.ts";
7
8
  import {
8
9
  DefaultEnvironmentVariable,
@@ -10,6 +11,7 @@ import {
10
11
  } from "./DefaultEnvironmentVariable.ts";
11
12
  import { Deploy, DeployProvider } from "./Deploy.ts";
12
13
  import { DeployKey, DeployKeyProvider } from "./DeployKey.ts";
14
+ import { Deployment, DeploymentProvider } from "./Deployment.ts";
13
15
  import { EnvironmentVariable, EnvironmentVariableProvider } from "./EnvironmentVariable.ts";
14
16
  import { ManagementApiLive } from "./ManagementApi.ts";
15
17
  import { PreviewDeployKey, PreviewDeployKeyProvider } from "./PreviewDeployKey.ts";
@@ -26,26 +28,32 @@ export const providers = (credentials: Layer.Layer<Credentials> = fromConfig())
26
28
  Providers,
27
29
  Provider.collection([
28
30
  Project,
31
+ Deployment,
29
32
  DeployKey,
30
33
  PreviewDeployKey,
31
34
  EnvironmentVariable,
32
35
  DefaultEnvironmentVariable,
36
+ Code,
33
37
  Deploy,
34
38
  ]),
35
39
  ).pipe(
36
40
  Layer.provide(
37
41
  Layer.mergeAll(
38
42
  ProjectProvider(),
43
+ DeploymentProvider(),
39
44
  DeployKeyProvider(),
40
45
  PreviewDeployKeyProvider(),
41
46
  EnvironmentVariableProvider(),
42
47
  DefaultEnvironmentVariableProvider(),
48
+ CodeProvider(),
43
49
  DeployProvider(),
44
50
  ),
45
51
  ),
46
52
  Layer.provide(ManagementApiLive()),
47
53
  Layer.provide(credentials),
48
- // Convex.Deploy runs the Convex CLI through Alchemy's command executor.
54
+ // The deprecated Convex.Deploy runs the Convex CLI through Alchemy's
55
+ // command executor. Convex.Code spawns it itself, so that its output
56
+ // stays out of the logs.
49
57
  Layer.provide(CommandExecutorLive()),
50
58
  Layer.orDie,
51
59
  );
package/src/index.ts CHANGED
@@ -1,4 +1,16 @@
1
1
  // @samebase/alchemy-convex: Convex resources for Alchemy v2.
2
+ export {
3
+ Code,
4
+ type CodeAttributes,
5
+ type CodeDeployment,
6
+ type CodeProps,
7
+ CodePushFailed,
8
+ ConvexCliNotFound,
9
+ DeployKeyMismatch,
10
+ EnvironmentNeedsDeployKey,
11
+ parseDeployOutput,
12
+ PushTargetMismatch,
13
+ } from "./Code.ts";
2
14
  export {
3
15
  ACCESS_TOKEN_ENV,
4
16
  Credentials,
@@ -19,7 +31,6 @@ export {
19
31
  deployArgs,
20
32
  DeployOutputError,
21
33
  type DeployProps,
22
- parseDeployOutput,
23
34
  } from "./Deploy.ts";
24
35
  export {
25
36
  DeployKey,
@@ -29,6 +40,14 @@ export {
29
40
  type DeployKeyProps,
30
41
  DeployKeyRecoveryRequired,
31
42
  } from "./DeployKey.ts";
43
+ export {
44
+ AmbiguousDeployment,
45
+ Deployment,
46
+ type DeploymentAttributes,
47
+ DeploymentIdentityChange,
48
+ type DeploymentProps,
49
+ ProductionDeploymentNotFound,
50
+ } from "./Deployment.ts";
32
51
  export {
33
52
  EnvironmentVariable,
34
53
  type EnvironmentVariableAttributes,