@prisma/cli 3.0.0-beta.3 → 3.0.0-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,316 @@
1
+ import { access, readFile, readdir, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { spawn } from "node:child_process";
4
+ //#region src/lib/app/branch-database.ts
5
+ const SKIPPED_DIRECTORIES = new Set([
6
+ ".git",
7
+ ".next",
8
+ ".nuxt",
9
+ ".output",
10
+ ".prisma",
11
+ ".turbo",
12
+ ".vercel",
13
+ ".wrangler",
14
+ "build",
15
+ "coverage",
16
+ "dist",
17
+ "node_modules",
18
+ "out"
19
+ ]);
20
+ const DATABASE_URL_SCAN_EXTENSIONS = new Set([
21
+ ".cjs",
22
+ ".cts",
23
+ ".env",
24
+ ".js",
25
+ ".json",
26
+ ".jsx",
27
+ ".mjs",
28
+ ".mts",
29
+ ".prisma",
30
+ ".ts",
31
+ ".tsx"
32
+ ]);
33
+ const MAX_SCAN_DEPTH = 6;
34
+ const MAX_SCAN_FILES = 1e3;
35
+ const MAX_DATABASE_URL_REFERENCE_FILES = 10;
36
+ const MAX_TEXT_FILE_BYTES = 1024 * 1024;
37
+ async function inspectBranchDatabaseSignal(cwd, signal) {
38
+ const state = {
39
+ filesVisited: 0,
40
+ schemaCandidates: [],
41
+ prismaNextConfigCandidates: [],
42
+ databaseUrlReferences: []
43
+ };
44
+ await scanDirectory(cwd, cwd, 0, state, signal);
45
+ const prismaNextConfigs = await Promise.all(state.prismaNextConfigCandidates.map((configPath) => classifyPrismaNextConfig(configPath, signal)));
46
+ const supportedPrismaNextConfig = selectPrismaNextConfig(cwd, prismaNextConfigs, "supported");
47
+ const unsupportedPrismaNextConfig = selectPrismaNextConfig(cwd, prismaNextConfigs, "unsupported");
48
+ const selectedPrismaOrmSchema = await selectPrismaOrmSchema(cwd, state.schemaCandidates, signal);
49
+ const schema = supportedPrismaNextConfig ? {
50
+ kind: "prisma-next",
51
+ path: supportedPrismaNextConfig.path,
52
+ hasMigrations: false,
53
+ command: "prisma-next-db-init",
54
+ target: supportedPrismaNextConfig.target
55
+ } : selectedPrismaOrmSchema.schema;
56
+ return {
57
+ schema,
58
+ unsupportedSchema: schema ? null : unsupportedPrismaNextConfig ? {
59
+ kind: "prisma-next",
60
+ path: unsupportedPrismaNextConfig.path,
61
+ target: unsupportedPrismaNextConfig.target
62
+ } : selectedPrismaOrmSchema.unsupportedSchema,
63
+ databaseUrlReferences: state.databaseUrlReferences
64
+ };
65
+ }
66
+ function hasBranchDatabaseSignal(signal) {
67
+ if (signal.unsupportedSchema) return false;
68
+ return Boolean(signal.schema || signal.databaseUrlReferences.length > 0);
69
+ }
70
+ async function runBranchDatabaseSchemaSetup(options) {
71
+ const schemaPath = path.relative(options.context.runtime.cwd, options.schema.path) || defaultSchemaSourcePath(options.schema);
72
+ const commands = buildSchemaSetupCommands(options.schema, schemaPath, options.databaseUrl);
73
+ for (const command of commands) await runPrismaCommand({
74
+ context: options.context,
75
+ ...command,
76
+ env: {
77
+ DATABASE_URL: options.databaseUrl,
78
+ ...options.directUrl ? { DIRECT_URL: options.directUrl } : {}
79
+ }
80
+ });
81
+ return {
82
+ command: options.schema.command,
83
+ source: options.schema.kind,
84
+ schemaPath
85
+ };
86
+ }
87
+ async function scanDirectory(cwd, directory, depth, state, signal) {
88
+ signal.throwIfAborted();
89
+ if (depth > MAX_SCAN_DEPTH || state.filesVisited >= MAX_SCAN_FILES) return;
90
+ let entries;
91
+ try {
92
+ entries = await readdir(directory, { withFileTypes: true });
93
+ } catch (error) {
94
+ if (error.code === "ENOENT") return;
95
+ throw error;
96
+ }
97
+ entries.sort((left, right) => left.name.localeCompare(right.name));
98
+ for (const entry of entries) {
99
+ signal.throwIfAborted();
100
+ if (state.filesVisited >= MAX_SCAN_FILES) return;
101
+ const entryPath = path.join(directory, entry.name);
102
+ if (entry.isDirectory()) {
103
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) await scanDirectory(cwd, entryPath, depth + 1, state, signal);
104
+ continue;
105
+ }
106
+ if (!entry.isFile()) continue;
107
+ state.filesVisited += 1;
108
+ if (entry.name === "schema.prisma") state.schemaCandidates.push(entryPath);
109
+ if (isPrismaNextConfigFile(entry.name)) state.prismaNextConfigCandidates.push(entryPath);
110
+ if (state.databaseUrlReferences.length < MAX_DATABASE_URL_REFERENCE_FILES && shouldScanForDatabaseUrl(entry.name) && await fileContainsDatabaseUrl(entryPath, signal)) state.databaseUrlReferences.push(path.relative(cwd, entryPath) || entry.name);
111
+ }
112
+ }
113
+ async function selectPrismaOrmSchema(cwd, candidates, signal) {
114
+ const sorted = sortByPreferredRelativePath(cwd, candidates, "schema.prisma");
115
+ for (const schemaPath of sorted) {
116
+ const target = await classifyPrismaOrmSchemaTarget(schemaPath, signal);
117
+ if (target === "postgresql" || target === "unknown") {
118
+ const hasMigrations = await hasMigrationsDirectory(path.dirname(schemaPath), signal);
119
+ return {
120
+ schema: {
121
+ kind: "prisma-orm",
122
+ path: schemaPath,
123
+ hasMigrations,
124
+ command: hasMigrations ? "migrate-deploy" : "db-push",
125
+ target
126
+ },
127
+ unsupportedSchema: null
128
+ };
129
+ }
130
+ return {
131
+ schema: null,
132
+ unsupportedSchema: {
133
+ kind: "prisma-orm",
134
+ path: schemaPath,
135
+ target
136
+ }
137
+ };
138
+ }
139
+ return {
140
+ schema: null,
141
+ unsupportedSchema: null
142
+ };
143
+ }
144
+ function selectPrismaNextConfig(cwd, candidates, mode) {
145
+ const matches = candidates.filter((candidate) => {
146
+ const isSupported = candidate.target === "postgresql" || candidate.target === "unknown";
147
+ return mode === "supported" ? isSupported : !isSupported;
148
+ });
149
+ return sortByPreferredRelativePath(cwd, matches.map((candidate) => candidate.path), "prisma-next.config.ts").map((candidatePath) => matches.find((candidate) => candidate.path === candidatePath)).find((candidate) => Boolean(candidate)) ?? null;
150
+ }
151
+ function sortByPreferredRelativePath(cwd, candidates, preferredRootFile) {
152
+ return candidates.map((candidate) => ({
153
+ absolute: candidate,
154
+ relative: path.relative(cwd, candidate) || preferredRootFile
155
+ })).sort((left, right) => {
156
+ if (left.relative === preferredRootFile) return -1;
157
+ if (right.relative === preferredRootFile) return 1;
158
+ return left.relative.length - right.relative.length || left.relative.localeCompare(right.relative);
159
+ }).map((candidate) => candidate.absolute);
160
+ }
161
+ async function hasMigrationsDirectory(schemaDirectory, signal) {
162
+ signal.throwIfAborted();
163
+ const migrationsPath = path.join(schemaDirectory, "migrations");
164
+ try {
165
+ await access(migrationsPath);
166
+ return (await readdir(migrationsPath)).length > 0;
167
+ } catch (error) {
168
+ if (error.code === "ENOENT") return false;
169
+ throw error;
170
+ }
171
+ }
172
+ async function classifyPrismaNextConfig(configPath, signal) {
173
+ const content = await readTextFileIfSmall(configPath, signal);
174
+ if (!content) return {
175
+ path: configPath,
176
+ target: "unknown"
177
+ };
178
+ if (content.includes("@prisma-next/postgres/config")) return {
179
+ path: configPath,
180
+ target: "postgresql"
181
+ };
182
+ if (content.includes("@prisma-next/mongo/config")) return {
183
+ path: configPath,
184
+ target: "mongodb"
185
+ };
186
+ if (content.includes("@prisma-next/sqlite/config")) return {
187
+ path: configPath,
188
+ target: "sqlite"
189
+ };
190
+ return {
191
+ path: configPath,
192
+ target: "unknown"
193
+ };
194
+ }
195
+ async function classifyPrismaOrmSchemaTarget(schemaPath, signal) {
196
+ switch ((await readTextFileIfSmall(schemaPath, signal))?.match(/\bprovider\s*=\s*"([^"]+)"/)?.[1] ?? null) {
197
+ case "postgresql": return "postgresql";
198
+ case "mongodb": return "mongodb";
199
+ case "mysql": return "mysql";
200
+ case "sqlite": return "sqlite";
201
+ case "sqlserver": return "sqlserver";
202
+ case "cockroachdb": return "cockroachdb";
203
+ default: return "unknown";
204
+ }
205
+ }
206
+ function shouldScanForDatabaseUrl(fileName) {
207
+ if (fileName === ".env" || fileName.startsWith(".env.")) return true;
208
+ return DATABASE_URL_SCAN_EXTENSIONS.has(path.extname(fileName));
209
+ }
210
+ function isPrismaNextConfigFile(fileName) {
211
+ if (!fileName.startsWith("prisma-next.config.")) return false;
212
+ return [
213
+ ".cjs",
214
+ ".cts",
215
+ ".js",
216
+ ".mjs",
217
+ ".mts",
218
+ ".ts"
219
+ ].some((extension) => fileName.endsWith(extension));
220
+ }
221
+ async function fileContainsDatabaseUrl(filePath, signal) {
222
+ return (await readTextFileIfSmall(filePath, signal))?.includes("DATABASE_URL") ?? false;
223
+ }
224
+ async function readTextFileIfSmall(filePath, signal) {
225
+ signal.throwIfAborted();
226
+ if ((await stat(filePath)).size > MAX_TEXT_FILE_BYTES) return null;
227
+ return readFile(filePath, {
228
+ encoding: "utf8",
229
+ signal
230
+ });
231
+ }
232
+ function buildSchemaSetupCommands(schema, schemaPath, databaseUrl) {
233
+ if (schema.command === "migrate-deploy") return [{
234
+ args: [
235
+ "--no-install",
236
+ "prisma",
237
+ "migrate",
238
+ "deploy",
239
+ "--schema",
240
+ schemaPath
241
+ ],
242
+ displayCommand: "npx --no-install prisma migrate deploy"
243
+ }];
244
+ if (schema.command === "db-push") return [{
245
+ args: [
246
+ "--no-install",
247
+ "prisma",
248
+ "db",
249
+ "push",
250
+ "--schema",
251
+ schemaPath
252
+ ],
253
+ displayCommand: "npx --no-install prisma db push"
254
+ }];
255
+ return [{
256
+ args: [
257
+ "--no-install",
258
+ "prisma-next",
259
+ "contract",
260
+ "emit",
261
+ "--config",
262
+ schemaPath
263
+ ],
264
+ displayCommand: "npx --no-install prisma-next contract emit"
265
+ }, {
266
+ args: [
267
+ "--no-install",
268
+ "prisma-next",
269
+ "db",
270
+ "init",
271
+ "--config",
272
+ schemaPath,
273
+ "--db",
274
+ databaseUrl
275
+ ],
276
+ displayCommand: "npx --no-install prisma-next db init"
277
+ }];
278
+ }
279
+ function defaultSchemaSourcePath(schema) {
280
+ return schema.kind === "prisma-next" ? "prisma-next.config.ts" : "schema.prisma";
281
+ }
282
+ async function runPrismaCommand(options) {
283
+ const shouldPipeOutput = !options.context.flags.json && !options.context.flags.quiet;
284
+ const child = spawn("npx", options.args, {
285
+ cwd: options.context.runtime.cwd,
286
+ env: {
287
+ ...options.context.runtime.env,
288
+ ...options.env
289
+ },
290
+ signal: options.context.runtime.signal,
291
+ stdio: shouldPipeOutput ? [
292
+ "ignore",
293
+ "pipe",
294
+ "pipe"
295
+ ] : [
296
+ "ignore",
297
+ "ignore",
298
+ "ignore"
299
+ ]
300
+ });
301
+ if (shouldPipeOutput) {
302
+ child.stdout?.pipe(options.context.output.stderr, { end: false });
303
+ child.stderr?.pipe(options.context.output.stderr, { end: false });
304
+ }
305
+ const exit = await new Promise((resolve, reject) => {
306
+ child.once("error", reject);
307
+ child.once("close", (code, signal) => resolve({
308
+ code,
309
+ signal
310
+ }));
311
+ });
312
+ if (exit.signal) throw new Error(`${options.displayCommand} was terminated by ${exit.signal}.`);
313
+ if (exit.code !== 0) throw new Error(`${options.displayCommand} exited with code ${exit.code ?? 1}.`);
314
+ }
315
+ //#endregion
316
+ export { hasBranchDatabaseSignal, inspectBranchDatabaseSignal, runBranchDatabaseSchemaSetup };
@@ -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.", [`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);
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 };
@@ -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 };
@@ -1,5 +1,6 @@
1
1
  import { envVarNames } from "./env-vars.js";
2
2
  import { PreviewBuildStrategy } from "./preview-build.js";
3
+ import { createBranchDatabase, createEnvironmentVariable, deleteBranchDatabase, deleteEnvironmentVariable, listEnvironmentVariables, updateEnvironmentVariable } from "./preview-branch-database.js";
3
4
  import path from "node:path";
4
5
  import { ApiError, CancelledError, ComputeClient, streamLogs } from "@prisma/compute-sdk";
5
6
  //#region src/lib/app/preview-provider.ts
@@ -36,6 +37,36 @@ function createPreviewAppProvider(client, options) {
36
37
  signal: options?.signal
37
38
  });
38
39
  },
40
+ async resolveBranch(projectId, options) {
41
+ const branch = await resolveOrCreateBranch(client, {
42
+ projectId,
43
+ gitName: options.branchName,
44
+ signal: options.signal
45
+ });
46
+ return {
47
+ id: branch.id,
48
+ name: branch.gitName,
49
+ role: branch.role
50
+ };
51
+ },
52
+ async createBranchDatabase(options) {
53
+ return createBranchDatabase(client, options);
54
+ },
55
+ async deleteBranchDatabase(options) {
56
+ return deleteBranchDatabase(client, options);
57
+ },
58
+ async listEnvironmentVariables(options) {
59
+ return listEnvironmentVariables(client, options);
60
+ },
61
+ async createEnvironmentVariable(options) {
62
+ return createEnvironmentVariable(client, options);
63
+ },
64
+ async updateEnvironmentVariable(options) {
65
+ return updateEnvironmentVariable(client, options);
66
+ },
67
+ async deleteEnvironmentVariable(options) {
68
+ return deleteEnvironmentVariable(client, options);
69
+ },
39
70
  async removeApp(appId, options) {
40
71
  const appResult = await sdk.showService({
41
72
  serviceId: appId,
@@ -316,17 +347,19 @@ async function listBranches(client, options) {
316
347
  signal: options.signal
317
348
  });
318
349
  if (result.error || !result.data) throw apiCallError("Failed to list branches", result.response, result.error);
319
- return result.data.data;
350
+ return result.data.data.map((branch) => ({
351
+ id: branch.id,
352
+ gitName: branch.gitName,
353
+ isDefault: branch.isDefault,
354
+ role: branch.role
355
+ }));
320
356
  }
321
357
  async function resolveOrCreateBranch(client, options) {
322
358
  const existing = (await listBranches(client, options))[0];
323
359
  if (existing) return existing;
324
360
  const result = await client.POST("/v1/projects/{projectId}/branches", {
325
361
  params: { path: { projectId: options.projectId } },
326
- body: {
327
- gitName: options.gitName,
328
- isDefault: options.gitName === "main"
329
- },
362
+ body: { gitName: options.gitName },
330
363
  signal: options.signal
331
364
  });
332
365
  if (result.error || !result.data) {
@@ -336,7 +369,13 @@ async function resolveOrCreateBranch(client, options) {
336
369
  }
337
370
  throw apiCallError(`Failed to create branch "${options.gitName}"`, result.response, result.error);
338
371
  }
339
- return result.data.data;
372
+ const branch = result.data.data;
373
+ return {
374
+ id: branch.id,
375
+ gitName: branch.gitName,
376
+ isDefault: branch.isDefault,
377
+ role: branch.role
378
+ };
340
379
  }
341
380
  async function listComputeServices(client, options) {
342
381
  const services = [];