@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.
package/lib/index.js CHANGED
@@ -1,25 +1,42 @@
1
- import { readFile } from "node:fs/promises";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
- import * as Config from "effect/Config";
5
- import * as Context from "effect/Context";
6
- import * as Effect from "effect/Effect";
7
- import * as Layer from "effect/Layer";
8
- import * as Redacted from "effect/Redacted";
9
- import * as Schema from "effect/Schema";
1
+ import { createRequire } from "node:module";
2
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
3
+ import { homedir, tmpdir } from "node:os";
4
+ import { dirname, join, resolve } from "node:path";
5
+ import { stripVTControlCharacters } from "node:util";
10
6
  import { Resource } from "alchemy";
11
7
  import { AdoptPolicy, OwnedBySomeoneElse, Unowned } from "alchemy/AdoptPolicy";
12
- import { havePropsChanged, isResolved } from "alchemy/Diff";
13
8
  import * as Provider from "alchemy/Provider";
9
+ import * as Clock from "effect/Clock";
10
+ import * as Effect from "effect/Effect";
11
+ import * as ChildProcess from "effect/process/ChildProcess";
12
+ import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
13
+ import * as Redacted from "effect/Redacted";
14
+ import * as Schema from "effect/Schema";
15
+ import * as Stream from "effect/Stream";
14
16
  import { AlchemyContext } from "alchemy/AlchemyContext";
15
17
  import { Stack } from "alchemy/Stack";
16
18
  import * as Option from "effect/Option";
19
+ import { havePropsChanged, isResolved } from "alchemy/Diff";
17
20
  import { createDeploymentClient, createManagementClient } from "@convex-dev/platform";
21
+ import * as Context from "effect/Context";
22
+ import * as Layer from "effect/Layer";
18
23
  import * as Schedule from "effect/Schedule";
19
- import { stripVTControlCharacters } from "node:util";
24
+ import * as Config from "effect/Config";
20
25
  import { CommandExecutor, CommandExecutorLive } from "alchemy/Command";
21
26
  import { hashDirectory } from "alchemy/Command/Memo";
22
27
  import { createHash } from "node:crypto";
28
+ //#region src/Adoption.ts
29
+ /** True when the resource `fqn` may take over an object that is not in state. */
30
+ const shouldAdopt = (fqn) => Effect.gen(function* () {
31
+ const stack = yield* Effect.serviceOption(Stack);
32
+ const own = Option.isSome(stack) ? stack.value.resources[fqn]?.Adopt : void 0;
33
+ if (own !== void 0) return own;
34
+ const policy = yield* Effect.serviceOption(AdoptPolicy);
35
+ if (Option.isSome(policy)) return policy.value;
36
+ const context = yield* Effect.serviceOption(AlchemyContext);
37
+ return Option.isSome(context) && context.value.adopt;
38
+ });
39
+ //#endregion
23
40
  //#region src/Credentials.ts
24
41
  var Credentials = class extends Context.Service()("@samebase/alchemy-convex/Credentials") {};
25
42
  /** Environment variable read by {@link fromConfig}. */
@@ -60,18 +77,6 @@ const fromToken = (accessToken) => Layer.succeed(Credentials, Effect.succeed({
60
77
  source: "explicit"
61
78
  }));
62
79
  //#endregion
63
- //#region src/Adoption.ts
64
- /** True when the resource `fqn` may take over an object that is not in state. */
65
- const shouldAdopt = (fqn) => Effect.gen(function* () {
66
- const stack = yield* Effect.serviceOption(Stack);
67
- const own = Option.isSome(stack) ? stack.value.resources[fqn]?.Adopt : void 0;
68
- if (own !== void 0) return own;
69
- const policy = yield* Effect.serviceOption(AdoptPolicy);
70
- if (Option.isSome(policy)) return policy.value;
71
- const context = yield* Effect.serviceOption(AlchemyContext);
72
- return Option.isSome(context) && context.value.adopt;
73
- });
74
- //#endregion
75
80
  //#region src/ManagementApi.ts
76
81
  /**
77
82
  * One error for every control-plane and deployment call. Resources branch on
@@ -184,6 +189,196 @@ const ManagementApiLive = () => Layer.effect(ManagementApi, Effect.gen(function*
184
189
  };
185
190
  }));
186
191
  //#endregion
192
+ //#region src/Deployment.ts
193
+ const Deployment = Resource("Convex.Deployment", { defaultRemovalPolicy: "retain" });
194
+ /** The props name another deployment than the one in state. */
195
+ var DeploymentIdentityChange = class extends Schema.TaggedError()("DeploymentIdentityChange", {
196
+ /** Name of the deployment in state, such as `happy-animal-123`. */
197
+ deployment: Schema.String,
198
+ field: Schema.Literals([
199
+ "projectId",
200
+ "type",
201
+ "name"
202
+ ]),
203
+ current: Schema.String,
204
+ requested: Schema.String
205
+ }) {
206
+ get message() {
207
+ 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.`;
208
+ }
209
+ };
210
+ /** The project has no default production deployment for a `prod` resource to take over. */
211
+ var ProductionDeploymentNotFound = class extends Schema.TaggedError()("ProductionDeploymentNotFound", { projectId: Schema.Number }) {
212
+ get message() {
213
+ 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.`;
214
+ }
215
+ };
216
+ /** More than one deployment of the project has the name, so the lookup cannot choose one. */
217
+ var AmbiguousDeployment = class extends Schema.TaggedError()("AmbiguousDeployment", {
218
+ projectId: Schema.Number,
219
+ type: Schema.Literals(["dev", "preview"]),
220
+ name: Schema.String,
221
+ deployments: Schema.Array(Schema.String)
222
+ }) {
223
+ get message() {
224
+ 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.`;
225
+ }
226
+ };
227
+ /**
228
+ * The URL of a Convex cloud deployment: `https://<name>.convex.cloud`, or
229
+ * `https://<name>.<region>.convex.cloud` outside US East.
230
+ */
231
+ const CLOUD_URL = /^https:\/\/[a-z0-9]+(?:-[a-z0-9]+)*(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)?\.convex\.cloud$/;
232
+ /**
233
+ * The HTTP actions URL of a deployment: the client URL with `.convex.site`
234
+ * for `.convex.cloud`, as Convex sets CONVEX_SITE_URL. Custom domains are
235
+ * not covered.
236
+ */
237
+ const siteUrlOf = (url) => url.replace(/\.convex\.cloud$/, ".convex.site");
238
+ /** One row of list_deployments, GET /deployments/{name}, and create_deployment. Only the fields the provider uses. */
239
+ const DeploymentRow$1 = Schema.Struct({
240
+ name: Schema.String,
241
+ projectId: Schema.Number,
242
+ deploymentType: Schema.String,
243
+ isDefault: Schema.Boolean,
244
+ reference: Schema.String,
245
+ /** Set in list_deployments and create_deployment rows only: GET /deployments/{name} answers null. */
246
+ previewIdentifier: Schema.optionalKey(Schema.NullOr(Schema.String)),
247
+ deploymentUrl: Schema.String.check(Schema.isPattern(CLOUD_URL))
248
+ });
249
+ const DeploymentRows$1 = Schema.Array(DeploymentRow$1);
250
+ const attributesOf = (row, type, previewName) => ({
251
+ projectId: row.projectId,
252
+ type,
253
+ name: row.name,
254
+ reference: row.reference,
255
+ previewName: type === "preview" ? previewName : void 0,
256
+ url: row.deploymentUrl,
257
+ siteUrl: siteUrlOf(row.deploymentUrl)
258
+ });
259
+ /** The `name` prop of the deployment in state: the dev reference or the preview name. */
260
+ const nameOf = (attributes) => attributes.type === "dev" ? attributes.reference : attributes.type === "preview" ? attributes.previewName : void 0;
261
+ /**
262
+ * Fails with {@link DeploymentIdentityChange} when the props name another
263
+ * deployment than `current`: the attributes in state, or a live row.
264
+ */
265
+ const checkIdentity$2 = (current, news) => {
266
+ const change = (field, from, to) => Effect.fail(new DeploymentIdentityChange({
267
+ deployment: current.name,
268
+ field,
269
+ current: from,
270
+ requested: to
271
+ }));
272
+ if (current.projectId !== news.projectId) return change("projectId", String(current.projectId), String(news.projectId));
273
+ if (current.type !== news.type) return change("type", current.type, news.type);
274
+ const currentName = nameOf(current);
275
+ if (news.type !== "prod" && currentName !== news.name) return change("name", currentName ?? "(none)", news.name);
276
+ return Effect.void;
277
+ };
278
+ const DeploymentProvider = () => Provider.effect(Deployment, Effect.gen(function* () {
279
+ const api = yield* ManagementApi;
280
+ /** The live deployment, or undefined after a 404. GET /deployments/{name} answers no preview identifier. */
281
+ const getByName = (name) => absentAsUndefined(api.request("get deployment", (client) => client.GET("/deployments/{deployment_name}", { params: { path: { deployment_name: name } } }))).pipe(Effect.map((body) => body === void 0 ? void 0 : Schema.decodeUnknownSync(DeploymentRow$1)(body)));
282
+ const listDeployments = (projectId, type, isDefault) => api.request("list deployments", (client) => client.GET("/projects/{project_id}/list_deployments", { params: {
283
+ path: { project_id: projectId },
284
+ query: {
285
+ deploymentType: type,
286
+ ...isDefault === void 0 ? {} : { isDefault }
287
+ }
288
+ } })).pipe(Effect.map((body) => Schema.decodeUnknownSync(DeploymentRows$1)(body)));
289
+ /** The project's default production deployment, or undefined. */
290
+ const production = (projectId) => listDeployments(projectId, "prod", true).pipe(Effect.map((rows) => {
291
+ const row = rows.find((candidate) => candidate.deploymentType === "prod" && candidate.isDefault);
292
+ return row === void 0 ? void 0 : attributesOf(row, "prod", void 0);
293
+ }));
294
+ /**
295
+ * The one dev deployment with this reference, or the one preview
296
+ * deployment with this preview name, or undefined.
297
+ */
298
+ const findByName = (projectId, type, name) => Effect.gen(function* () {
299
+ const matches = (yield* listDeployments(projectId, type)).filter((row) => row.deploymentType === type && (type === "dev" ? row.reference === name : row.previewIdentifier === name));
300
+ if (matches.length > 1) return yield* new AmbiguousDeployment({
301
+ projectId,
302
+ type,
303
+ name,
304
+ deployments: matches.map((row) => row.name)
305
+ });
306
+ return matches[0];
307
+ });
308
+ /** The deployment that the props name, for a resource without state. */
309
+ const lookup = (news) => news.type === "prod" ? production(news.projectId) : findByName(news.projectId, news.type, news.name).pipe(Effect.map((row) => row === void 0 ? void 0 : attributesOf(row, news.type, news.name)));
310
+ return {
311
+ stables: [
312
+ "projectId",
313
+ "type",
314
+ "previewName"
315
+ ],
316
+ diff: Effect.fn(function* ({ news, output }) {
317
+ if (!isResolved(news) || output === void 0) return void 0;
318
+ yield* checkIdentity$2(output, news);
319
+ if (news.type === "prod") return (yield* absentAsUndefined(production(news.projectId)))?.name === output.name ? void 0 : { action: "update" };
320
+ return (yield* getByName(output.name)) === void 0 ? { action: "update" } : void 0;
321
+ }),
322
+ read: Effect.fn(function* ({ olds, output }) {
323
+ if (output?.type === "prod") return yield* absentAsUndefined(production(output.projectId));
324
+ if (output !== void 0) {
325
+ const live = yield* getByName(output.name);
326
+ return live === void 0 ? void 0 : attributesOf(live, output.type, output.previewName);
327
+ }
328
+ const found = yield* lookup(olds);
329
+ if (found === void 0) return void 0;
330
+ return olds.type === "prod" ? found : Unowned(found);
331
+ }),
332
+ reconcile: Effect.fn(function* ({ fqn, news, output }) {
333
+ if (output !== void 0) yield* checkIdentity$2(output, news);
334
+ if (news.type === "prod") {
335
+ const current = yield* production(news.projectId);
336
+ if (current === void 0) return yield* new ProductionDeploymentNotFound({ projectId: news.projectId });
337
+ return current;
338
+ }
339
+ if (output !== void 0) {
340
+ const live = yield* getByName(output.name);
341
+ if (live !== void 0) {
342
+ if (live.deploymentType !== output.type) return yield* new DeploymentIdentityChange({
343
+ deployment: live.name,
344
+ field: "type",
345
+ current: live.deploymentType,
346
+ requested: news.type
347
+ });
348
+ const current = attributesOf(live, output.type, output.previewName);
349
+ yield* checkIdentity$2(current, news);
350
+ return current;
351
+ }
352
+ }
353
+ const existing = yield* lookup(news);
354
+ if (existing !== void 0) {
355
+ if (!(yield* shouldAdopt(fqn))) return yield* new OwnedBySomeoneElse({
356
+ 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.`,
357
+ resourceType: Deployment.Type,
358
+ physicalName: existing.name
359
+ });
360
+ return existing;
361
+ }
362
+ const created = yield* api.request("create deployment", (client) => client.POST("/projects/{project_id}/create_deployment", {
363
+ params: { path: { project_id: news.projectId } },
364
+ body: {
365
+ type: news.type,
366
+ reference: news.name
367
+ }
368
+ }));
369
+ return attributesOf(Schema.decodeUnknownSync(DeploymentRow$1)(created), news.type, news.name);
370
+ }),
371
+ delete: Effect.fn(function* ({ output }) {
372
+ const keep = Effect.logWarning(`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.`);
373
+ if (output.type === "prod") return yield* keep;
374
+ const live = yield* getByName(output.name);
375
+ if (live === void 0) return;
376
+ if (live.deploymentType === "prod") return yield* keep;
377
+ yield* absentAsUndefined(retryIdempotentWrite(api.requestVoid("delete deployment", (client) => client.POST("/deployments/{deployment_name}/delete", { params: { path: { deployment_name: output.name } } }))));
378
+ })
379
+ };
380
+ }));
381
+ //#endregion
187
382
  //#region src/EnvironmentVariable.ts
188
383
  /**
189
384
  * The variable's identity when the listing has `name`. Own keys only: a
@@ -194,13 +389,19 @@ const findVariable = (list, deployment, name) => Object.hasOwn(list.environmentV
194
389
  name
195
390
  } : void 0;
196
391
  /**
197
- * One attempt to set one variable, or to remove it when `value` is null. The
198
- * deployment answers 200 with an empty body, so this reads no response data.
392
+ * One attempt to set variables, or to remove those whose `value` is null, in
393
+ * one call. The deployment answers 200 with an empty body, so this reads no
394
+ * response data.
199
395
  */
200
- const writeVariable = (deployment, deployKey, name, value) => settleVoid("update environment variables", () => createDeploymentClient(deployment, Redacted.value(deployKey)).POST("/update_environment_variables", { body: { changes: [{
396
+ const writeVariables = (deployment, deployKey, changes) => settleVoid("update environment variables", () => createDeploymentClient(deployment, Redacted.value(deployKey)).POST("/update_environment_variables", { body: { changes: changes.map(({ name, value }) => ({
201
397
  name,
202
398
  value: Redacted.isRedacted(value) ? Redacted.value(value) : value
203
- }] } }));
399
+ })) } }));
400
+ /** One attempt to set one variable, or to remove it when `value` is null. */
401
+ const writeVariable = (deployment, deployKey, name, value) => writeVariables(deployment, deployKey, [{
402
+ name,
403
+ value
404
+ }]);
204
405
  /**
205
406
  * Sets one variable, or removes it when `value` is null. The call sets a
206
407
  * value, so it runs again after a write conflict or a 5xx answer, for example
@@ -210,6 +411,8 @@ const writeVariable = (deployment, deployKey, name, value) => settleVoid("update
210
411
  const updateVariable = (deployment, deployKey, name, value) => retryIdempotentWrite(writeVariable(deployment, deployKey, name, value));
211
412
  /** GET https://<deployment>.convex.cloud/api/v1/list_environment_variables */
212
413
  const EnvironmentVariableList = Schema.Struct({ environmentVariables: Schema.Record(Schema.String, Schema.String) });
414
+ /** Every variable of the deployment with its value. Fails with a 404 when the deployment is gone. */
415
+ const listVariables = (deployment, deployKey) => settle("list environment variables", () => createDeploymentClient(deployment, Redacted.value(deployKey)).GET("/list_environment_variables")).pipe(Effect.map((body) => Schema.decodeUnknownSync(EnvironmentVariableList)(body).environmentVariables));
213
416
  /** The variable's identity, or undefined when the variable or the deployment is absent. */
214
417
  const readVariable = (api, deployment, deployKey, name) => absentAsUndefined(api.deploymentRequest("list environment variables", deployment, deployKey, (client) => client.GET("/list_environment_variables"))).pipe(Effect.map((body) => body === void 0 ? void 0 : findVariable(Schema.decodeUnknownSync(EnvironmentVariableList)(body), deployment, name)));
215
418
  const EnvironmentVariable = Resource("Convex.EnvironmentVariable");
@@ -297,6 +500,261 @@ const EnvironmentVariableProvider = () => Provider.effect(EnvironmentVariable, E
297
500
  };
298
501
  }));
299
502
  //#endregion
503
+ //#region src/Code.ts
504
+ const Code = Resource("Convex.Code");
505
+ /**
506
+ * The CLI prints this line with `logFinishedStep` after the push succeeds, to
507
+ * stderr (convex 1.46 `cli/lib/deploy2.js` for existing deployments,
508
+ * `cli/deploy.js` for preview deployments). The host is
509
+ * `<deployment>.convex.cloud` or `<deployment>.<region>.convex.cloud`.
510
+ */
511
+ const DEPLOYED_LINE = /Deployed Convex functions to (https:\/\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)?\.convex\.cloud)\/?(?=\s|$)/m;
512
+ /** Reads the deployment URL from `convex deploy` output. ANSI escape codes are removed first. */
513
+ const parseDeployOutput = (text) => {
514
+ const match = DEPLOYED_LINE.exec(stripVTControlCharacters(text));
515
+ const url = match?.[1];
516
+ const deploymentName = match?.[2];
517
+ return url === void 0 || deploymentName === void 0 ? void 0 : {
518
+ url,
519
+ deploymentName
520
+ };
521
+ };
522
+ /**
523
+ * What a deploy key targets, from its prefix: `<type>:<deployment>|<secret>`
524
+ * (or `<deployment>|<secret>` in old keys) for one deployment,
525
+ * `preview:<team>:<project>|<secret>` for preview deployments of a project,
526
+ * `project:<team>:<project>|<secret>` for the project's production
527
+ * deployment. Same rules as convex 1.46 `cli/lib/deployment.js`.
528
+ */
529
+ const keyTargetOf = (key) => {
530
+ const bar = key.indexOf("|");
531
+ if (bar <= 0 || /[\s#'"`]/.test(key)) return { kind: "unknown" };
532
+ const parts = key.slice(0, bar).split(":");
533
+ const [first, second] = parts;
534
+ if (parts.length === 3 && first === "preview") return { kind: "preview" };
535
+ if (parts.length === 3 && first === "project") return { kind: "project" };
536
+ if (parts.length === 2 && second) return {
537
+ kind: "deployment",
538
+ deployment: second
539
+ };
540
+ if (parts.length === 1 && first) return {
541
+ kind: "deployment",
542
+ deployment: first
543
+ };
544
+ return { kind: "unknown" };
545
+ };
546
+ /** The deploy key does not fit the deployment. Checked before the push. */
547
+ var DeployKeyMismatch = class extends Schema.TaggedError()("DeployKeyMismatch", {
548
+ /** The deployment that should receive the push. */
549
+ deployment: Schema.String,
550
+ /** What the key targets. */
551
+ key: Schema.Literals([
552
+ "deployment",
553
+ "preview",
554
+ "project",
555
+ "unknown"
556
+ ]),
557
+ /** For a deployment key: the deployment in the key prefix. Never the secret. */
558
+ keyDeployment: Schema.UndefinedOr(Schema.String)
559
+ }) {
560
+ get message() {
561
+ switch (this.key) {
562
+ case "deployment": return `The deploy key is for deployment ${this.keyDeployment}, not for ${this.deployment}. Pass a deploy key of ${this.deployment}, such as Convex.DeployKey with deployment ${this.deployment}.`;
563
+ case "preview": return `A preview deploy key pushes to a preview deployment by its preview name, and ${this.deployment} is not a Convex.Deployment of type "preview". Pass the Convex.Deployment preview resource as deployment, or a deploy key of ${this.deployment}.`;
564
+ case "project": return `Convex.Code does not take a project deploy key: it cannot check before the push that the key targets ${this.deployment}. Pass a deploy key of ${this.deployment} (Convex.DeployKey).`;
565
+ case "unknown": return `deployKey is not a Convex deploy key ("<type>:<deployment>|<secret>" or "preview:<team>:<project>|<secret>"). Pass Convex.DeployKey(...).deployKey or Convex.PreviewDeployKey(...).previewDeployKey.`;
566
+ }
567
+ }
568
+ };
569
+ /** `env` needs the deployment API, which takes only a deploy key of the deployment. */
570
+ var EnvironmentNeedsDeployKey = class extends Schema.TaggedError()("EnvironmentNeedsDeployKey", { deployment: Schema.String }) {
571
+ get message() {
572
+ return `Convex.Code cannot set or remove env variables on ${this.deployment} with a preview deploy key: the deployment API takes only a deploy key of the deployment. Pass a Convex.DeployKey of ${this.deployment}, or use Convex.DefaultEnvironmentVariable with deploymentType "preview" for values that every new preview deployment gets.`;
573
+ }
574
+ };
575
+ /** No `convex` package resolves from `cwd`. */
576
+ var ConvexCliNotFound = class extends Schema.TaggedError()("ConvexCliNotFound", { cwd: Schema.String }) {
577
+ get message() {
578
+ return `Convex.Code found no installed convex package from ${this.cwd}, and it does not download one. Install convex in the app (npm install convex), set cwd to the app directory, or set the command prop.`;
579
+ }
580
+ };
581
+ /** The Convex CLI failed, or exited 0 without the deployment URL. The output tail never contains the key. */
582
+ var CodePushFailed = class extends Schema.TaggedError()("CodePushFailed", {
583
+ /** Exit code of the CLI. Undefined when it did not start or a signal stopped it. */
584
+ exitCode: Schema.UndefinedOr(Schema.Number),
585
+ /** The last lines of the CLI output, with the deploy key removed. */
586
+ outputTail: Schema.String
587
+ }) {
588
+ get message() {
589
+ return this.exitCode === 0 ? `convex deploy exited 0 but printed no "Deployed Convex functions to <url>" line, so Convex.Code has no deployment URL. Last output, with the deploy key removed:\n${this.outputTail}` : `convex deploy failed${this.exitCode === void 0 ? "" : ` with exit code ${this.exitCode}`}. Last output, with the deploy key removed:\n${this.outputTail}`;
590
+ }
591
+ };
592
+ /** The CLI pushed to another deployment than the one in the props. Checked after the push. */
593
+ var PushTargetMismatch = class extends Schema.TaggedError()("PushTargetMismatch", {
594
+ expected: Schema.String,
595
+ actual: Schema.String
596
+ }) {
597
+ get message() {
598
+ return `The Convex CLI pushed the functions to deployment ${this.actual}, but Convex.Code expected ${this.expected}. With a preview deploy key, this happens when the key belongs to another project, or when the preview deployment ${this.expected} expired during the deploy and the CLI created ${this.actual} for the same preview name. Check the key. After an expiry, run the deploy again with --adopt, so that Convex.Deployment takes over ${this.actual}.`;
599
+ }
600
+ };
601
+ /**
602
+ * Checks that the key fits the deployment, and returns the preview name for
603
+ * `--preview-name` when the key is a preview deploy key.
604
+ */
605
+ const previewNameFor = (target, keyTarget, changesEnvironment) => {
606
+ const mismatch = new DeployKeyMismatch({
607
+ deployment: target.name,
608
+ key: keyTarget.kind,
609
+ keyDeployment: keyTarget.kind === "deployment" ? keyTarget.deployment : void 0
610
+ });
611
+ switch (keyTarget.kind) {
612
+ case "deployment": return keyTarget.deployment === target.name ? Effect.succeed(void 0) : Effect.fail(mismatch);
613
+ case "preview":
614
+ if (target.type !== "preview" || target.previewName === void 0) return Effect.fail(mismatch);
615
+ return changesEnvironment ? Effect.fail(new EnvironmentNeedsDeployKey({ deployment: target.name })) : Effect.succeed(target.previewName);
616
+ case "project":
617
+ case "unknown": return Effect.fail(mismatch);
618
+ default: return keyTarget;
619
+ }
620
+ };
621
+ /** The last non-empty lines of `text`, without escape codes and with every secret replaced. */
622
+ const outputTail = (text, secrets) => secrets.reduce((redacted, secret) => redacted.split(secret).join("[REDACTED]"), stripVTControlCharacters(text)).split(/\r?\n|\r/).filter((line) => line.trim() !== "").slice(-20).join("\n");
623
+ const CliManifest = Schema.Struct({ bin: Schema.Union([Schema.String, Schema.Record(Schema.String, Schema.String)]) });
624
+ /** `[node, <convex bin>]` for the `convex` package that `cwd` resolves. Never downloads. */
625
+ const installedCli = (cwd) => Effect.tryPromise({
626
+ try: async () => {
627
+ const manifestPath = createRequire(join(cwd, "package.json")).resolve("convex/package.json");
628
+ const { bin } = Schema.decodeUnknownSync(Schema.fromJsonString(CliManifest))(await readFile(manifestPath, "utf8"));
629
+ const entry = typeof bin === "string" ? bin : bin["convex"];
630
+ if (entry === void 0) throw new Error("the convex package has no convex bin");
631
+ return [process.execPath, resolve(dirname(manifestPath), entry)];
632
+ },
633
+ catch: () => new ConvexCliNotFound({ cwd })
634
+ });
635
+ /**
636
+ * Runs `use` with an env file that holds the key, in a new temporary
637
+ * directory, and removes the directory afterwards: on success, on failure,
638
+ * and on interruption.
639
+ */
640
+ const withKeyFile = (key, use) => Effect.acquireUseRelease(Effect.promise(() => mkdtemp(join(tmpdir(), "alchemy-convex-"))), (home) => Effect.promise(() => writeFile(join(home, "deploy.env"), `CONVEX_DEPLOY_KEY=${key}\n`, { mode: 384 })).pipe(Effect.andThen(use({
641
+ envFile: join(home, "deploy.env"),
642
+ home
643
+ }))), (home) => Effect.promise(() => rm(home, {
644
+ recursive: true,
645
+ force: true
646
+ })));
647
+ /**
648
+ * The environment of the CLI: this process's variables without `CONVEX_*`,
649
+ * with `home` as the home directory.
650
+ */
651
+ const cliEnvironment = (home) => ({
652
+ ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.toUpperCase().startsWith("CONVEX_"))),
653
+ HOME: home,
654
+ USERPROFILE: home,
655
+ XDG_CONFIG_HOME: home
656
+ });
657
+ const CodeProvider = () => Provider.effect(Code, Effect.gen(function* () {
658
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
659
+ /** Runs the CLI with piped output. Interruption stops its process group. */
660
+ const run = (argv, cwd, home) => Effect.gen(function* () {
661
+ const [command, ...args] = argv;
662
+ const child = yield* spawner.spawn(ChildProcess.make(command, args, {
663
+ cwd,
664
+ env: cliEnvironment(home),
665
+ extendEnv: false,
666
+ stdin: "ignore",
667
+ stdout: "pipe",
668
+ stderr: "pipe"
669
+ }));
670
+ const [exitCode, stdout, stderr] = yield* Effect.all([
671
+ child.exitCode,
672
+ Stream.mkString(Stream.decodeText(child.stdout)),
673
+ Stream.mkString(Stream.decodeText(child.stderr))
674
+ ], { concurrency: "unbounded" });
675
+ return {
676
+ exitCode: Number(exitCode),
677
+ output: `${stdout}\n${stderr}`
678
+ };
679
+ }).pipe(Effect.scoped);
680
+ return {
681
+ diff: () => Effect.succeed({ action: "update" }),
682
+ reconcile: Effect.fn(function* ({ fqn, news, output }) {
683
+ const target = typeof news.deployment === "string" ? {
684
+ name: news.deployment,
685
+ address: news.deployment,
686
+ type: void 0,
687
+ previewName: void 0
688
+ } : {
689
+ ...news.deployment,
690
+ address: news.deployment.url
691
+ };
692
+ const key = Redacted.value(news.deployKey);
693
+ const secrets = [key, key.slice(key.indexOf("|") + 1)].filter((secret) => secret.length >= 8);
694
+ const env = Object.entries(news.env ?? {}).sort(([a], [b]) => a < b ? -1 : 1);
695
+ const previous = output !== void 0 && output.deploymentName === target.name ? output.envNames : [];
696
+ const removed = previous.filter((name) => !env.some(([envName]) => envName === name));
697
+ const previewName = yield* previewNameFor(target, keyTargetOf(key), env.length > 0 || removed.length > 0);
698
+ const cwd = resolve(news.cwd);
699
+ const program = news.command ?? (yield* installedCli(cwd));
700
+ if (env.length > 0 || removed.length > 0) {
701
+ const adopt = yield* shouldAdopt(fqn);
702
+ const fresh = env.filter(([name]) => !previous.includes(name));
703
+ yield* retryIdempotentWrite(Effect.gen(function* () {
704
+ if (fresh.length > 0 && !adopt) {
705
+ const current = yield* listVariables(target.address, news.deployKey);
706
+ const taken = fresh.filter(([name, value]) => Object.hasOwn(current, name) && current[name] !== (Redacted.isRedacted(value) ? Redacted.value(value) : value)).map(([name]) => name);
707
+ if (taken.length > 0) return yield* new OwnedBySomeoneElse({
708
+ message: `Environment variables ${taken.join(", ")} already exist on deployment ${target.name} with other values. Re-run with --adopt to take them over and overwrite them, or remove them from env.`,
709
+ resourceType: Code.Type,
710
+ physicalName: target.name
711
+ });
712
+ }
713
+ yield* writeVariables(target.address, news.deployKey, [...env.map(([name, value]) => ({
714
+ name,
715
+ value
716
+ })), ...removed.map((name) => ({
717
+ name,
718
+ value: null
719
+ }))]);
720
+ }));
721
+ }
722
+ const args = [
723
+ "deploy",
724
+ ...previewName === void 0 ? [] : ["--preview-name", previewName],
725
+ ...news.typecheck === void 0 ? [] : [`--typecheck=${news.typecheck}`],
726
+ ...news.codegen === void 0 ? [] : [`--codegen=${news.codegen}`]
727
+ ];
728
+ const result = yield* withKeyFile(key, ({ envFile, home }) => run([
729
+ ...program,
730
+ ...args,
731
+ "--env-file",
732
+ envFile
733
+ ], cwd, home)).pipe(Effect.mapError((error) => new CodePushFailed({
734
+ exitCode: void 0,
735
+ outputTail: outputTail(error.message, secrets)
736
+ })));
737
+ const deployed = parseDeployOutput(result.output);
738
+ if (result.exitCode !== 0 || deployed === void 0) return yield* new CodePushFailed({
739
+ exitCode: result.exitCode,
740
+ outputTail: outputTail(result.output, secrets)
741
+ });
742
+ if (deployed.deploymentName !== target.name) return yield* new PushTargetMismatch({
743
+ expected: target.name,
744
+ actual: deployed.deploymentName
745
+ });
746
+ return {
747
+ deploymentName: deployed.deploymentName,
748
+ url: deployed.url,
749
+ siteUrl: siteUrlOf(deployed.url),
750
+ deployedAt: yield* Clock.currentTimeMillis,
751
+ envNames: env.map(([name]) => name)
752
+ };
753
+ }),
754
+ delete: () => Effect.void
755
+ };
756
+ }));
757
+ //#endregion
300
758
  //#region src/DefaultEnvironmentVariable.ts
301
759
  /**
302
760
  * Attributes when the listing has `name` for `deploymentType`. An unfiltered
@@ -404,24 +862,13 @@ const DefaultEnvironmentVariableProvider = () => Provider.effect(DefaultEnvironm
404
862
  }));
405
863
  //#endregion
406
864
  //#region src/Deploy.ts
407
- const Deploy = Resource("Convex.Deploy");
408
865
  /**
409
- * The CLI prints this line with `logFinishedStep` after the push succeeds, to
410
- * stderr (convex 1.45 `cli/lib/deploy2.js` for existing deployments,
411
- * `cli/deploy.js` for preview deployments). The host is
412
- * `<deployment>.convex.cloud` or `<deployment>.<region>.convex.cloud`.
866
+ * @deprecated Since 0.3.0. Use Convex.Code with a Convex.Deployment: it
867
+ * runs the installed Convex CLI, keeps the deploy key out of the process
868
+ * environment, checks that the key fits the deployment, and outputs the site
869
+ * URL. Removing a Deploy resource changes nothing in Convex.
413
870
  */
414
- const DEPLOYED_LINE = /Deployed Convex functions to (https:\/\/([a-z0-9]+(?:-[a-z0-9]+)*)(?:\.[a-z0-9]+(?:-[a-z0-9]+)*)?\.convex\.cloud)\/?(?=\s|$)/m;
415
- /** Reads the deployment URL from `convex deploy` output. ANSI escape codes are removed first. */
416
- const parseDeployOutput = (text) => {
417
- const match = DEPLOYED_LINE.exec(stripVTControlCharacters(text));
418
- const url = match?.[1];
419
- const deploymentName = match?.[2];
420
- return url === void 0 || deploymentName === void 0 ? void 0 : {
421
- url,
422
- deploymentName
423
- };
424
- };
871
+ const Deploy = Resource("Convex.Deploy");
425
872
  /** The `convex deploy` argv, starting with `npx`. */
426
873
  const deployArgs = (props) => [
427
874
  "npx",
@@ -951,13 +1398,15 @@ var Providers = class extends Provider.ProviderCollection()("Convex") {};
951
1398
  */
952
1399
  const providers = (credentials = fromConfig()) => Layer.effect(Providers, Provider.collection([
953
1400
  Project,
1401
+ Deployment,
954
1402
  DeployKey,
955
1403
  PreviewDeployKey,
956
1404
  EnvironmentVariable,
957
1405
  DefaultEnvironmentVariable,
1406
+ Code,
958
1407
  Deploy
959
- ])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DeployKeyProvider(), PreviewDeployKeyProvider(), EnvironmentVariableProvider(), DefaultEnvironmentVariableProvider(), DeployProvider())), Layer.provide(ManagementApiLive()), Layer.provide(credentials), Layer.provide(CommandExecutorLive()), Layer.orDie);
1408
+ ])).pipe(Layer.provide(Layer.mergeAll(ProjectProvider(), DeploymentProvider(), DeployKeyProvider(), PreviewDeployKeyProvider(), EnvironmentVariableProvider(), DefaultEnvironmentVariableProvider(), CodeProvider(), DeployProvider())), Layer.provide(ManagementApiLive()), Layer.provide(credentials), Layer.provide(CommandExecutorLive()), Layer.orDie);
960
1409
  //#endregion
961
- export { ACCESS_TOKEN_ENV, AmbiguousProject, ConvexApiError, CreatedProjectNotFound, Credentials, CredentialsError, DefaultEnvironmentVariable, Deploy, DeployArgumentError, DeployKey, DeployKeyIsCredential, DeployKeyRecoveryRequired, DeployOutputError, EnvironmentVariable, ManagementApi, PreviewDeployKey, Project, ProjectDeploymentChange, ProjectTeamChange, Providers, VariableIdentityChange, deployArgs, fromConfig, fromToken, parseDeployOutput, providers };
1410
+ export { ACCESS_TOKEN_ENV, AmbiguousDeployment, AmbiguousProject, Code, CodePushFailed, ConvexApiError, ConvexCliNotFound, CreatedProjectNotFound, Credentials, CredentialsError, DefaultEnvironmentVariable, Deploy, DeployArgumentError, DeployKey, DeployKeyIsCredential, DeployKeyMismatch, DeployKeyRecoveryRequired, DeployOutputError, Deployment, DeploymentIdentityChange, EnvironmentNeedsDeployKey, EnvironmentVariable, ManagementApi, PreviewDeployKey, ProductionDeploymentNotFound, Project, ProjectDeploymentChange, ProjectTeamChange, Providers, PushTargetMismatch, VariableIdentityChange, deployArgs, fromConfig, fromToken, parseDeployOutput, providers };
962
1411
 
963
1412
  //# sourceMappingURL=index.js.map