@prisma/cli 3.0.0-alpha.1 → 3.0.0-alpha.11

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 (56) hide show
  1. package/README.md +1 -16
  2. package/dist/adapters/git.js +49 -0
  3. package/dist/adapters/local-state.js +39 -1
  4. package/dist/adapters/token-storage.js +57 -1
  5. package/dist/cli2.js +60 -4
  6. package/dist/commands/app/index.js +41 -21
  7. package/dist/commands/auth/index.js +3 -2
  8. package/dist/commands/branch/index.js +2 -1
  9. package/dist/commands/env.js +87 -0
  10. package/dist/commands/git/index.js +36 -0
  11. package/dist/commands/project/index.js +12 -14
  12. package/dist/commands/version/index.js +18 -0
  13. package/dist/controllers/app-env.js +223 -0
  14. package/dist/controllers/app.js +1026 -169
  15. package/dist/controllers/auth.js +9 -9
  16. package/dist/controllers/branch.js +6 -6
  17. package/dist/controllers/project.js +451 -161
  18. package/dist/controllers/version.js +12 -0
  19. package/dist/lib/app/bun-project.js +1 -1
  20. package/dist/lib/app/deploy-output.js +15 -0
  21. package/dist/lib/app/env-config.js +57 -0
  22. package/dist/lib/app/env-vars.js +4 -4
  23. package/dist/lib/app/local-dev.js +1 -1
  24. package/dist/lib/app/preview-build.js +128 -1
  25. package/dist/lib/app/preview-interaction.js +2 -35
  26. package/dist/lib/app/preview-progress.js +43 -58
  27. package/dist/lib/app/preview-provider.js +125 -24
  28. package/dist/lib/auth/auth-ops.js +58 -13
  29. package/dist/lib/auth/client.js +1 -1
  30. package/dist/lib/auth/guard.js +1 -1
  31. package/dist/lib/auth/login.js +115 -4
  32. package/dist/lib/project/local-pin.js +51 -0
  33. package/dist/lib/project/resolution.js +201 -0
  34. package/dist/lib/version.js +55 -0
  35. package/dist/output/patterns.js +15 -18
  36. package/dist/presenters/app-env.js +129 -0
  37. package/dist/presenters/app.js +16 -29
  38. package/dist/presenters/auth.js +2 -2
  39. package/dist/presenters/branch.js +6 -6
  40. package/dist/presenters/project.js +87 -44
  41. package/dist/presenters/version.js +29 -0
  42. package/dist/shell/command-meta.js +148 -91
  43. package/dist/shell/command-runner.js +45 -7
  44. package/dist/shell/errors.js +9 -3
  45. package/dist/shell/global-flags.js +13 -1
  46. package/dist/shell/help.js +8 -7
  47. package/dist/shell/output.js +29 -12
  48. package/dist/shell/prompt.js +12 -2
  49. package/dist/shell/runtime.js +1 -1
  50. package/dist/shell/ui.js +19 -1
  51. package/dist/use-cases/auth.js +9 -12
  52. package/dist/use-cases/branch.js +20 -20
  53. package/dist/use-cases/create-cli-gateways.js +3 -13
  54. package/dist/use-cases/project.js +2 -48
  55. package/package.json +3 -3
  56. package/dist/adapters/config.js +0 -74
@@ -1,5 +1,5 @@
1
- import path from "node:path";
2
1
  import { access, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
3
  //#region src/lib/app/bun-project.ts
4
4
  async function readBunPackageJson(appPath) {
5
5
  const packageJsonPath = path.join(appPath, "package.json");
@@ -0,0 +1,15 @@
1
+ import { padDisplay } from "../../shell/ui.js";
2
+ //#region src/lib/app/deploy-output.ts
3
+ const DEPLOY_OUTPUT_MIN_LABEL_WIDTH = 9;
4
+ const DEPLOY_OUTPUT_MIN_VALUE_WIDTH = 9;
5
+ function renderDeployOutputRows(ui, rows) {
6
+ if (rows.length === 0) return [];
7
+ const labelWidth = Math.max(DEPLOY_OUTPUT_MIN_LABEL_WIDTH, ...rows.map((row) => row.label.length));
8
+ const valueWidth = Math.max(DEPLOY_OUTPUT_MIN_VALUE_WIDTH, ...rows.map((row) => row.value?.length ?? 0));
9
+ return rows.map((row) => {
10
+ if (!row.value) return ` ${row.label}`;
11
+ return ` ${padDisplay(row.label, labelWidth)} ${padDisplay(ui.strong(row.value), valueWidth)}${row.origin ? ` ${ui.dim(`· ${row.origin}`)}` : ""}`.trimEnd();
12
+ });
13
+ }
14
+ //#endregion
15
+ export { renderDeployOutputRows };
@@ -0,0 +1,57 @@
1
+ import { usageError } from "../../shell/errors.js";
2
+ //#region src/lib/app/env-config.ts
3
+ const VALID_ROLES = new Set(["production", "preview"]);
4
+ function positionalHint(command) {
5
+ if (command === "add" || command === "update") return "KEY=value ";
6
+ if (command === "rm") return "KEY ";
7
+ return "";
8
+ }
9
+ function resolveEnvScope(flags, options) {
10
+ if (flags.roleName) {
11
+ if (!VALID_ROLES.has(flags.roleName)) throw usageError(`Unknown role "${flags.roleName}"`, "--role accepts production or preview.", "Pass --role production or --role preview.", [`prisma-cli project env ${options.command} --role production`, `prisma-cli project env ${options.command} --role preview`], "app");
12
+ return {
13
+ kind: "role",
14
+ role: flags.roleName
15
+ };
16
+ }
17
+ if (options.requireExplicit) {
18
+ const positional = positionalHint(options.command);
19
+ throw usageError(`prisma-cli project env ${options.command} requires --role`, "Writing without an explicit scope is rejected so the command never silently targets production.", "Pass --role production or --role preview.", [`prisma-cli project env ${options.command} ${positional}--role production`, `prisma-cli project env ${options.command} ${positional}--role preview`], "app");
20
+ }
21
+ return null;
22
+ }
23
+ function parseKeyValuePositional(raw, command, env = process.env) {
24
+ if (!raw) throw usageError(`prisma-cli project env ${command} requires KEY=VALUE`, "No KEY=VALUE positional argument was supplied.", "Pass the variable as KEY=VALUE, e.g. STRIPE_KEY=sk_test_xxx.", [`prisma-cli project env ${command} STRIPE_KEY=sk_test_xxx --role production`], "app");
25
+ const separatorIndex = raw.indexOf("=");
26
+ if (separatorIndex === -1) {
27
+ if (KEY_SHAPE.test(raw)) {
28
+ validateKey(raw, command);
29
+ const value = env[raw];
30
+ if (typeof value === "string" && value.length > 0) return {
31
+ key: raw,
32
+ value
33
+ };
34
+ throw usageError(`Value for "${raw}" was not provided`, `No KEY=VALUE assignment was supplied, and ${raw} is not set in the current environment.`, "Pass KEY=VALUE or export the variable before running the command.", [`prisma-cli project env ${command} ${raw}=value --role production`, `${raw}=value prisma-cli project env ${command} ${raw} --role production`], "app");
35
+ }
36
+ throw usageError(`KEY=VALUE argument is missing the = separator`, `"${raw}" does not contain an = character.`, "Pass the variable as KEY=VALUE, e.g. STRIPE_KEY=sk_test_xxx.", [`prisma-cli project env ${command} STRIPE_KEY=sk_test_xxx --role production`], "app");
37
+ }
38
+ const key = raw.slice(0, separatorIndex);
39
+ const value = raw.slice(separatorIndex + 1);
40
+ validateKey(key, command);
41
+ if (value.length === 0) throw usageError(`KEY=VALUE argument has an empty value`, `"${raw}" has an empty value after the = separator.`, `Pass a non-empty value, or use prisma-cli project env rm to remove a variable.`, [`prisma-cli project env ${command} ${key}=value --role production`], "app");
42
+ return {
43
+ key,
44
+ value
45
+ };
46
+ }
47
+ const KEY_SHAPE = /^[A-Z_][A-Z0-9_]*$/;
48
+ function validateKey(key, command) {
49
+ if (key.length === 0) throw usageError(`Variable key cannot be empty`, "An empty key was passed.", "Pass an env-var key, e.g. STRIPE_KEY.", [`prisma-cli project env ${command} STRIPE_KEY${command === "rm" ? "" : "=value"} --role production`], "app");
50
+ if (key.length > 256) throw usageError(`Variable key "${key}" exceeds the 256-character limit`, "Env-var keys are capped at 256 characters by the platform.", "Use a shorter key.", [], "app");
51
+ if (!KEY_SHAPE.test(key)) throw usageError(`Variable key "${key}" must match the POSIX env-var shape`, "Keys must start with an uppercase letter or underscore and contain only uppercase letters, digits, and underscores.", "Rename the key to match [A-Z_][A-Z0-9_]*.", [`prisma-cli project env ${command} STRIPE_KEY${command === "rm" ? "" : "=value"} --role production`], "app");
52
+ }
53
+ function formatScopeLabel(scope) {
54
+ return scope.role;
55
+ }
56
+ //#endregion
57
+ export { formatScopeLabel, parseKeyValuePositional, resolveEnvScope };
@@ -2,15 +2,15 @@ import { usageError } from "../../shell/errors.js";
2
2
  //#region src/lib/app/env-vars.ts
3
3
  function parseEnvAssignments(assignments, options) {
4
4
  const values = assignments ?? [];
5
- if (options.requireAtLeastOne && values.length === 0) throw usageError("At least one environment variable is required", `prisma app ${options.commandName} needs at least one --env NAME=VALUE flag in the current mode.`, `Pass one or more --env NAME=VALUE flags, for example prisma app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
5
+ if (options.requireAtLeastOne && values.length === 0) throw usageError("At least one environment variable is required", `prisma-cli app ${options.commandName} needs at least one --env NAME=VALUE flag in the current mode.`, `Pass one or more --env NAME=VALUE flags, for example prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
6
6
  const parsed = {};
7
7
  const seen = /* @__PURE__ */ new Set();
8
8
  for (const assignment of values) {
9
9
  const separatorIndex = assignment.indexOf("=");
10
- if (separatorIndex === -1) throw usageError("Environment variable assignment must use NAME=VALUE", "A provided --env flag is missing the = separator.", `Pass repeated --env NAME=VALUE flags, for example prisma app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
10
+ if (separatorIndex === -1) throw usageError("Environment variable assignment must use NAME=VALUE", "A provided --env flag is missing the = separator.", `Pass repeated --env NAME=VALUE flags, for example prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
11
11
  const name = assignment.slice(0, separatorIndex);
12
- if (name.length === 0) throw usageError("Environment variable name is required", "A provided --env flag has an empty variable name.", `Pass repeated --env NAME=VALUE flags, for example prisma app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
13
- if (seen.has(name)) throw usageError(`Environment variable "${name}" was provided more than once`, "Each environment variable name may be set only once per command invocation.", `Remove the duplicate "${name}" assignment and rerun prisma app ${options.commandName}.`, [`prisma app ${options.commandName} --env ${name}=value`], "app");
12
+ if (name.length === 0) throw usageError("Environment variable name is required", "A provided --env flag has an empty variable name.", `Pass repeated --env NAME=VALUE flags, for example prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example.`, [`prisma-cli app ${options.commandName} --env DATABASE_URL=postgresql://example`], "app");
13
+ if (seen.has(name)) throw usageError(`Environment variable "${name}" was provided more than once`, "Each environment variable name may be set only once per command invocation.", `Remove the duplicate "${name}" assignment and rerun prisma-cli app ${options.commandName}.`, [`prisma-cli app ${options.commandName} --env ${name}=value`], "app");
14
14
  seen.add(name);
15
15
  parsed[name] = assignment.slice(separatorIndex + 1);
16
16
  }
@@ -1,6 +1,6 @@
1
1
  import { readBunPackageEntrypoint, readBunPackageJson, resolveBunEntrypoint } from "./bun-project.js";
2
- import path from "node:path";
3
2
  import { access } from "node:fs/promises";
3
+ import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  //#region src/lib/app/local-dev.ts
6
6
  const NEXT_CONFIG_FILENAMES = [
@@ -1,6 +1,6 @@
1
1
  import { resolveBunEntrypoint } from "./bun-project.js";
2
+ import { chmod, copyFile, cp, lstat, mkdir, readFile, readdir, readlink, rm, stat } from "node:fs/promises";
2
3
  import path from "node:path";
3
- import { cp, readdir, readlink, rm, stat } from "node:fs/promises";
4
4
  import { AstroBuild, BunBuild, NextjsBuild, NuxtBuild, TanstackStartBuild } from "@prisma/compute-sdk";
5
5
  //#region src/lib/app/preview-build.ts
6
6
  const PREVIEW_BUILD_TYPES = [
@@ -46,6 +46,7 @@ async function executePreviewBuild(options) {
46
46
  });
47
47
  const artifact = await strategy.execute();
48
48
  try {
49
+ if (buildType === "nextjs") await restageNextjsArtifact(artifact.directory, options.appPath);
49
50
  await normalizeArtifactSymlinks(artifact.directory, options.appPath);
50
51
  return {
51
52
  artifact,
@@ -104,6 +105,68 @@ async function createPreviewBuildStrategy(options) {
104
105
  }
105
106
  }
106
107
  }
108
+ async function stageNextjsStandaloneArtifact(options) {
109
+ const standaloneRoot = path.resolve(options.standaloneDir);
110
+ const artifactRoot = path.resolve(options.artifactDir);
111
+ const appRoot = path.resolve(options.appPath);
112
+ await copyPathMaterializingSymlinks(standaloneRoot, artifactRoot, {
113
+ standaloneRoot,
114
+ appRoot,
115
+ sourceRoot: await resolveSourceRoot(appRoot)
116
+ });
117
+ await hoistPnpmDependencies(path.join(artifactRoot, "node_modules"));
118
+ }
119
+ async function restageNextjsArtifact(artifactDir, appPath) {
120
+ const standaloneDir = path.join(appPath, ".next", "standalone");
121
+ await rm(artifactDir, {
122
+ recursive: true,
123
+ force: true
124
+ });
125
+ await stageNextjsStandaloneArtifact({
126
+ standaloneDir,
127
+ artifactDir,
128
+ appPath
129
+ });
130
+ const publicDir = path.join(appPath, "public");
131
+ if (await directoryExists(publicDir)) await cp(publicDir, path.join(artifactDir, "public"), {
132
+ recursive: true,
133
+ verbatimSymlinks: true
134
+ });
135
+ const staticDir = path.join(appPath, ".next", "static");
136
+ if (await directoryExists(staticDir)) await cp(staticDir, path.join(artifactDir, ".next", "static"), {
137
+ recursive: true,
138
+ verbatimSymlinks: true
139
+ });
140
+ }
141
+ async function hoistPnpmDependencies(nodeModulesDir) {
142
+ const pnpmNodeModulesDir = path.join(nodeModulesDir, ".pnpm", "node_modules");
143
+ if (!await directoryExists(pnpmNodeModulesDir)) return;
144
+ const entries = await readdir(pnpmNodeModulesDir, { withFileTypes: true });
145
+ for (const entry of entries) {
146
+ const sourcePath = path.join(pnpmNodeModulesDir, entry.name);
147
+ if (entry.name.startsWith("@") && entry.isDirectory()) {
148
+ const scopedEntries = await readdir(sourcePath, { withFileTypes: true });
149
+ for (const scopedEntry of scopedEntries) {
150
+ const scopedDestination = path.join(nodeModulesDir, entry.name, scopedEntry.name);
151
+ if (await pathExists(scopedDestination)) continue;
152
+ await mkdir(path.dirname(scopedDestination), { recursive: true });
153
+ await copyPathMaterializingSymlinks(path.join(sourcePath, scopedEntry.name), scopedDestination, {
154
+ standaloneRoot: pnpmNodeModulesDir,
155
+ appRoot: nodeModulesDir,
156
+ sourceRoot: nodeModulesDir
157
+ });
158
+ }
159
+ continue;
160
+ }
161
+ const destinationPath = path.join(nodeModulesDir, entry.name);
162
+ if (await pathExists(destinationPath)) continue;
163
+ await copyPathMaterializingSymlinks(sourcePath, destinationPath, {
164
+ standaloneRoot: pnpmNodeModulesDir,
165
+ appRoot: nodeModulesDir,
166
+ sourceRoot: nodeModulesDir
167
+ });
168
+ }
169
+ }
107
170
  async function normalizeArtifactSymlinks(artifactDir, appPath) {
108
171
  const normalizedArtifactDir = path.resolve(artifactDir);
109
172
  const normalizedAppPath = path.resolve(appPath);
@@ -138,5 +201,69 @@ function isPathWithin(rootPath, candidatePath) {
138
201
  const relativePath = path.relative(rootPath, candidatePath);
139
202
  return relativePath === "" || !relativePath.startsWith(`..${path.sep}`) && relativePath !== ".." && !path.isAbsolute(relativePath);
140
203
  }
204
+ async function copyPathMaterializingSymlinks(sourcePath, destinationPath, options) {
205
+ const sourceStat = await lstat(sourcePath);
206
+ if (sourceStat.isSymbolicLink()) {
207
+ await copyPathMaterializingSymlinks(await resolveSymlinkTarget(sourcePath, options), destinationPath, options);
208
+ return;
209
+ }
210
+ if (sourceStat.isDirectory()) {
211
+ await mkdir(destinationPath, { recursive: true });
212
+ const entries = await readdir(sourcePath, { withFileTypes: true });
213
+ for (const entry of entries) await copyPathMaterializingSymlinks(path.join(sourcePath, entry.name), path.join(destinationPath, entry.name), options);
214
+ return;
215
+ }
216
+ if (sourceStat.isFile()) {
217
+ await mkdir(path.dirname(destinationPath), { recursive: true });
218
+ await copyFile(sourcePath, destinationPath);
219
+ await chmod(destinationPath, sourceStat.mode);
220
+ }
221
+ }
222
+ async function resolveSymlinkTarget(symlinkPath, options) {
223
+ const linkTarget = await readlink(symlinkPath);
224
+ const resolvedTarget = path.resolve(path.dirname(symlinkPath), linkTarget);
225
+ if (await pathExists(resolvedTarget)) {
226
+ if (!isPathWithin(options.appRoot, resolvedTarget) && !isPathWithin(options.sourceRoot, resolvedTarget)) throw new Error(`Build artifact symlink escapes the app directory: ${resolvedTarget}`);
227
+ return resolvedTarget;
228
+ }
229
+ if (isPathWithin(options.standaloneRoot, resolvedTarget)) {
230
+ const fallbackTarget = path.join(options.appRoot, path.relative(options.standaloneRoot, resolvedTarget));
231
+ if (await pathExists(fallbackTarget)) return fallbackTarget;
232
+ }
233
+ throw new Error(`Next.js standalone symlink target is missing: ${symlinkPath} -> ${linkTarget} (resolved to ${resolvedTarget})`);
234
+ }
235
+ async function pathExists(targetPath) {
236
+ try {
237
+ await stat(targetPath);
238
+ return true;
239
+ } catch {
240
+ return false;
241
+ }
242
+ }
243
+ async function directoryExists(targetPath) {
244
+ try {
245
+ return (await stat(targetPath)).isDirectory();
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+ async function resolveSourceRoot(appRoot) {
251
+ let current = path.resolve(appRoot);
252
+ while (true) {
253
+ if (await pathExists(path.join(current, ".git")) || await pathExists(path.join(current, "pnpm-workspace.yaml")) || await pathExists(path.join(current, "bun.lock")) || await pathExists(path.join(current, "bun.lockb")) || await packageJsonDeclaresWorkspaces(current)) return current;
254
+ const parent = path.dirname(current);
255
+ if (parent === current) return path.resolve(appRoot);
256
+ current = parent;
257
+ }
258
+ }
259
+ async function packageJsonDeclaresWorkspaces(directory) {
260
+ try {
261
+ const content = await readFile(path.join(directory, "package.json"), "utf8");
262
+ const parsed = JSON.parse(content);
263
+ return Boolean(parsed.workspaces);
264
+ } catch {
265
+ return false;
266
+ }
267
+ }
141
268
  //#endregion
142
269
  export { PREVIEW_BUILD_TYPES, PreviewBuildStrategy, RESOLVED_PREVIEW_BUILD_TYPES, executePreviewBuild };
@@ -1,38 +1,5 @@
1
- import { selectPrompt, textPrompt } from "../../shell/prompt.js";
1
+ import "../../shell/prompt.js";
2
2
  //#region src/lib/app/preview-interaction.ts
3
- const CREATE_NEW_APP = "__create_new_app__";
4
3
  const PREVIEW_DEFAULT_REGION = "eu-central-1";
5
- function createPreviewDeployInteraction(context) {
6
- return {
7
- async selectService(services) {
8
- const sorted = services.slice().sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
9
- const selection = await selectPrompt({
10
- input: context.runtime.stdin,
11
- output: context.runtime.stderr,
12
- message: "Select an app",
13
- choices: [...sorted.map((service) => ({
14
- label: service.name,
15
- value: service.id
16
- })), {
17
- label: "Create a new app",
18
- value: CREATE_NEW_APP
19
- }]
20
- });
21
- return selection === CREATE_NEW_APP ? null : selection;
22
- },
23
- async provideServiceName() {
24
- return textPrompt({
25
- input: context.runtime.stdin,
26
- output: context.runtime.stderr,
27
- message: "App name",
28
- placeholder: "hello-world",
29
- validate: (value) => !value?.trim() ? "App name is required" : void 0
30
- }).then((value) => value.trim());
31
- },
32
- async selectRegion(_regions) {
33
- return PREVIEW_DEFAULT_REGION;
34
- }
35
- };
36
- }
37
4
  //#endregion
38
- export { PREVIEW_DEFAULT_REGION, createPreviewDeployInteraction };
5
+ export { PREVIEW_DEFAULT_REGION };
@@ -1,83 +1,68 @@
1
+ import { renderDeployOutputRows } from "./deploy-output.js";
1
2
  //#region src/lib/app/preview-progress.ts
2
- function createPreviewDeployProgress(output, enabled) {
3
- if (!enabled) return;
3
+ function createPreviewDeployProgressState() {
4
+ return {
5
+ buildStarted: false,
6
+ buildCompleted: false,
7
+ archiveReady: false,
8
+ uploadCompleted: false,
9
+ versionId: null,
10
+ startRequested: false,
11
+ containerLive: false,
12
+ deploymentUrl: null,
13
+ promotedUrl: null
14
+ };
15
+ }
16
+ function createPreviewDeployProgress(output, ui, enabled, state = createPreviewDeployProgressState()) {
4
17
  const write = (line) => {
18
+ if (!enabled) return;
5
19
  output.write(`${line}\n`);
6
20
  };
21
+ const writeRows = (rows) => {
22
+ for (const line of renderDeployOutputRows(ui, rows)) write(line);
23
+ };
7
24
  return {
8
25
  onBuildStart() {
9
- write("Building application...");
26
+ state.buildStarted = true;
27
+ write("Building locally...");
10
28
  },
11
29
  onBuildComplete() {
12
- write("Build complete.");
13
- },
14
- onArchiveCreating() {
15
- write("Creating deployment artifact...");
30
+ state.buildCompleted = true;
16
31
  },
17
32
  onArchiveReady(byteLength) {
18
- write(`Artifact ready (${(byteLength / 1024).toFixed(1)} KB).`);
33
+ state.archiveReady = true;
34
+ writeRows([{
35
+ label: "Built",
36
+ value: formatArtifactSize(byteLength)
37
+ }]);
38
+ },
39
+ onUploadStart() {
40
+ write("Uploading...");
19
41
  },
20
42
  onVersionCreated(versionId) {
21
- write(`Deployment ${versionId} created.`);
43
+ state.versionId = versionId;
22
44
  },
23
45
  onUploadComplete() {
24
- write("Upload complete.");
46
+ state.uploadCompleted = true;
47
+ writeRows([{ label: "Uploaded" }]);
25
48
  },
26
49
  onStartRequested() {
27
- write("Starting deployment...");
28
- },
29
- onStatusChange(status) {
30
- write(`Status: ${status}`);
50
+ state.startRequested = true;
51
+ write("Deploying...");
31
52
  },
32
53
  onRunning(url) {
33
- if (url) {
34
- write(`Deployment is running at ${url}.`);
35
- return;
36
- }
37
- write("Deployment is running.");
38
- },
39
- onPromoteStart() {
40
- write("Promoting deployment...");
54
+ state.containerLive = true;
55
+ state.deploymentUrl = url;
56
+ writeRows([{ label: "Deployed" }]);
41
57
  },
42
58
  onPromoted(url) {
43
- if (url) {
44
- write(`Promoted to ${url}.`);
45
- return;
46
- }
47
- write("Promotion complete.");
48
- },
49
- onPromoteFailed(error) {
50
- write(`Promotion failed${error?.message ? `: ${error.message}` : "."}`);
51
- },
52
- onOldVersionStopping(versionId) {
53
- write(`Stopping previous deployment ${versionId}...`);
54
- },
55
- onOldVersionStopped(versionId) {
56
- write(`Previous deployment ${versionId} stopped.`);
57
- },
58
- onOldVersionStopFailed(versionId) {
59
- write(`Failed to stop previous deployment ${versionId} (non-fatal).`);
60
- },
61
- onOldVersionDeleting(versionId) {
62
- write(`Deleting previous deployment ${versionId}...`);
63
- },
64
- onOldVersionDeleted(versionId) {
65
- write(`Previous deployment ${versionId} deleted.`);
66
- },
67
- onOldVersionDeleteFailed(versionId) {
68
- write(`Failed to delete previous deployment ${versionId} (non-fatal).`);
69
- },
70
- onCleanupDanglingVersion(versionId) {
71
- write(`Cleaning up deployment ${versionId}...`);
72
- },
73
- onCleanupDanglingVersionComplete(versionId) {
74
- write(`Deployment ${versionId} cleaned up.`);
75
- },
76
- onCleanupDanglingVersionFailed(versionId) {
77
- write(`Failed to clean up deployment ${versionId}.`);
59
+ state.promotedUrl = url;
78
60
  }
79
61
  };
80
62
  }
63
+ function formatArtifactSize(byteLength) {
64
+ return `${(byteLength / 1024 / 1024).toFixed(1)} MB`;
65
+ }
81
66
  function createPreviewPromoteProgress(output, enabled) {
82
67
  if (!enabled) return;
83
68
  const write = (line) => {
@@ -136,4 +121,4 @@ function createPreviewUpdateEnvProgress(output, enabled) {
136
121
  };
137
122
  }
138
123
  //#endregion
139
- export { createPreviewDeployProgress, createPreviewPromoteProgress, createPreviewUpdateEnvProgress };
124
+ export { createPreviewDeployProgress, createPreviewDeployProgressState, createPreviewPromoteProgress, createPreviewUpdateEnvProgress };
@@ -1,9 +1,9 @@
1
1
  import { envVarNames } from "./env-vars.js";
2
2
  import { PreviewBuildStrategy } from "./preview-build.js";
3
3
  import path from "node:path";
4
- import { ApiError, ComputeClient } from "@prisma/compute-sdk";
4
+ import { ApiError, CancelledError, ComputeClient, streamLogs } from "@prisma/compute-sdk";
5
5
  //#region src/lib/app/preview-provider.ts
6
- function createPreviewAppProvider(client) {
6
+ function createPreviewAppProvider(client, options) {
7
7
  const sdk = new ComputeClient(client);
8
8
  return {
9
9
  async createProject(options) {
@@ -14,25 +14,11 @@ function createPreviewAppProvider(client) {
14
14
  name: projectResult.value.name
15
15
  };
16
16
  },
17
- async listApps(projectId) {
18
- const servicesResult = await sdk.listServices({ projectId });
19
- if (servicesResult.isErr()) throw new Error(servicesResult.error.message);
20
- return (await Promise.all(servicesResult.value.map(async (service) => {
21
- const detailResult = await sdk.showService({ serviceId: service.id });
22
- return detailResult.isOk() ? detailResult.value : {
23
- id: service.id,
24
- name: service.name,
25
- region: service.region,
26
- latestVersionId: null,
27
- serviceEndpointDomain: void 0
28
- };
29
- }))).map((service) => ({
30
- id: service.id,
31
- name: service.name,
32
- region: service.region ?? null,
33
- liveDeploymentId: service.latestVersionId ?? null,
34
- liveUrl: toAbsoluteUrl(service.serviceEndpointDomain ?? null)
35
- }));
17
+ async listApps(projectId, options) {
18
+ return listComputeServices(client, {
19
+ projectId,
20
+ branchGitName: options?.branchName
21
+ });
36
22
  },
37
23
  async removeApp(appId) {
38
24
  const appResult = await sdk.showService({ serviceId: appId });
@@ -60,6 +46,20 @@ function createPreviewAppProvider(client) {
60
46
  if (promoteResult.isErr()) throw new Error(promoteResult.error.message);
61
47
  },
62
48
  async deployApp(options) {
49
+ const resolvedApp = options.appId ? {
50
+ appId: options.appId,
51
+ appName: options.appName,
52
+ region: options.region
53
+ } : options.branchName && options.appName ? await createBranchApp(client, {
54
+ projectId: options.projectId,
55
+ branchName: options.branchName,
56
+ appName: options.appName,
57
+ region: options.region
58
+ }) : {
59
+ appId: void 0,
60
+ appName: options.appName,
61
+ region: options.region
62
+ };
63
63
  const deployResult = await sdk.deploy({
64
64
  strategy: new PreviewBuildStrategy({
65
65
  appPath: path.resolve(options.cwd),
@@ -67,9 +67,9 @@ function createPreviewAppProvider(client) {
67
67
  buildType: options.buildType
68
68
  }),
69
69
  projectId: options.projectId,
70
- serviceId: options.appId,
71
- serviceName: options.appName,
72
- region: options.region,
70
+ serviceId: resolvedApp.appId,
71
+ serviceName: resolvedApp.appName,
72
+ region: resolvedApp.region,
73
73
  portMapping: options.portMapping,
74
74
  envVars: options.envVars,
75
75
  timeoutSeconds: 120,
@@ -197,9 +197,110 @@ function createPreviewAppProvider(client) {
197
197
  live: null
198
198
  }
199
199
  };
200
+ },
201
+ async streamDeploymentLogs(streamOptions) {
202
+ if (!options?.baseUrl || !options.getToken) throw new Error("Log streaming requires an authenticated API base URL and token.");
203
+ const result = await streamLogs({
204
+ baseUrl: options.baseUrl,
205
+ token: await options.getToken(),
206
+ versionId: streamOptions.deploymentId,
207
+ signal: streamOptions.signal
208
+ }, streamOptions.onRecord);
209
+ if (result.isErr()) {
210
+ if (CancelledError.is(result.error)) return;
211
+ throw result.error;
212
+ }
200
213
  }
201
214
  };
202
215
  }
216
+ async function listBranches(client, options) {
217
+ const result = await client.GET("/v1/projects/{projectId}/branches", { params: {
218
+ path: { projectId: options.projectId },
219
+ query: { gitName: options.gitName }
220
+ } });
221
+ if (result.error || !result.data) throw apiCallError("Failed to list branches", result.response, result.error);
222
+ return result.data.data;
223
+ }
224
+ async function resolveOrCreateBranch(client, options) {
225
+ const existing = (await listBranches(client, options))[0];
226
+ if (existing) return existing;
227
+ const result = await client.POST("/v1/projects/{projectId}/branches", {
228
+ params: { path: { projectId: options.projectId } },
229
+ body: {
230
+ gitName: options.gitName,
231
+ isDefault: options.gitName === "main"
232
+ }
233
+ });
234
+ if (result.error || !result.data) {
235
+ if (result.response.status === 409) {
236
+ const raced = (await listBranches(client, options))[0];
237
+ if (raced) return raced;
238
+ }
239
+ throw apiCallError(`Failed to create branch "${options.gitName}"`, result.response, result.error);
240
+ }
241
+ return result.data.data;
242
+ }
243
+ async function listComputeServices(client, options) {
244
+ const services = [];
245
+ let cursor;
246
+ while (true) {
247
+ const result = await client.GET("/v1/compute-services", { params: { query: {
248
+ projectId: options.projectId,
249
+ branchGitName: options.branchGitName,
250
+ cursor
251
+ } } });
252
+ if (result.error || !result.data) throw apiCallError("Failed to list apps", result.response, result.error);
253
+ services.push(...result.data.data);
254
+ if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
255
+ cursor = result.data.pagination.nextCursor;
256
+ }
257
+ return services.map((service) => ({
258
+ id: service.id,
259
+ name: service.name,
260
+ region: service.region.id ?? null,
261
+ branchId: service.branchId,
262
+ liveDeploymentId: service.latestVersionId ?? null,
263
+ liveUrl: toAbsoluteUrl(service.serviceEndpointDomain ?? null)
264
+ }));
265
+ }
266
+ async function createBranchApp(client, options) {
267
+ const branch = await resolveOrCreateBranch(client, {
268
+ projectId: options.projectId,
269
+ gitName: options.branchName
270
+ });
271
+ const result = await client.POST("/v1/compute-services", { body: {
272
+ projectId: options.projectId,
273
+ branchId: branch.id,
274
+ displayName: options.appName,
275
+ ...options.region ? { regionId: options.region } : {}
276
+ } });
277
+ if (result.error || !result.data) {
278
+ if (result.response.status === 409) {
279
+ const matched = (await listComputeServices(client, {
280
+ projectId: options.projectId,
281
+ branchGitName: options.branchName
282
+ })).find((app) => app.name === options.appName);
283
+ if (matched) return {
284
+ appId: matched.id,
285
+ appName: matched.name,
286
+ region: matched.region ?? options.region
287
+ };
288
+ }
289
+ throw apiCallError(`Failed to create app "${options.appName}"`, result.response, result.error);
290
+ }
291
+ const service = result.data.data;
292
+ return {
293
+ appId: service.id,
294
+ appName: service.name,
295
+ region: service.region.id ?? options.region
296
+ };
297
+ }
298
+ function apiCallError(summary, response, error) {
299
+ if (response.status === 404) return /* @__PURE__ */ new Error("Resource Not Found");
300
+ const message = error.error?.message ?? `Management API returned HTTP ${response.status}.`;
301
+ const hint = error.error?.hint ? ` ${error.error.hint}` : "";
302
+ return /* @__PURE__ */ new Error(`${summary}: ${message}${hint}`);
303
+ }
203
304
  async function findAppForDeployment(sdk, deploymentId) {
204
305
  const projectsResult = await sdk.listProjects();
205
306
  if (projectsResult.isErr()) throw new Error(projectsResult.error.message);