@gaganref/confect-alchemy 0.0.0-stage → 10.3.1-fork.2

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.
Files changed (113) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +22 -2
  3. package/dist/Backend.d.ts +45 -0
  4. package/dist/Backend.d.ts.map +1 -0
  5. package/dist/Backend.js +38 -0
  6. package/dist/Backend.js.map +1 -0
  7. package/dist/Code.d.ts +80 -0
  8. package/dist/Code.d.ts.map +1 -0
  9. package/dist/Code.js +29 -0
  10. package/dist/Code.js.map +1 -0
  11. package/dist/Confect.d.ts +2 -0
  12. package/dist/Confect.d.ts.map +1 -0
  13. package/dist/Confect.js +8 -0
  14. package/dist/Confect.js.map +1 -0
  15. package/dist/Convex.d.ts +7 -0
  16. package/dist/Convex.d.ts.map +1 -0
  17. package/dist/Convex.js +21 -0
  18. package/dist/Convex.js.map +1 -0
  19. package/dist/ConvexClient.d.ts +215 -0
  20. package/dist/ConvexClient.d.ts.map +1 -0
  21. package/dist/ConvexClient.js +224 -0
  22. package/dist/ConvexClient.js.map +1 -0
  23. package/dist/DeployKey.d.ts +120 -0
  24. package/dist/DeployKey.d.ts.map +1 -0
  25. package/dist/DeployKey.js +151 -0
  26. package/dist/DeployKey.js.map +1 -0
  27. package/dist/Deployment.d.ts +113 -0
  28. package/dist/Deployment.d.ts.map +1 -0
  29. package/dist/Deployment.js +160 -0
  30. package/dist/Deployment.js.map +1 -0
  31. package/dist/EnvironmentVariables.d.ts +155 -0
  32. package/dist/EnvironmentVariables.d.ts.map +1 -0
  33. package/dist/EnvironmentVariables.js +135 -0
  34. package/dist/EnvironmentVariables.js.map +1 -0
  35. package/dist/Project.d.ts +100 -0
  36. package/dist/Project.d.ts.map +1 -0
  37. package/dist/Project.js +123 -0
  38. package/dist/Project.js.map +1 -0
  39. package/dist/Providers.d.ts +17 -0
  40. package/dist/Providers.d.ts.map +1 -0
  41. package/dist/Providers.js +46 -0
  42. package/dist/Providers.js.map +1 -0
  43. package/dist/_virtual/_rolldown/runtime.js +13 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.d.ts.map +1 -0
  46. package/dist/index.js +3 -0
  47. package/dist/internal/CodeDeployment.d.ts +17 -0
  48. package/dist/internal/CodeDeployment.d.ts.map +1 -0
  49. package/dist/internal/CodeDeployment.js +53 -0
  50. package/dist/internal/CodeDeployment.js.map +1 -0
  51. package/dist/internal/CommandRunner.d.ts +55 -0
  52. package/dist/internal/CommandRunner.d.ts.map +1 -0
  53. package/dist/internal/CommandRunner.js +126 -0
  54. package/dist/internal/CommandRunner.js.map +1 -0
  55. package/dist/internal/FileLock.d.ts +30 -0
  56. package/dist/internal/FileLock.d.ts.map +1 -0
  57. package/dist/internal/FileLock.js +83 -0
  58. package/dist/internal/FileLock.js.map +1 -0
  59. package/dist/internal/Lifecycle.d.ts +11 -0
  60. package/dist/internal/Lifecycle.d.ts.map +1 -0
  61. package/dist/internal/Lifecycle.js +45 -0
  62. package/dist/internal/Lifecycle.js.map +1 -0
  63. package/dist/internal/LocalBackend.d.ts +63 -0
  64. package/dist/internal/LocalBackend.d.ts.map +1 -0
  65. package/dist/internal/LocalBackend.js +333 -0
  66. package/dist/internal/LocalBackend.js.map +1 -0
  67. package/dist/internal/LocalSidecar.d.ts +3 -0
  68. package/dist/internal/LocalSidecar.d.ts.map +1 -0
  69. package/dist/internal/LocalSidecar.js +7 -0
  70. package/dist/internal/LocalSidecar.js.map +1 -0
  71. package/dist/internal/Loopback.d.ts +18 -0
  72. package/dist/internal/Loopback.d.ts.map +1 -0
  73. package/dist/internal/Loopback.js +31 -0
  74. package/dist/internal/Loopback.js.map +1 -0
  75. package/dist/internal/PushLock.d.ts +36 -0
  76. package/dist/internal/PushLock.d.ts.map +1 -0
  77. package/dist/internal/PushLock.js +61 -0
  78. package/dist/internal/PushLock.js.map +1 -0
  79. package/dist/internal/ResourceIdentity.d.ts +3 -0
  80. package/dist/internal/ResourceIdentity.d.ts.map +1 -0
  81. package/dist/internal/ResourceIdentity.js +10 -0
  82. package/dist/internal/ResourceIdentity.js.map +1 -0
  83. package/dist/internal/UserCache.d.ts +12 -0
  84. package/dist/internal/UserCache.d.ts.map +1 -0
  85. package/dist/internal/UserCache.js +25 -0
  86. package/dist/internal/UserCache.js.map +1 -0
  87. package/dist/internal/Zip.d.ts +15 -0
  88. package/dist/internal/Zip.d.ts.map +1 -0
  89. package/dist/internal/Zip.js +65 -0
  90. package/dist/internal/Zip.js.map +1 -0
  91. package/package.json +65 -3
  92. package/src/Backend.ts +101 -0
  93. package/src/Code.ts +55 -0
  94. package/src/Confect.ts +1 -0
  95. package/src/Convex.ts +18 -0
  96. package/src/ConvexClient.ts +544 -0
  97. package/src/DeployKey.ts +256 -0
  98. package/src/Deployment.ts +277 -0
  99. package/src/EnvironmentVariables.ts +254 -0
  100. package/src/Project.ts +205 -0
  101. package/src/Providers.ts +67 -0
  102. package/src/index.ts +2 -0
  103. package/src/internal/CodeDeployment.ts +81 -0
  104. package/src/internal/CommandRunner.ts +251 -0
  105. package/src/internal/FileLock.ts +136 -0
  106. package/src/internal/Lifecycle.ts +49 -0
  107. package/src/internal/LocalBackend.ts +492 -0
  108. package/src/internal/LocalSidecar.ts +9 -0
  109. package/src/internal/Loopback.ts +34 -0
  110. package/src/internal/PushLock.ts +99 -0
  111. package/src/internal/ResourceIdentity.ts +20 -0
  112. package/src/internal/UserCache.ts +25 -0
  113. package/src/internal/Zip.ts +108 -0
@@ -0,0 +1,251 @@
1
+ import * as Config from "effect/Config";
2
+ import * as Context from "effect/Context";
3
+ import * as Effect from "effect/Effect";
4
+ import * as FileSystem from "effect/FileSystem";
5
+ import * as Layer from "effect/Layer";
6
+ import * as Path from "effect/Path";
7
+ import * as Redacted from "effect/Redacted";
8
+ import * as Schema from "effect/Schema";
9
+ import { ChildProcess, ChildProcessSpawner } from "effect/process";
10
+
11
+ export interface PrepareCommand {
12
+ readonly command: string;
13
+ readonly args: ReadonlyArray<string>;
14
+ }
15
+
16
+ /**
17
+ * Where a push goes: a cloud deployment through its deploy key, or a local
18
+ * backend through the CLI's self-hosted target and its admin key.
19
+ */
20
+ export type PushTarget =
21
+ | {
22
+ readonly _tag: "Cloud";
23
+ readonly deploymentName: string;
24
+ readonly deployKey: Redacted.Redacted<string>;
25
+ }
26
+ | {
27
+ readonly _tag: "SelfHosted";
28
+ readonly deploymentName: string;
29
+ readonly url: string;
30
+ readonly adminKey: Redacted.Redacted<string>;
31
+ };
32
+
33
+ const selection = (target: PushTarget) =>
34
+ target._tag === "Cloud"
35
+ ? `CONVEX_DEPLOY_KEY=${Redacted.value(target.deployKey)}\n`
36
+ : `CONVEX_SELF_HOSTED_URL=${target.url}\nCONVEX_SELF_HOSTED_ADMIN_KEY=${Redacted.value(target.adminKey)}\n`;
37
+
38
+ export class CommandError extends Schema.TaggedError<CommandError>()(
39
+ "CommandError",
40
+ {
41
+ operation: Schema.Literals(["prepare", "deploy"]),
42
+ reason: Schema.Literals(["configuration", "spawn", "exit"]),
43
+ exitCode: Schema.optionalKey(Schema.Finite),
44
+ },
45
+ ) {}
46
+
47
+ export class CommandRunner extends Context.Service<
48
+ CommandRunner,
49
+ {
50
+ readonly prepare: (
51
+ command: PrepareCommand,
52
+ cwd?: string,
53
+ ) => Effect.Effect<void, CommandError>;
54
+ readonly deploy: (
55
+ target: PushTarget,
56
+ cwd?: string,
57
+ ) => Effect.Effect<void, CommandError>;
58
+ }
59
+ >()("@confect/alchemy/internal/CommandRunner") {}
60
+
61
+ /**
62
+ * The ambient variables a child process needs to run on every platform; the
63
+ * rest of the parent environment, credentials included, is withheld.
64
+ */
65
+ export const processEnvironment = Config.all({
66
+ PATH: Config.String("PATH").pipe(
67
+ Config.orElse(() => Config.String("Path")),
68
+ Config.withDefault(""),
69
+ ),
70
+ HOME: Config.String("HOME").pipe(Config.withDefault("")),
71
+ USERPROFILE: Config.String("USERPROFILE").pipe(Config.withDefault("")),
72
+ SystemRoot: Config.String("SystemRoot").pipe(
73
+ Config.orElse(() => Config.String("SYSTEMROOT")),
74
+ Config.withDefault(""),
75
+ ),
76
+ TMPDIR: Config.String("TMPDIR").pipe(Config.withDefault("")),
77
+ TEMP: Config.String("TEMP").pipe(Config.withDefault("")),
78
+ });
79
+
80
+ const packageBin = Effect.fn("CommandRunner.packageBin")(function* (
81
+ fs: FileSystem.FileSystem,
82
+ path: Path.Path,
83
+ cwd: string,
84
+ packageName: string,
85
+ binName: string,
86
+ ) {
87
+ const invalid = () =>
88
+ new CommandError({
89
+ operation: packageName === "convex" ? "deploy" : "prepare",
90
+ reason: "configuration",
91
+ });
92
+ let directory = cwd;
93
+ while (true) {
94
+ const manifestPath = path.join(
95
+ directory,
96
+ "node_modules",
97
+ packageName,
98
+ "package.json",
99
+ );
100
+ if (yield* fs.exists(manifestPath).pipe(Effect.mapError(invalid))) {
101
+ const manifest = yield* fs.readFileString(manifestPath).pipe(
102
+ Effect.flatMap(
103
+ Schema.decodeEffect(
104
+ Schema.fromJsonString(
105
+ Schema.Struct({
106
+ bin: Schema.Union([
107
+ Schema.String,
108
+ Schema.Record(Schema.String, Schema.String),
109
+ ]),
110
+ }),
111
+ ),
112
+ ),
113
+ ),
114
+ Effect.mapError(invalid),
115
+ );
116
+ const relative =
117
+ typeof manifest.bin === "string" ? manifest.bin : manifest.bin[binName];
118
+ if (!relative) return yield* invalid();
119
+ return path.resolve(path.dirname(manifestPath), relative);
120
+ }
121
+ const parent = path.dirname(directory);
122
+ if (parent === directory) return yield* invalid();
123
+ directory = parent;
124
+ }
125
+ });
126
+
127
+ export const layer = Layer.effect(
128
+ CommandRunner,
129
+ Effect.gen(function* () {
130
+ const fs = yield* FileSystem.FileSystem;
131
+ const path = yield* Path.Path;
132
+ const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
133
+ const run = Effect.fn("CommandRunner.run")(function* (
134
+ operation: "prepare" | "deploy",
135
+ command: string,
136
+ args: ReadonlyArray<string>,
137
+ cwd: string,
138
+ env: Record<string, string | undefined>,
139
+ ) {
140
+ const code = yield* spawner
141
+ .exitCode(
142
+ ChildProcess.make(command, args, {
143
+ cwd,
144
+ env,
145
+ extendEnv: false,
146
+ stdin: "ignore",
147
+ stdout: "ignore",
148
+ stderr: "ignore",
149
+ }),
150
+ )
151
+ .pipe(
152
+ Effect.mapError(
153
+ () => new CommandError({ operation, reason: "spawn" }),
154
+ ),
155
+ );
156
+ if (code !== 0)
157
+ return yield* new CommandError({
158
+ operation,
159
+ reason: "exit",
160
+ exitCode: Number(code),
161
+ });
162
+ });
163
+ const environment = processEnvironment.pipe(
164
+ Effect.map((env) => ({ ...env, CI: "true", DO_NOT_TRACK: "1" })),
165
+ );
166
+ return CommandRunner.of({
167
+ prepare: Effect.fn("CommandRunner.prepare")(function* (
168
+ command,
169
+ cwd = process.cwd(),
170
+ ) {
171
+ const directory = path.resolve(cwd);
172
+ const env = yield* environment.pipe(
173
+ Effect.mapError(
174
+ () =>
175
+ new CommandError({
176
+ operation: "prepare",
177
+ reason: "configuration",
178
+ }),
179
+ ),
180
+ );
181
+ if (command.command === "confect") {
182
+ const bin = yield* packageBin(
183
+ fs,
184
+ path,
185
+ directory,
186
+ "@confect/cli",
187
+ "confect",
188
+ );
189
+ yield* run(
190
+ "prepare",
191
+ process.execPath,
192
+ [bin, ...command.args],
193
+ directory,
194
+ env,
195
+ );
196
+ } else {
197
+ yield* run("prepare", command.command, command.args, directory, env);
198
+ }
199
+ }),
200
+ deploy: Effect.fn("CommandRunner.deploy")(function* (
201
+ target,
202
+ cwd = process.cwd(),
203
+ ) {
204
+ const directory = path.resolve(cwd);
205
+ const bin = yield* packageBin(fs, path, directory, "convex", "convex");
206
+ const env = yield* environment.pipe(
207
+ Effect.mapError(
208
+ () =>
209
+ new CommandError({
210
+ operation: "deploy",
211
+ reason: "configuration",
212
+ }),
213
+ ),
214
+ );
215
+ yield* Effect.scoped(
216
+ Effect.gen(function* () {
217
+ const temp = yield* fs.makeTempDirectoryScoped({
218
+ prefix: "confect-deploy-",
219
+ });
220
+ const envFile = path.join(temp, "deployment.env");
221
+ yield* fs.writeFileString(envFile, selection(target), {
222
+ mode: 0o600,
223
+ });
224
+ yield* run(
225
+ "deploy",
226
+ process.execPath,
227
+ [bin, "deploy", "--yes", "--env-file", envFile],
228
+ directory,
229
+ {
230
+ ...env,
231
+ HOME: temp,
232
+ USERPROFILE: temp,
233
+ LOCALAPPDATA: temp,
234
+ XDG_CONFIG_HOME: temp,
235
+ },
236
+ );
237
+ }).pipe(
238
+ Effect.mapError((error) =>
239
+ Schema.is(CommandError)(error)
240
+ ? error
241
+ : new CommandError({
242
+ operation: "deploy",
243
+ reason: "configuration",
244
+ }),
245
+ ),
246
+ ),
247
+ );
248
+ }),
249
+ });
250
+ }),
251
+ );
@@ -0,0 +1,136 @@
1
+ import * as Crypto from "effect/Crypto";
2
+ import type * as Duration from "effect/Duration";
3
+ import * as Effect from "effect/Effect";
4
+ import * as FileSystem from "effect/FileSystem";
5
+ import * as Option from "effect/Option";
6
+ import * as Path from "effect/Path";
7
+ import * as Predicate from "effect/Predicate";
8
+ import * as Schedule from "effect/Schedule";
9
+ import * as Schema from "effect/Schema";
10
+
11
+ export class FileLockError extends Schema.TaggedError<FileLockError>()(
12
+ "FileLockError",
13
+ { reason: Schema.Literals(["held", "timeout", "filesystem"]) },
14
+ ) {}
15
+
16
+ const Holder = Schema.fromJsonString(
17
+ Schema.Struct({ pid: Schema.Finite, token: Schema.String }),
18
+ );
19
+ const decodeHolder = Schema.decodeUnknownOption(Holder);
20
+
21
+ const isAlive = (pid: number) => {
22
+ try {
23
+ process.kill(pid, 0);
24
+ return true;
25
+ } catch (error) {
26
+ return Predicate.hasProperty(error, "code") && error.code === "EPERM";
27
+ }
28
+ };
29
+
30
+ /**
31
+ * Hold an exclusive lock file for the ambient scope, waiting up to `wait` for
32
+ * another holder or failing with `held` at once when `wait` is omitted.
33
+ *
34
+ * The holder, `{ pid, token }`, is written to a private claim file and
35
+ * published with a hard link, which fails when the lock exists, so a visible
36
+ * lock always names its holder. A lock whose holder has exited is removed only
37
+ * under `<file>.recovery`, after re-reading that the same dead holder still
38
+ * owns it, so a successor's lock is never removed in its place. Publishing and
39
+ * recovering are uninterruptible: Node cannot cancel a link or unlink it has
40
+ * started, so stopping early would let one land after `.recovery` is released.
41
+ * The recovery lock is never reclaimed: a crash inside its few file operations
42
+ * leaves waiters timing out rather than overlapping. A timeout that lands while
43
+ * an attempt succeeds still fails, and the published lock is removed only when
44
+ * the ambient scope closes.
45
+ */
46
+ export const acquire = Effect.fn("FileLock.acquire")(function* (
47
+ file: string,
48
+ wait?: Duration.Input,
49
+ ) {
50
+ const fs = yield* FileSystem.FileSystem;
51
+ const path = yield* Path.Path;
52
+ const crypto = yield* Crypto.Crypto;
53
+ const failed = () => new FileLockError({ reason: "filesystem" });
54
+ const token = Buffer.from(
55
+ yield* crypto.randomBytes(16).pipe(Effect.mapError(failed)),
56
+ ).toString("hex");
57
+ const holder = yield* Schema.encodeEffect(Holder)({
58
+ pid: process.pid,
59
+ token,
60
+ }).pipe(Effect.mapError(failed));
61
+ const claim = `${file}.${token}`;
62
+ const recovery = `${file}.recovery`;
63
+ const removeIfOwned = (target: string) =>
64
+ fs.readFileString(target).pipe(
65
+ Effect.flatMap((content) =>
66
+ content === holder ? fs.remove(target) : Effect.void,
67
+ ),
68
+ Effect.ignore,
69
+ );
70
+ yield* Effect.addFinalizer(() =>
71
+ Effect.all([
72
+ removeIfOwned(file),
73
+ fs.remove(claim, { force: true }).pipe(Effect.ignore),
74
+ ]),
75
+ );
76
+ yield* fs
77
+ .makeDirectory(path.dirname(file), { recursive: true })
78
+ .pipe(Effect.mapError(failed));
79
+ yield* fs
80
+ .writeFileString(claim, holder, { mode: 0o600 })
81
+ .pipe(Effect.mapError(failed));
82
+
83
+ const read = (target: string) =>
84
+ fs.readFileString(target).pipe(
85
+ Effect.map(decodeHolder),
86
+ Effect.orElseSucceed(() => Option.none()),
87
+ );
88
+ const publish = (target: string) =>
89
+ fs.link(claim, target).pipe(
90
+ Effect.as(true),
91
+ Effect.catchReason("PlatformError", "AlreadyExists", () =>
92
+ Effect.succeed(false),
93
+ ),
94
+ );
95
+ // Whether a dead holder's lock was removed.
96
+ const reclaim = Effect.gen(function* () {
97
+ const stale = yield* read(file);
98
+ if (Option.isNone(stale) || isAlive(stale.value.pid)) return false;
99
+ if (!(yield* publish(recovery))) return false;
100
+ return yield* read(file).pipe(
101
+ Effect.flatMap((current) =>
102
+ Option.isSome(current) && current.value.token === stale.value.token
103
+ ? fs.remove(file).pipe(Effect.as(true))
104
+ : Effect.succeed(false),
105
+ ),
106
+ Effect.ensuring(removeIfOwned(recovery)),
107
+ );
108
+ });
109
+ const attempt = publish(file).pipe(
110
+ Effect.flatMap((acquired) =>
111
+ acquired
112
+ ? Effect.succeed(true)
113
+ : Effect.flatMap(reclaim, (removed) =>
114
+ removed ? publish(file) : Effect.succeed(false),
115
+ ),
116
+ ),
117
+ Effect.uninterruptible,
118
+ Effect.mapError(failed),
119
+ );
120
+
121
+ if (wait === undefined) {
122
+ if (!(yield* attempt)) return yield* new FileLockError({ reason: "held" });
123
+ } else {
124
+ yield* attempt.pipe(
125
+ Effect.repeat({
126
+ until: (acquired) => acquired,
127
+ schedule: Schedule.spaced("100 millis"),
128
+ }),
129
+ Effect.timeoutOrElse({
130
+ duration: wait,
131
+ orElse: () => Effect.fail(new FileLockError({ reason: "timeout" })),
132
+ }),
133
+ );
134
+ }
135
+ yield* fs.remove(claim, { force: true }).pipe(Effect.ignore);
136
+ });
@@ -0,0 +1,49 @@
1
+ import { AdoptPolicy } from "alchemy/AdoptPolicy";
2
+ import { AlchemyContext } from "alchemy/AlchemyContext";
3
+ import { Stack } from "alchemy/Stack";
4
+ import { State } from "alchemy/State/State";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Option from "effect/Option";
7
+ import * as Schema from "effect/Schema";
8
+
9
+ export class LifecycleStateError extends Schema.TaggedError<LifecycleStateError>()(
10
+ "LifecycleStateError",
11
+ { fqn: Schema.String },
12
+ ) {}
13
+
14
+ export const checkpointCreate = Effect.fn("Lifecycle.checkpointCreate")(
15
+ function* (fqn: string, instanceId: string, props: object, output?: object) {
16
+ const stack = yield* Effect.serviceOption(Stack);
17
+ if (Option.isNone(stack)) return;
18
+ const state = yield* Effect.serviceOption(State);
19
+ if (Option.isNone(state)) return yield* new LifecycleStateError({ fqn });
20
+ const store = yield* state.value;
21
+ const key = { stack: stack.value.name, stage: stack.value.stage, fqn };
22
+ const row = yield* store.get(key);
23
+ if (!row || row.kind === "action" || row.instanceId !== instanceId)
24
+ return yield* new LifecycleStateError({ fqn });
25
+ if (row.status !== "creating") return;
26
+ yield* store.set({
27
+ ...key,
28
+ value: {
29
+ ...row,
30
+ props,
31
+ ...(output === undefined ? {} : { attr: output }),
32
+ },
33
+ });
34
+ },
35
+ );
36
+
37
+ export const shouldAdopt = Effect.fn("Lifecycle.shouldAdopt")(function* (
38
+ fqn: string,
39
+ ) {
40
+ const stack = yield* Effect.serviceOption(Stack);
41
+ const resourcePolicy = Option.isSome(stack)
42
+ ? stack.value.resources[fqn]?.Adopt
43
+ : undefined;
44
+ if (resourcePolicy !== undefined) return resourcePolicy;
45
+ const policy = yield* Effect.serviceOption(AdoptPolicy);
46
+ if (Option.isSome(policy)) return policy.value;
47
+ const context = yield* Effect.serviceOption(AlchemyContext);
48
+ return Option.isSome(context) && context.value.adopt;
49
+ });