@prisma/cli 3.0.0-beta.1 → 3.0.0-beta.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 (66) hide show
  1. package/README.md +5 -3
  2. package/dist/adapters/git.js +8 -3
  3. package/dist/adapters/local-state.js +12 -4
  4. package/dist/adapters/mock-api.js +81 -2
  5. package/dist/adapters/token-storage.js +64 -23
  6. package/dist/cli.js +18 -3
  7. package/dist/cli2.js +9 -5
  8. package/dist/commands/app/index.js +84 -59
  9. package/dist/commands/branch/index.js +2 -27
  10. package/dist/commands/database/index.js +159 -0
  11. package/dist/commands/env.js +8 -4
  12. package/dist/controllers/app-env-api.js +54 -0
  13. package/dist/controllers/app-env-file.js +181 -0
  14. package/dist/controllers/app-env.js +286 -131
  15. package/dist/controllers/app.js +699 -244
  16. package/dist/controllers/auth.js +8 -8
  17. package/dist/controllers/branch.js +78 -48
  18. package/dist/controllers/database.js +318 -0
  19. package/dist/controllers/project.js +156 -85
  20. package/dist/lib/app/branch-database-deploy.js +325 -0
  21. package/dist/lib/app/branch-database.js +215 -0
  22. package/dist/lib/app/bun-project.js +12 -5
  23. package/dist/lib/app/compute-config.js +147 -0
  24. package/dist/lib/app/deploy-output.js +10 -1
  25. package/dist/lib/app/deploy-plan.js +58 -0
  26. package/dist/lib/app/env-config.js +1 -1
  27. package/dist/lib/app/env-file.js +82 -0
  28. package/dist/lib/app/env-vars.js +28 -2
  29. package/dist/lib/app/local-dev.js +8 -49
  30. package/dist/lib/app/preview-branch-database.js +102 -0
  31. package/dist/lib/app/preview-build-settings.js +376 -0
  32. package/dist/lib/app/preview-build.js +314 -97
  33. package/dist/lib/app/preview-provider.js +178 -65
  34. package/dist/lib/app/production-deploy-gate.js +161 -0
  35. package/dist/lib/auth/auth-ops.js +30 -21
  36. package/dist/lib/auth/guard.js +3 -2
  37. package/dist/lib/auth/login.js +116 -14
  38. package/dist/lib/database/provider.js +167 -0
  39. package/dist/lib/diagnostics.js +15 -0
  40. package/dist/lib/fs/home-path.js +24 -0
  41. package/dist/lib/git/local-branch.js +53 -0
  42. package/dist/lib/git/local-status.js +57 -0
  43. package/dist/lib/project/interactive-setup.js +1 -1
  44. package/dist/lib/project/local-pin.js +182 -33
  45. package/dist/lib/project/resolution.js +204 -50
  46. package/dist/lib/project/setup.js +66 -19
  47. package/dist/output/patterns.js +1 -1
  48. package/dist/presenters/app-env.js +149 -14
  49. package/dist/presenters/app.js +178 -23
  50. package/dist/presenters/branch.js +37 -102
  51. package/dist/presenters/database.js +274 -0
  52. package/dist/presenters/project.js +57 -39
  53. package/dist/presenters/verbose-context.js +64 -0
  54. package/dist/shell/command-meta.js +103 -13
  55. package/dist/shell/command-runner.js +57 -19
  56. package/dist/shell/diagnostics-output.js +57 -0
  57. package/dist/shell/errors.js +12 -1
  58. package/dist/shell/help.js +30 -20
  59. package/dist/shell/output.js +10 -1
  60. package/dist/shell/runtime.js +11 -7
  61. package/dist/shell/ui.js +42 -3
  62. package/dist/shell/update-check.js +247 -0
  63. package/dist/use-cases/branch.js +20 -68
  64. package/dist/use-cases/create-cli-gateways.js +2 -17
  65. package/dist/use-cases/project.js +2 -1
  66. package/package.json +26 -10
@@ -0,0 +1,147 @@
1
+ import { CliError } from "../../shell/errors.js";
2
+ import { COMPUTE_CONFIG_FILENAME, COMPUTE_CONFIG_FILENAME as COMPUTE_CONFIG_FILENAME$1, ComputeConfigTargetRequiredError, computeTargetAppDir, frameworkByKey, inferComputeTargetFromCwd, loadComputeConfig, selectComputeDeployTarget } from "@prisma/compute-sdk/config";
3
+ import path from "node:path";
4
+ import { matchError } from "better-result";
5
+ //#region src/lib/app/compute-config.ts
6
+ /**
7
+ * Loads the nearest compute config, searching from `cwd` up to the source
8
+ * root (repository or workspace boundary). Thin adapter over the SDK loader
9
+ * keeping the CLI's positional-signal call shape.
10
+ */
11
+ async function loadComputeConfig$1(cwd, signal) {
12
+ return loadComputeConfig(cwd, { signal });
13
+ }
14
+ /** Local build/run strategy implied by a configured framework. */
15
+ function computeFrameworkToBuildType(framework) {
16
+ return frameworkByKey(framework).buildType;
17
+ }
18
+ function mergeComputeDeployInputs(options) {
19
+ const { cli, target, configFilename } = options;
20
+ const configAnnotation = `set by ${configFilename}`;
21
+ const framework = cli.framework ? {
22
+ value: cli.framework,
23
+ annotation: "set by --framework"
24
+ } : target?.framework ? {
25
+ value: target.framework,
26
+ annotation: configAnnotation
27
+ } : void 0;
28
+ const entrypoint = cli.entrypoint ? {
29
+ value: cli.entrypoint,
30
+ annotation: "set by --entry"
31
+ } : target?.entry ? {
32
+ value: target.entry,
33
+ annotation: configAnnotation
34
+ } : void 0;
35
+ const httpPort = cli.httpPort ? {
36
+ value: cli.httpPort,
37
+ annotation: "set by --http-port"
38
+ } : target?.httpPort ? {
39
+ value: String(target.httpPort),
40
+ annotation: configAnnotation
41
+ } : void 0;
42
+ const cliEnvInputs = cli.envInputs && cli.envInputs.length > 0 ? cli.envInputs : void 0;
43
+ const configEnvInputs = target && target.envInputs.length > 0 ? target.envInputs : void 0;
44
+ const envInputs = cliEnvInputs ?? configEnvInputs;
45
+ const configAppName = target?.name ? {
46
+ value: target.name,
47
+ annotation: configAnnotation
48
+ } : target?.key ? {
49
+ value: target.key,
50
+ annotation: configAnnotation
51
+ } : void 0;
52
+ return {
53
+ framework,
54
+ entrypoint,
55
+ httpPort,
56
+ envInputs,
57
+ envInputsFromConfig: !cliEnvInputs && configEnvInputs !== void 0,
58
+ configAppName,
59
+ appRoot: target?.root ?? void 0
60
+ };
61
+ }
62
+ /**
63
+ * Merges CLI inputs for the local `app build` and `app run` commands with a
64
+ * selected config target. Explicit flags win; `--build-type auto` is the
65
+ * flag default and defers to the configured framework.
66
+ */
67
+ function mergeComputeLocalInputs(options) {
68
+ const { cli, target } = options;
69
+ const cliBuildType = cli.buildType && cli.buildType !== "auto" ? cli.buildType : void 0;
70
+ const configBuildType = target?.framework ? computeFrameworkToBuildType(target.framework) : void 0;
71
+ return {
72
+ entrypoint: cli.entrypoint ?? target?.entry ?? void 0,
73
+ buildType: cliBuildType ?? configBuildType,
74
+ buildTypeFromConfig: !cliBuildType && configBuildType !== void 0,
75
+ port: cli.port ?? (target?.httpPort ? String(target.httpPort) : void 0),
76
+ appRoot: target?.root ?? void 0
77
+ };
78
+ }
79
+ function computeConfigErrorToCliError(error, commandName = "deploy") {
80
+ const command = `prisma-cli app ${commandName}`;
81
+ return matchError(error, {
82
+ ComputeConfigAmbiguousError: (ambiguous) => new CliError({
83
+ code: "COMPUTE_CONFIG_INVALID",
84
+ domain: "app",
85
+ summary: "Multiple compute config files found",
86
+ why: ambiguous.message,
87
+ fix: `Keep exactly one compute config file, preferably ${COMPUTE_CONFIG_FILENAME}.`,
88
+ meta: { configPaths: ambiguous.configPaths },
89
+ exitCode: 2,
90
+ nextSteps: [command]
91
+ }),
92
+ ComputeConfigLoadError: (load) => new CliError({
93
+ code: "COMPUTE_CONFIG_INVALID",
94
+ domain: "app",
95
+ summary: `Could not load ${path.basename(load.configPath)}`,
96
+ why: load.message,
97
+ fix: `Fix the error in ${path.basename(load.configPath)} and rerun the command.`,
98
+ where: load.configPath,
99
+ meta: { configPath: load.configPath },
100
+ exitCode: 2,
101
+ nextSteps: [command]
102
+ }),
103
+ ComputeConfigInvalidError: (invalid) => new CliError({
104
+ code: "COMPUTE_CONFIG_INVALID",
105
+ domain: "app",
106
+ summary: `Invalid ${path.basename(invalid.configPath)}`,
107
+ why: invalid.issues.join(" "),
108
+ fix: `Edit ${path.basename(invalid.configPath)} so it default-exports defineComputeConfig({ app }) or defineComputeConfig({ apps }).`,
109
+ where: invalid.configPath,
110
+ meta: {
111
+ configPath: invalid.configPath,
112
+ issues: invalid.issues
113
+ },
114
+ exitCode: 2,
115
+ nextSteps: [command]
116
+ }),
117
+ ComputeConfigTargetRequiredError: (required) => new CliError({
118
+ code: "COMPUTE_CONFIG_TARGET_REQUIRED",
119
+ domain: "app",
120
+ summary: "App target required",
121
+ why: required.message,
122
+ fix: `Pass the app target, for example ${command} <target>.`,
123
+ meta: {
124
+ configPath: required.configPath,
125
+ availableTargets: required.availableTargets
126
+ },
127
+ exitCode: 2,
128
+ nextSteps: required.availableTargets.map((target) => `${command} ${target}`)
129
+ }),
130
+ ComputeConfigTargetUnknownError: (unknown) => new CliError({
131
+ code: "COMPUTE_CONFIG_TARGET_UNKNOWN",
132
+ domain: "app",
133
+ summary: `Unknown app target "${unknown.requestedTarget}"`,
134
+ why: unknown.message,
135
+ fix: unknown.availableTargets.length > 0 ? `Pass one of the configured targets: ${unknown.availableTargets.join(", ")}.` : "Remove the target argument; this config defines a single app.",
136
+ meta: {
137
+ configPath: unknown.configPath,
138
+ requestedTarget: unknown.requestedTarget,
139
+ availableTargets: unknown.availableTargets
140
+ },
141
+ exitCode: 2,
142
+ nextSteps: unknown.availableTargets.length > 0 ? unknown.availableTargets.map((target) => `${command} ${target}`) : [command]
143
+ })
144
+ });
145
+ }
146
+ //#endregion
147
+ export { COMPUTE_CONFIG_FILENAME$1 as COMPUTE_CONFIG_FILENAME, ComputeConfigTargetRequiredError, computeConfigErrorToCliError, computeFrameworkToBuildType, computeTargetAppDir, inferComputeTargetFromCwd, loadComputeConfig$1 as loadComputeConfig, mergeComputeDeployInputs, mergeComputeLocalInputs, selectComputeDeployTarget };
@@ -2,6 +2,7 @@ import { padDisplay } from "../../shell/ui.js";
2
2
  //#region src/lib/app/deploy-output.ts
3
3
  const DEPLOY_OUTPUT_MIN_LABEL_WIDTH = 9;
4
4
  const DEPLOY_OUTPUT_MIN_VALUE_WIDTH = 9;
5
+ const DEPLOY_SETTINGS_MIN_KEY_WIDTH = 10;
5
6
  function renderDeployOutputRows(ui, rows) {
6
7
  if (rows.length === 0) return [];
7
8
  const labelWidth = Math.max(DEPLOY_OUTPUT_MIN_LABEL_WIDTH, ...rows.map((row) => row.label.length));
@@ -11,5 +12,13 @@ function renderDeployOutputRows(ui, rows) {
11
12
  return ` ${padDisplay(row.label, labelWidth)} ${padDisplay(ui.strong(row.value), valueWidth)}${row.origin ? ` ${ui.dim(`· ${row.origin}`)}` : ""}`.trimEnd();
12
13
  });
13
14
  }
15
+ function renderDeploySettingsPreview(ui, rows) {
16
+ if (rows.length === 0) return [];
17
+ const keyWidth = Math.max(DEPLOY_SETTINGS_MIN_KEY_WIDTH, ...rows.map((row) => `${row.key}:`.length));
18
+ const rail = ui.dim("│");
19
+ return rows.map((row) => {
20
+ return `${rail} ${ui.accent(padDisplay(`${row.key}:`, keyWidth))} ${ui.strong(row.value)}`;
21
+ });
22
+ }
14
23
  //#endregion
15
- export { renderDeployOutputRows };
24
+ export { renderDeployOutputRows, renderDeploySettingsPreview };
@@ -0,0 +1,58 @@
1
+ //#region src/lib/app/deploy-plan.ts
2
+ /**
3
+ * Decides whether a deploy covers one app or the whole system. A multi-app
4
+ * config with no target named or inferred deploys every target in declaration
5
+ * order; everything else (single-app config, an explicit/inferred target, or
6
+ * no config at all) is a single deploy.
7
+ */
8
+ function planAppDeploy(options) {
9
+ const { config, requestedTarget, hasCreateProject } = options;
10
+ if (!(config !== null && config.kind === "multi" && !requestedTarget && config.targets.length > 1)) return { mode: "single" };
11
+ const total = config.targets.length;
12
+ return {
13
+ mode: "all",
14
+ targets: config.targets.map((target, index) => ({
15
+ targetKey: target.key,
16
+ index,
17
+ total,
18
+ bindsCreateProject: index === 0 && hasCreateProject
19
+ }))
20
+ };
21
+ }
22
+ /**
23
+ * Names the per-app inputs present in a deploy-all invocation, in the order
24
+ * they should be reported. An empty list means the run is unambiguous; the
25
+ * caller renders the usage error when it is not.
26
+ */
27
+ function perAppInputsForDeployAll(inputs) {
28
+ return [
29
+ ["--app", inputs.appName],
30
+ ["--framework", inputs.framework],
31
+ ["--entry", inputs.entrypoint],
32
+ ["--http-port", inputs.httpPort],
33
+ ["--env", inputs.envAssignments?.length ? inputs.envAssignments : void 0],
34
+ [inputs.appIdEnvVar.name, inputs.appIdEnvVar.value]
35
+ ].filter(([, value]) => value !== void 0).map(([flag]) => flag);
36
+ }
37
+ /**
38
+ * Describes a deploy-all run that stopped at the first failing target: which
39
+ * targets already went live and which were never attempted, so the caller can
40
+ * surface a converge path. Pure; the caller folds this into the CliError.
41
+ */
42
+ function describeDeployAllFailure(options) {
43
+ const { targetKeys, failedIndex, completed } = options;
44
+ const failedTarget = targetKeys[failedIndex];
45
+ const notAttempted = targetKeys.slice(failedIndex + 1);
46
+ return {
47
+ failedTarget,
48
+ completed,
49
+ notAttempted,
50
+ contextLines: [
51
+ `Deploying all apps stopped at "${failedTarget}" (${failedIndex + 1}/${targetKeys.length}).`,
52
+ ...completed.length > 0 ? [`Already live: ${completed.map((entry) => entry.target).join(", ")}.`] : [],
53
+ ...notAttempted.length > 0 ? [`Not attempted: ${notAttempted.join(", ")}.`] : []
54
+ ]
55
+ };
56
+ }
57
+ //#endregion
58
+ export { describeDeployAllFailure, perAppInputsForDeployAll, planAppDeploy };
@@ -64,4 +64,4 @@ function formatScopeLabel(scope) {
64
64
  return `branch:${scope.branchName}`;
65
65
  }
66
66
  //#endregion
67
- export { formatScopeLabel, parseKeyValuePositional, resolveEnvScope };
67
+ export { formatScopeLabel, parseKeyValuePositional, resolveEnvScope, validateKey };
@@ -0,0 +1,82 @@
1
+ import { usageError } from "../../shell/errors.js";
2
+ import { validateKey } from "./env-config.js";
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { parse } from "dotenv";
6
+ //#region src/lib/app/env-file.ts
7
+ const ASSIGNMENT_KEY_PATTERN = /^\s*(?:export\s+)?([^#=\s]+)\s*=/;
8
+ async function readEnvFileAssignments(cwd, filePath, command) {
9
+ const resolvedPath = path.resolve(cwd, filePath);
10
+ let contents;
11
+ try {
12
+ contents = await readFile(resolvedPath, "utf8");
13
+ } catch (error) {
14
+ throw usageError(`Failed to read env file "${filePath}"`, error instanceof Error ? error.message : "The file could not be read.", "Pass a readable dotenv file path.", [command === "deploy" ? "prisma-cli app deploy --env .env" : `prisma-cli project env ${command} --file .env --role preview`], "app");
15
+ }
16
+ return parseEnvFileContents(contents, filePath, command);
17
+ }
18
+ function parseEnvFileContents(contents, filePath, command) {
19
+ const parsedKeys = extractParsedKeys(contents);
20
+ if (parsedKeys.length === 0) throw usageError(`No environment variables found in "${filePath}"`, "The file does not contain any KEY=VALUE assignments.", "Pass a dotenv file with at least one non-empty variable.", [], "app");
21
+ const seen = /* @__PURE__ */ new Map();
22
+ for (const entry of parsedKeys) {
23
+ validateEnvFileKey(entry.key, entry.line, filePath, command);
24
+ const firstLine = seen.get(entry.key);
25
+ if (firstLine !== void 0) throw usageError(`Duplicate environment variable "${entry.key}" in "${filePath}"`, `Lines ${firstLine} and ${entry.line} both define ${entry.key}.`, "Keep one assignment for each key before importing the file.", [], "app");
26
+ seen.set(entry.key, entry.line);
27
+ }
28
+ const parsedValues = parse(contents);
29
+ return parsedKeys.map(({ key }) => {
30
+ const value = parsedValues[key];
31
+ if (typeof value !== "string" || value.length === 0) {
32
+ const line = seen.get(key);
33
+ throw usageError(`Environment variable "${key}" in "${filePath}" has an empty value`, line === void 0 ? `${key} has an empty value.` : `Line ${line} defines ${key} with an empty value.`, "Pass a non-empty value, or omit the key from the file.", [], "app");
34
+ }
35
+ return {
36
+ key,
37
+ value
38
+ };
39
+ });
40
+ }
41
+ function extractParsedKeys(contents) {
42
+ const keys = [];
43
+ let multilineQuote = null;
44
+ const lines = contents.split(/\n/);
45
+ for (const [index, line] of lines.entries()) {
46
+ const lineNumber = index + 1;
47
+ if (multilineQuote !== null) {
48
+ if (hasClosingQuote(line, multilineQuote, 0)) multilineQuote = null;
49
+ continue;
50
+ }
51
+ const match = ASSIGNMENT_KEY_PATTERN.exec(line);
52
+ if (!match) continue;
53
+ const key = match[1];
54
+ keys.push({
55
+ key,
56
+ line: lineNumber
57
+ });
58
+ const valueStart = line.slice(match[0].length).trimStart();
59
+ const openingQuote = valueStart[0];
60
+ if ((openingQuote === "\"" || openingQuote === "'" || openingQuote === "`") && !hasClosingQuote(valueStart, openingQuote, 1)) multilineQuote = openingQuote;
61
+ }
62
+ return keys;
63
+ }
64
+ function validateEnvFileKey(key, line, filePath, command) {
65
+ try {
66
+ validateKey(key, command === "deploy" ? "add" : command);
67
+ } catch (error) {
68
+ const reason = error instanceof Error && error.message.length > 0 ? error.message : "Invalid environment variable key.";
69
+ throw usageError(`Invalid environment variable "${key}" in "${filePath}"`, `Line ${line}: ${reason}`, "Use a valid env-var key and retry the import.", [], "app");
70
+ }
71
+ }
72
+ function hasClosingQuote(value, quote, startIndex) {
73
+ for (let index = startIndex; index < value.length; index += 1) if (value[index] === quote && !isEscaped(value, index)) return true;
74
+ return false;
75
+ }
76
+ function isEscaped(value, index) {
77
+ let backslashes = 0;
78
+ for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) backslashes += 1;
79
+ return backslashes % 2 === 1;
80
+ }
81
+ //#endregion
82
+ export { readEnvFileAssignments };
@@ -1,4 +1,6 @@
1
1
  import { usageError } from "../../shell/errors.js";
2
+ import { validateKey } from "./env-config.js";
3
+ import { readEnvFileAssignments } from "./env-file.js";
2
4
  //#region src/lib/app/env-vars.ts
3
5
  function parseEnvAssignments(assignments, options) {
4
6
  const values = assignments ?? [];
@@ -10,15 +12,39 @@ function parseEnvAssignments(assignments, options) {
10
12
  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
13
  const name = assignment.slice(0, separatorIndex);
12
14
  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");
15
+ validateEnvAssignmentName(name, options.commandName);
13
16
  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");
17
+ const value = assignment.slice(separatorIndex + 1);
18
+ if (value.length === 0) throw usageError(`Environment variable "${name}" has an empty value`, `A provided --env flag defines ${name} with no value.`, "Pass a non-empty value, or omit the key from the deploy command.", [`prisma-cli app ${options.commandName} --env ${name}=value`], "app");
14
19
  seen.add(name);
15
- parsed[name] = assignment.slice(separatorIndex + 1);
20
+ parsed[name] = value;
16
21
  }
17
22
  return parsed;
18
23
  }
24
+ async function parseEnvInputs(cwd, inputs, options) {
25
+ const values = inputs ?? [];
26
+ const expandedAssignments = [];
27
+ for (const value of values) {
28
+ if (value.includes("=")) {
29
+ expandedAssignments.push(value);
30
+ continue;
31
+ }
32
+ const fileAssignments = await readEnvFileAssignments(cwd, value, options.commandName);
33
+ expandedAssignments.push(...fileAssignments.map((assignment) => `${assignment.key}=${assignment.value}`));
34
+ }
35
+ return parseEnvAssignments(expandedAssignments, options);
36
+ }
37
+ function validateEnvAssignmentName(name, commandName) {
38
+ try {
39
+ validateKey(name, "add");
40
+ } catch (error) {
41
+ const reason = error instanceof Error && error.message.length > 0 ? error.message : "Invalid environment variable name.";
42
+ throw usageError(`Invalid environment variable "${name}"`, reason, "Use a valid env-var name and retry the deploy.", [`prisma-cli app ${commandName} --env DATABASE_URL=postgresql://example`], "app");
43
+ }
44
+ }
19
45
  function envVarNames(envVars) {
20
46
  if (!envVars) return [];
21
47
  return Object.entries(envVars).filter(([, value]) => value !== null).map(([name]) => name).sort((left, right) => left.localeCompare(right));
22
48
  }
23
49
  //#endregion
24
- export { envVarNames, parseEnvAssignments };
50
+ export { envVarNames, parseEnvInputs };
@@ -1,25 +1,9 @@
1
- import { readBunPackageEntrypoint, readBunPackageJson, resolveBunEntrypoint } from "./bun-project.js";
2
- import { access } from "node:fs/promises";
1
+ import { resolveBunEntrypoint } from "./bun-project.js";
2
+ import "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { spawn } from "node:child_process";
5
5
  //#region src/lib/app/local-dev.ts
6
- const NEXT_CONFIG_FILENAMES = [
7
- "next.config.js",
8
- "next.config.mjs",
9
- "next.config.ts",
10
- "next.config.mts"
11
- ];
12
6
  const DEFAULT_LOCAL_DEV_PORT = 3e3;
13
- async function resolveLocalBuildType(appPath, buildType) {
14
- if (buildType === "bun" || buildType === "nextjs") return buildType;
15
- if (buildType !== "auto") return null;
16
- return detectLocalBuildType(appPath);
17
- }
18
- async function detectLocalBuildType(appPath) {
19
- if (await isNextProject(appPath)) return "nextjs";
20
- if (await isBunProject(appPath)) return "bun";
21
- return null;
22
- }
23
7
  async function runLocalApp(options) {
24
8
  const spawnImpl = options.spawnImpl ?? spawn;
25
9
  if (options.buildType === "nextjs") {
@@ -58,7 +42,8 @@ async function runLocalApp(options) {
58
42
  env: {
59
43
  ...options.env,
60
44
  PORT: String(options.port)
61
- }
45
+ },
46
+ signal: options.signal
62
47
  }, spawnImpl, "Could not find the Next.js CLI. Install it with `npm install next` or ensure npx/bunx is available.");
63
48
  return {
64
49
  framework: "nextjs",
@@ -69,7 +54,7 @@ async function runLocalApp(options) {
69
54
  signal: command.signal
70
55
  };
71
56
  }
72
- const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint);
57
+ const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint, options.signal);
73
58
  const command = await runWithFallback([{
74
59
  command: "bun",
75
60
  args: ["--watch", entrypoint],
@@ -79,7 +64,8 @@ async function runLocalApp(options) {
79
64
  env: {
80
65
  ...options.env,
81
66
  PORT: String(options.port)
82
- }
67
+ },
68
+ signal: options.signal
83
69
  }, spawnImpl, "Bun is required to run this app locally. Install it from https://bun.sh.");
84
70
  return {
85
71
  framework: "bun",
@@ -90,33 +76,6 @@ async function runLocalApp(options) {
90
76
  signal: command.signal
91
77
  };
92
78
  }
93
- async function isNextProject(appPath) {
94
- for (const fileName of NEXT_CONFIG_FILENAMES) try {
95
- await access(path.join(appPath, fileName));
96
- return true;
97
- } catch {}
98
- return hasDependency(await readBunPackageJson(appPath), "next");
99
- }
100
- async function isBunProject(appPath) {
101
- try {
102
- await access(path.join(appPath, "bun.lock"));
103
- return true;
104
- } catch {}
105
- try {
106
- await access(path.join(appPath, "bun.lockb"));
107
- return true;
108
- } catch {}
109
- const packageJson = await readBunPackageJson(appPath);
110
- if (!packageJson) return false;
111
- const hasEntrypoint = typeof readBunPackageEntrypoint(packageJson) === "string";
112
- const hasBunDependency = hasDependency(packageJson, "@types/bun") || hasDependency(packageJson, "bun");
113
- const usesBunScripts = (typeof packageJson.scripts === "object" && packageJson.scripts !== null ? Object.values(packageJson.scripts) : []).some((value) => typeof value === "string" && /\bbun\b/.test(value));
114
- return hasEntrypoint && (hasBunDependency || usesBunScripts);
115
- }
116
- function hasDependency(packageJson, dependencyName) {
117
- if (!packageJson) return false;
118
- return [packageJson.dependencies, packageJson.devDependencies].some((group) => typeof group === "object" && group !== null && dependencyName in group);
119
- }
120
79
  async function runWithFallback(candidates, options, spawnImpl, missingCommandMessage) {
121
80
  for (const candidate of candidates) try {
122
81
  const result = await spawnCommand(candidate, options, spawnImpl);
@@ -147,4 +106,4 @@ function spawnCommand(candidate, options, spawnImpl) {
147
106
  });
148
107
  }
149
108
  //#endregion
150
- export { DEFAULT_LOCAL_DEV_PORT, resolveLocalBuildType, runLocalApp };
109
+ export { DEFAULT_LOCAL_DEV_PORT, runLocalApp };
@@ -0,0 +1,102 @@
1
+ //#region src/lib/app/preview-branch-database.ts
2
+ async function createBranchDatabase(client, options) {
3
+ const result = await client.POST("/v1/databases", {
4
+ body: {
5
+ projectId: options.projectId,
6
+ branchId: options.branchId,
7
+ name: options.branchName,
8
+ source: { type: "empty" }
9
+ },
10
+ signal: options.signal
11
+ });
12
+ if (result.error || !result.data) throw apiCallError(`Failed to create database for branch "${options.branchName}"`, result.response, result.error);
13
+ return normalizeBranchDatabaseRecord(result.data.data);
14
+ }
15
+ async function listEnvironmentVariables(client, options) {
16
+ const variables = [];
17
+ let cursor;
18
+ while (true) {
19
+ const result = await client.GET("/v1/environment-variables", {
20
+ params: { query: {
21
+ projectId: options.projectId,
22
+ class: options.className,
23
+ key: options.key,
24
+ branchId: options.branchId,
25
+ cursor
26
+ } },
27
+ signal: options.signal
28
+ });
29
+ if (result.error || !result.data) throw apiCallError("Failed to list environment variables", result.response, result.error);
30
+ variables.push(...result.data.data);
31
+ if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
32
+ cursor = result.data.pagination.nextCursor;
33
+ }
34
+ return variables.map((variable) => normalizeEnvironmentVariable(variable));
35
+ }
36
+ async function createEnvironmentVariable(client, options) {
37
+ const result = await client.POST("/v1/environment-variables", {
38
+ body: {
39
+ projectId: options.projectId,
40
+ class: options.className,
41
+ key: options.key,
42
+ value: options.value,
43
+ ...options.branchId ? { branchId: options.branchId } : {}
44
+ },
45
+ signal: options.signal
46
+ });
47
+ if (result.error || !result.data) throw apiCallError(`Failed to add ${options.key}`, result.response, result.error);
48
+ return normalizeEnvironmentVariable(result.data.data);
49
+ }
50
+ async function deleteBranchDatabase(client, options) {
51
+ const result = await client.DELETE("/v1/databases/{databaseId}", {
52
+ params: { path: { databaseId: options.databaseId } },
53
+ signal: options.signal
54
+ });
55
+ if (result.error) throw apiCallError("Failed to delete branch database", result.response, result.error);
56
+ }
57
+ async function updateEnvironmentVariable(client, options) {
58
+ const result = await client.PATCH("/v1/environment-variables/{envVarId}", {
59
+ params: { path: { envVarId: options.envVarId } },
60
+ body: { value: options.value },
61
+ signal: options.signal
62
+ });
63
+ if (result.error || !result.data) throw apiCallError("Failed to update environment variable", result.response, result.error);
64
+ return normalizeEnvironmentVariable(result.data.data);
65
+ }
66
+ async function deleteEnvironmentVariable(client, options) {
67
+ const result = await client.DELETE("/v1/environment-variables/{envVarId}", {
68
+ params: { path: { envVarId: options.envVarId } },
69
+ signal: options.signal
70
+ });
71
+ if (result.error) throw apiCallError("Failed to delete environment variable", result.response, result.error);
72
+ }
73
+ function normalizeEnvironmentVariable(variable) {
74
+ return {
75
+ id: variable.id,
76
+ key: variable.key,
77
+ branchId: variable.branchId,
78
+ className: variable.class,
79
+ isManagedBySystem: variable.isManagedBySystem
80
+ };
81
+ }
82
+ function normalizeBranchDatabaseRecord(database) {
83
+ const connection = database.connections?.[0];
84
+ const databaseUrl = connection?.endpoints?.pooled?.connectionString;
85
+ const directUrl = connection?.endpoints?.direct?.connectionString ?? null;
86
+ if (!databaseUrl) throw new Error("Created database did not return a pooled connection string.");
87
+ return {
88
+ id: database.id,
89
+ name: database.name,
90
+ branchId: database.branchId,
91
+ databaseUrl,
92
+ directUrl
93
+ };
94
+ }
95
+ function apiCallError(summary, response, error) {
96
+ if (response.status === 404) return /* @__PURE__ */ new Error("Resource Not Found");
97
+ const message = error.error?.message ?? `Management API returned HTTP ${response.status}.`;
98
+ const hint = error.error?.hint ? ` ${error.error.hint}` : "";
99
+ return /* @__PURE__ */ new Error(`${summary}: ${message}${hint}`);
100
+ }
101
+ //#endregion
102
+ export { createBranchDatabase, createEnvironmentVariable, deleteBranchDatabase, deleteEnvironmentVariable, listEnvironmentVariables, updateEnvironmentVariable };