@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
@@ -24,20 +24,21 @@ function workspaceIdFromClaims(claims) {
24
24
  const id = sub.slice(10).trim();
25
25
  return id.length > 0 ? id : null;
26
26
  }
27
- async function performLogin(env) {
27
+ async function performLogin(env, signal) {
28
28
  await login({
29
- tokenStorage: new FileTokenStorage(env),
30
- env
29
+ tokenStorage: new FileTokenStorage(env, signal),
30
+ env,
31
+ signal
31
32
  });
32
33
  }
33
- async function readAuthState(env) {
34
+ async function readAuthState(env, signal) {
34
35
  const rawServiceToken = env[SERVICE_TOKEN_ENV_VAR];
35
36
  if (rawServiceToken !== void 0) {
36
37
  const serviceToken = rawServiceToken.trim();
37
38
  if (serviceToken.length === 0) throw new Error(`${SERVICE_TOKEN_ENV_VAR} is set but empty. Provide a valid token or unset the variable.`);
38
- return readServiceTokenAuthState(serviceToken, env);
39
+ return readServiceTokenAuthState(serviceToken, env, signal);
39
40
  }
40
- const tokens = await new FileTokenStorage(env).getTokens();
41
+ const tokens = await new FileTokenStorage(env, signal).getTokens();
41
42
  if (!tokens) return {
42
43
  authenticated: false,
43
44
  provider: null,
@@ -45,20 +46,21 @@ async function readAuthState(env) {
45
46
  workspace: null,
46
47
  credential: null
47
48
  };
48
- const client = await requireComputeAuth(env);
49
- const currentPrincipal = await readCurrentPrincipalAuthState(client);
49
+ const client = await requireComputeAuth(env, signal);
50
+ const currentPrincipal = await readCurrentPrincipalAuthState(client, signal);
50
51
  if (currentPrincipal) return currentPrincipal;
51
52
  const claims = decodeJwtPayload(tokens.accessToken);
52
53
  return buildAuthState({
53
54
  workspaceIdFromCredential: tokens.workspaceId,
54
55
  claims,
55
56
  env,
56
- client
57
+ client,
58
+ signal
57
59
  });
58
60
  }
59
- async function readServiceTokenAuthState(token, env) {
60
- const client = await requireComputeAuth(env);
61
- const currentPrincipal = await readCurrentPrincipalAuthState(client);
61
+ async function readServiceTokenAuthState(token, env, signal) {
62
+ const client = await requireComputeAuth(env, signal);
63
+ const currentPrincipal = await readCurrentPrincipalAuthState(client, signal);
62
64
  if (currentPrincipal) return currentPrincipal;
63
65
  const claims = decodeJwtPayload(token);
64
66
  const workspaceId = workspaceIdFromClaims(claims);
@@ -73,15 +75,19 @@ async function readServiceTokenAuthState(token, env) {
73
75
  workspaceIdFromCredential: workspaceId,
74
76
  claims,
75
77
  env,
76
- client
78
+ client,
79
+ signal
77
80
  });
78
81
  }
79
- async function buildAuthState({ workspaceIdFromCredential, claims, env, client }) {
82
+ async function buildAuthState({ workspaceIdFromCredential, claims, env, client, signal }) {
80
83
  let workspaceId = workspaceIdFromCredential;
81
84
  let workspaceName = workspaceIdFromCredential;
82
- client ??= await requireComputeAuth(env);
85
+ client ??= await requireComputeAuth(env, signal);
83
86
  if (client) try {
84
- const { data, response } = await client.GET("/v1/workspaces/{id}", { params: { path: { id: workspaceIdFromCredential } } });
87
+ const { data, response } = await client.GET("/v1/workspaces/{id}", {
88
+ params: { path: { id: workspaceIdFromCredential } },
89
+ signal
90
+ });
85
91
  if (response?.status === 401) return {
86
92
  authenticated: false,
87
93
  provider: null,
@@ -94,7 +100,9 @@ async function buildAuthState({ workspaceIdFromCredential, claims, env, client }
94
100
  workspaceName = data.data.id;
95
101
  }
96
102
  if (data?.data?.name) workspaceName = data.data.name;
97
- } catch {}
103
+ } catch {
104
+ signal?.throwIfAborted();
105
+ }
98
106
  const email = emailFromClaims(claims);
99
107
  return {
100
108
  authenticated: true,
@@ -107,10 +115,10 @@ async function buildAuthState({ workspaceIdFromCredential, claims, env, client }
107
115
  credential: null
108
116
  };
109
117
  }
110
- async function readCurrentPrincipalAuthState(client) {
118
+ async function readCurrentPrincipalAuthState(client, signal) {
111
119
  if (!client) return null;
112
120
  try {
113
- const { data, response } = await client.GET("/v1/me");
121
+ const { data, response } = await client.GET("/v1/me", { signal });
114
122
  if (response?.status === 401) return {
115
123
  authenticated: false,
116
124
  provider: null,
@@ -133,11 +141,12 @@ async function readCurrentPrincipalAuthState(client) {
133
141
  credential: principal.credential
134
142
  };
135
143
  } catch {
144
+ signal?.throwIfAborted();
136
145
  return null;
137
146
  }
138
147
  }
139
- async function performLogout(env) {
140
- await new FileTokenStorage(env).clearTokens();
148
+ async function performLogout(env, signal) {
149
+ await new FileTokenStorage(env, signal).clearTokens();
141
150
  }
142
151
  //#endregion
143
152
  export { performLogin, performLogout, readAuthState };
@@ -11,7 +11,8 @@ import { createManagementApiClient, createManagementApiSdk } from "@prisma/manag
11
11
  *
12
12
  * Returns null if not authenticated.
13
13
  */
14
- async function requireComputeAuth(env = process.env) {
14
+ async function requireComputeAuth(env = process.env, signal) {
15
+ signal?.throwIfAborted();
15
16
  const rawToken = env[SERVICE_TOKEN_ENV_VAR];
16
17
  if (rawToken !== void 0) {
17
18
  const token = rawToken.trim();
@@ -21,7 +22,7 @@ async function requireComputeAuth(env = process.env) {
21
22
  token
22
23
  });
23
24
  }
24
- const tokenStorage = new FileTokenStorage(env);
25
+ const tokenStorage = new FileTokenStorage(env, signal);
25
26
  if (!await tokenStorage.getTokens()) return null;
26
27
  return createManagementApiSdk({
27
28
  clientId: CLIENT_ID,
@@ -4,6 +4,7 @@ import open from "open";
4
4
  import { AuthError, createManagementApiSdk } from "@prisma/management-api-sdk";
5
5
  import events from "node:events";
6
6
  import http from "node:http";
7
+ import readline from "node:readline/promises";
7
8
  //#region src/lib/auth/login.ts
8
9
  var AuthError$1 = class extends Error {
9
10
  constructor(message) {
@@ -14,11 +15,15 @@ var AuthError$1 = class extends Error {
14
15
  async function login(options = {}) {
15
16
  const hostname = options.hostname ?? "localhost";
16
17
  const port = options.port ?? 0;
18
+ const input = options.input ?? process.stdin;
19
+ const output = options.output ?? process.stderr;
20
+ const interactive = input.isTTY === true;
17
21
  const server = http.createServer();
18
22
  server.listen({
19
23
  host: hostname,
20
24
  port
21
25
  });
26
+ const pasteAbort = new AbortController();
22
27
  try {
23
28
  const state = new LoginState({
24
29
  hostname,
@@ -28,9 +33,30 @@ async function login(options = {}) {
28
33
  apiBaseUrl: options.apiBaseUrl,
29
34
  authBaseUrl: options.authBaseUrl,
30
35
  openUrl: options.openUrl,
31
- env: options.env
36
+ env: options.env,
37
+ signal: options.signal,
38
+ output
32
39
  });
33
- const authResult = new Promise((resolve, reject) => {
40
+ let completed = false;
41
+ let completion;
42
+ const completeOnce = (url) => {
43
+ if (!completion) completion = state.handleCallback(url).then(() => {
44
+ completed = true;
45
+ }, (error) => {
46
+ completion = void 0;
47
+ throw error;
48
+ });
49
+ return completion;
50
+ };
51
+ const httpResult = new Promise((resolve, reject) => {
52
+ const onAbort = () => {
53
+ reject(options.signal?.reason);
54
+ };
55
+ options.signal?.addEventListener("abort", onAbort, { once: true });
56
+ const settle = (callback) => {
57
+ options.signal?.removeEventListener("abort", onAbort);
58
+ callback();
59
+ };
34
60
  server.on("request", async (req, res) => {
35
61
  const url = new URL(`http://${state.host}${req.url}`);
36
62
  if (url.pathname !== "/auth/callback") {
@@ -38,36 +64,94 @@ async function login(options = {}) {
38
64
  res.end("Not found");
39
65
  return;
40
66
  }
67
+ if (completed) {
68
+ const workspaceName = await state.resolveWorkspaceName();
69
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
70
+ res.end(renderSuccessPage(workspaceName));
71
+ return;
72
+ }
41
73
  try {
42
- await state.handleCallback(url);
74
+ await completeOnce(url);
75
+ const workspaceName = await state.resolveWorkspaceName();
76
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
77
+ res.end(renderSuccessPage(workspaceName));
78
+ settle(resolve);
43
79
  } catch (error) {
44
80
  res.statusCode = 400;
45
81
  const message = error instanceof Error ? error.message : String(error);
46
82
  res.end(message);
47
- reject(error);
83
+ settle(() => reject(error));
48
84
  return;
49
85
  }
50
- const workspaceName = await state.resolveWorkspaceName();
51
- res.setHeader("Content-Type", "text/html; charset=utf-8");
52
- res.end(renderSuccessPage(workspaceName));
53
- resolve();
54
86
  });
55
87
  });
56
- await state.openLoginPage();
57
- await authResult;
88
+ options.signal?.throwIfAborted();
89
+ const callbackResult = interactive ? Promise.race([httpResult, consumePastedCallback({
90
+ input,
91
+ output,
92
+ signal: pasteAbort.signal,
93
+ complete: completeOnce
94
+ })]) : httpResult;
95
+ await Promise.all([state.openLoginPage(interactive), callbackResult]);
58
96
  } finally {
97
+ pasteAbort.abort();
59
98
  if (server.listening) await new Promise((resolve) => server.close(() => resolve()));
60
99
  }
61
100
  }
101
+ async function consumePastedCallback(options) {
102
+ if (!options.input.isTTY) return;
103
+ const rl = readline.createInterface({
104
+ input: options.input,
105
+ output: options.output
106
+ });
107
+ try {
108
+ for (;;) {
109
+ const url = await readPastedCallbackUrl(rl, options);
110
+ if (url === null) return;
111
+ if (url === void 0) continue;
112
+ if (await tryCompletePastedCallback(url, options)) return;
113
+ }
114
+ } finally {
115
+ rl.close();
116
+ }
117
+ }
118
+ async function readPastedCallbackUrl(rl, options) {
119
+ let answer;
120
+ try {
121
+ answer = await rl.question("Paste the callback URL here: ", { signal: options.signal });
122
+ } catch (error) {
123
+ if (error?.name === "AbortError") return null;
124
+ throw error;
125
+ }
126
+ const trimmed = answer.trim().replace(/^["']|["']$/g, "");
127
+ try {
128
+ if (!trimmed) throw new Error("empty input");
129
+ return new URL(trimmed);
130
+ } catch {
131
+ options.output.write("That didn't look like a URL. Paste the full localhost callback URL and try again.\n");
132
+ return;
133
+ }
134
+ }
135
+ async function tryCompletePastedCallback(url, options) {
136
+ try {
137
+ await options.complete(url);
138
+ return true;
139
+ } catch (error) {
140
+ const message = error instanceof Error ? error.message : String(error);
141
+ options.output.write(`Sign-in didn't complete (${message}). Paste the callback URL to try again.\n`);
142
+ return false;
143
+ }
144
+ }
62
145
  var LoginState = class {
63
146
  latestVerifier;
64
147
  latestState;
65
148
  sdk;
66
149
  openUrl;
67
150
  tokenStorage;
151
+ output;
68
152
  constructor(options) {
69
153
  this.options = options;
70
- this.tokenStorage = options.tokenStorage ?? new FileTokenStorage(options.env);
154
+ this.tokenStorage = options.tokenStorage ?? new FileTokenStorage(options.env, options.signal);
71
155
  this.sdk = createManagementApiSdk({
72
156
  clientId: options.clientId ?? "cmm3lndn701oo0uefvxzo0ivw",
73
157
  redirectUri: `http://${options.hostname}:${options.port}/auth/callback`,
@@ -76,8 +160,10 @@ var LoginState = class {
76
160
  authBaseUrl: options.authBaseUrl
77
161
  });
78
162
  this.openUrl = options.openUrl ?? open;
163
+ this.output = options.output;
79
164
  }
80
- async openLoginPage() {
165
+ async openLoginPage(interactive) {
166
+ this.options.signal?.throwIfAborted();
81
167
  const { url, state, verifier } = await this.sdk.getLoginUrl({
82
168
  scope: "workspace:admin offline_access",
83
169
  additionalParams: {
@@ -88,7 +174,19 @@ var LoginState = class {
88
174
  });
89
175
  this.latestState = state;
90
176
  this.latestVerifier = verifier;
91
- await this.openUrl(url);
177
+ this.options.signal?.throwIfAborted();
178
+ if (interactive) this.printLoginInstructions(url);
179
+ try {
180
+ await this.openUrl(url);
181
+ } catch (error) {
182
+ if (!interactive) throw error;
183
+ }
184
+ this.options.signal?.throwIfAborted();
185
+ }
186
+ printLoginInstructions(url) {
187
+ const output = this.output;
188
+ if (!output) return;
189
+ output.write(`\nOpen this URL to sign in: ${url}\n\nIf the browser opens on another machine, finish sign-in there. When it\nredirects to localhost, copy the full localhost URL from the address bar\nand paste it here.\n\n`);
92
190
  }
93
191
  async handleCallback(url) {
94
192
  if (url.pathname !== "/auth/callback") throw new AuthError$1("Not a callback URL");
@@ -115,10 +213,14 @@ var LoginState = class {
115
213
  try {
116
214
  const tokens = await this.tokenStorage.getTokens();
117
215
  if (!tokens?.workspaceId) return null;
118
- const { data } = await this.sdk.client.GET("/v1/workspaces/{id}", { params: { path: { id: tokens.workspaceId } } });
216
+ const { data } = await this.sdk.client.GET("/v1/workspaces/{id}", {
217
+ params: { path: { id: tokens.workspaceId } },
218
+ signal: this.options.signal
219
+ });
119
220
  const name = data?.data?.name;
120
221
  return typeof name === "string" && name.trim().length > 0 ? name.trim() : null;
121
222
  } catch {
223
+ this.options.signal?.throwIfAborted();
122
224
  return null;
123
225
  }
124
226
  }
@@ -0,0 +1,167 @@
1
+ import { CliError } from "../../shell/errors.js";
2
+ //#region src/lib/database/provider.ts
3
+ function createManagementDatabaseProvider(client) {
4
+ return {
5
+ async listDatabases(options) {
6
+ const databases = [];
7
+ let cursor;
8
+ while (true) {
9
+ const result = await client.GET("/v1/databases", {
10
+ params: { query: {
11
+ projectId: options.projectId,
12
+ branchGitName: options.branchName,
13
+ cursor
14
+ } },
15
+ signal: options.signal
16
+ });
17
+ if (result.error || !result.data) throw databaseApiError("Failed to list databases", result.response, result.error);
18
+ databases.push(...result.data.data);
19
+ if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) break;
20
+ cursor = result.data.pagination.nextCursor;
21
+ }
22
+ return databases.map((database) => normalizeDatabase(database, options.projectId));
23
+ },
24
+ async showDatabase(databaseId, options) {
25
+ const result = await client.GET("/v1/databases/{databaseId}", {
26
+ params: { path: { databaseId } },
27
+ signal: options?.signal
28
+ });
29
+ if (result.response?.status === 404) return null;
30
+ if (result.error || !result.data) throw databaseApiError("Failed to show database", result.response, result.error);
31
+ const database = result.data.data;
32
+ return normalizeDatabase(database, requireDatabaseProjectId(database, options?.projectId));
33
+ },
34
+ async createDatabase(options) {
35
+ const result = await client.POST("/v1/databases", {
36
+ body: {
37
+ projectId: options.projectId,
38
+ name: options.name,
39
+ source: { type: "empty" },
40
+ ...options.branchName ? { branchGitName: options.branchName } : {},
41
+ ...options.region ? { region: options.region } : {}
42
+ },
43
+ signal: options.signal
44
+ });
45
+ if (result.error || !result.data) throw databaseApiError("Failed to create database", result.response, result.error);
46
+ return normalizeCreatedDatabase(result.data.data, options.projectId);
47
+ },
48
+ async removeDatabase(databaseId, options) {
49
+ const result = await client.DELETE("/v1/databases/{databaseId}", {
50
+ params: { path: { databaseId } },
51
+ signal: options?.signal
52
+ });
53
+ if (result.error) throw databaseApiError("Failed to remove database", result.response, result.error);
54
+ },
55
+ async listConnections(databaseId, options) {
56
+ const result = await client.GET("/v1/databases/{databaseId}/connections", {
57
+ params: { path: { databaseId } },
58
+ signal: options?.signal
59
+ });
60
+ if (result.error || !result.data) throw databaseApiError("Failed to list database connections", result.response, result.error);
61
+ return result.data.data.map((connection) => normalizeConnection(connection, databaseId));
62
+ },
63
+ async createConnection(options) {
64
+ const result = await client.POST("/v1/databases/{databaseId}/connections", {
65
+ params: { path: { databaseId: options.databaseId } },
66
+ body: { name: options.name },
67
+ signal: options.signal
68
+ });
69
+ if (result.error || !result.data) throw databaseApiError("Failed to create database connection", result.response, result.error);
70
+ return normalizeCreatedConnection(result.data.data, options.databaseId);
71
+ },
72
+ async removeConnection(connectionId, options) {
73
+ const result = await client.DELETE("/v1/connections/{id}", {
74
+ params: { path: { id: connectionId } },
75
+ signal: options?.signal
76
+ });
77
+ if (result.error) throw databaseApiError("Failed to remove database connection", result.response, result.error);
78
+ }
79
+ };
80
+ }
81
+ function normalizeDatabase(database, fallbackProjectId) {
82
+ return {
83
+ id: database.id,
84
+ name: database.name,
85
+ projectId: database.projectId ?? fallbackProjectId,
86
+ branchId: database.branchId ?? database.branch?.id ?? null,
87
+ branchName: database.branchGitName ?? database.branchName ?? database.branch?.gitName ?? database.branch?.name ?? null,
88
+ region: normalizeRegion(database),
89
+ status: database.status ?? null,
90
+ isDefault: database.isDefault ?? null,
91
+ createdAt: database.createdAt ?? null
92
+ };
93
+ }
94
+ function normalizeConnection(connection, fallbackDatabaseId) {
95
+ return {
96
+ id: connection.id,
97
+ name: connection.name ?? connection.id,
98
+ databaseId: connection.databaseId ?? fallbackDatabaseId,
99
+ createdAt: connection.createdAt ?? null
100
+ };
101
+ }
102
+ function normalizeCreatedDatabase(database, fallbackProjectId) {
103
+ const rawConnection = database.connections?.[0];
104
+ if (!rawConnection) throw new CliError({
105
+ code: "DATABASE_CONNECTION_MISSING",
106
+ domain: "database",
107
+ summary: "Created database did not return a connection string",
108
+ why: "The Management API created the database but did not include the one-time connection payload.",
109
+ fix: "Create a connection explicitly with prisma-cli database connection create <database>.",
110
+ exitCode: 1,
111
+ nextSteps: [`prisma-cli database connection create ${database.id}`]
112
+ });
113
+ return {
114
+ database: normalizeDatabase(database, fallbackProjectId),
115
+ ...normalizeCreatedConnection(rawConnection, database.id)
116
+ };
117
+ }
118
+ function normalizeCreatedConnection(connection, fallbackDatabaseId) {
119
+ const connectionString = extractConnectionString(connection);
120
+ if (!connectionString) throw new CliError({
121
+ code: "DATABASE_CONNECTION_STRING_MISSING",
122
+ domain: "database",
123
+ summary: "Created connection did not return a connection string",
124
+ why: "Database connection strings are one-time-view secrets, but the Management API did not include one in this create response.",
125
+ fix: "Create another database connection and store the returned URL immediately.",
126
+ exitCode: 1,
127
+ nextSteps: [`prisma-cli database connection create ${fallbackDatabaseId}`]
128
+ });
129
+ return {
130
+ connection: normalizeConnection(connection, fallbackDatabaseId),
131
+ connectionString
132
+ };
133
+ }
134
+ function normalizeRegion(database) {
135
+ if (typeof database.region === "string") return database.region;
136
+ return database.region?.id ?? database.regionId ?? null;
137
+ }
138
+ function requireDatabaseProjectId(database, fallbackProjectId) {
139
+ const projectId = database.projectId ?? fallbackProjectId;
140
+ if (projectId) return projectId;
141
+ throw new CliError({
142
+ code: "DATABASE_API_ERROR",
143
+ domain: "database",
144
+ summary: "Database response did not include a project id",
145
+ why: "The Management API returned database metadata without project context.",
146
+ fix: "Re-run with --trace for the underlying API response details.",
147
+ exitCode: 1,
148
+ nextSteps: []
149
+ });
150
+ }
151
+ function extractConnectionString(connection) {
152
+ return connection.endpoints?.pooled?.connectionString ?? connection.connectionString ?? connection.endpoints?.direct?.connectionString ?? connection.endpoints?.accelerate?.connectionString ?? null;
153
+ }
154
+ function databaseApiError(summary, response, error) {
155
+ const status = response?.status ?? 0;
156
+ return new CliError({
157
+ code: error?.error?.code ?? "DATABASE_API_ERROR",
158
+ domain: "database",
159
+ summary,
160
+ why: error?.error?.message ?? `The Management API returned status ${status || "unknown"}.`,
161
+ fix: error?.error?.hint ?? "Re-run with --trace for the underlying API response details.",
162
+ exitCode: 1,
163
+ nextSteps: []
164
+ });
165
+ }
166
+ //#endregion
167
+ export { createManagementDatabaseProvider, normalizeConnection, normalizeDatabase };
@@ -0,0 +1,15 @@
1
+ import { resolveLocalStateFilePath } from "../adapters/local-state.js";
2
+ import { resolveStateDir } from "../shell/runtime.js";
3
+ import { readLocalGitState } from "./git/local-status.js";
4
+ //#region src/lib/diagnostics.ts
5
+ async function collectCommandDiagnostics(context, options = {}) {
6
+ const stateDir = await resolveStateDir(context.runtime);
7
+ return {
8
+ cwd: context.runtime.cwd,
9
+ stateFilePath: resolveLocalStateFilePath(stateDir),
10
+ git: await readLocalGitState(context.runtime.cwd, context.runtime.signal),
11
+ durationMs: options.durationMs
12
+ };
13
+ }
14
+ //#endregion
15
+ export { collectCommandDiagnostics };
@@ -0,0 +1,24 @@
1
+ import path from "node:path";
2
+ //#region src/lib/fs/home-path.ts
3
+ /**
4
+ * Shortens a path under the user's home directory to `~/...` for display,
5
+ * posix-style on every platform. Falls back to the Windows home variables
6
+ * when `HOME` is unset (native cmd/PowerShell sessions).
7
+ */
8
+ function shortenHomePath(value, env) {
9
+ const resolved = path.resolve(value);
10
+ const home = resolveHomeDirectory(env);
11
+ if (home && (resolved === home || resolved.startsWith(`${home}${path.sep}`))) {
12
+ const relative = path.relative(home, resolved).split(path.sep).join("/");
13
+ return relative ? `~/${relative}` : "~";
14
+ }
15
+ return resolved;
16
+ }
17
+ function resolveHomeDirectory(env) {
18
+ if (env.HOME) return path.resolve(env.HOME);
19
+ if (env.USERPROFILE) return path.resolve(env.USERPROFILE);
20
+ if (env.HOMEDRIVE && env.HOMEPATH) return path.resolve(`${env.HOMEDRIVE}${env.HOMEPATH}`);
21
+ return null;
22
+ }
23
+ //#endregion
24
+ export { shortenHomePath };
@@ -0,0 +1,53 @@
1
+ import { access, readFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ //#region src/lib/git/local-branch.ts
4
+ /**
5
+ * Resolves the checked-out branch the way git does: the nearest `.git`
6
+ * (directory or worktree file) from `cwd` upward owns the answer, so
7
+ * monorepo commands run from inside a package see the repository branch.
8
+ * Returns null for detached HEAD or when no repository contains `cwd`.
9
+ */
10
+ async function readLocalGitBranch(cwd, signal) {
11
+ for (let directory = path.resolve(cwd);;) {
12
+ const headPath = await resolveGitHeadPath(path.join(directory, ".git"), signal);
13
+ if (headPath) return readBranchFromHead(headPath, signal);
14
+ const parent = path.dirname(directory);
15
+ if (parent === directory) return null;
16
+ directory = parent;
17
+ }
18
+ }
19
+ async function readBranchFromHead(headPath, signal) {
20
+ try {
21
+ const head = (await readFile(headPath, {
22
+ encoding: "utf8",
23
+ signal
24
+ })).trim();
25
+ if (head.startsWith("ref: refs/heads/")) return head.slice(16);
26
+ } catch (error) {
27
+ if (signal.aborted) throw error;
28
+ }
29
+ return null;
30
+ }
31
+ async function resolveGitHeadPath(gitPath, signal) {
32
+ signal.throwIfAborted();
33
+ try {
34
+ const raw = await readFile(gitPath, {
35
+ encoding: "utf8",
36
+ signal
37
+ });
38
+ if (raw.startsWith("gitdir:")) return path.join(path.resolve(path.dirname(gitPath), raw.slice(7).trim()), "HEAD");
39
+ } catch (error) {
40
+ if (signal.aborted) throw error;
41
+ }
42
+ signal.throwIfAborted();
43
+ try {
44
+ await access(path.join(gitPath, "HEAD"));
45
+ signal.throwIfAborted();
46
+ return path.join(gitPath, "HEAD");
47
+ } catch (error) {
48
+ if (signal.aborted) throw error;
49
+ return null;
50
+ }
51
+ }
52
+ //#endregion
53
+ export { readLocalGitBranch };
@@ -0,0 +1,57 @@
1
+ import { execFile } from "node:child_process";
2
+ //#region src/lib/git/local-status.ts
3
+ async function readLocalGitState(cwd, signal) {
4
+ signal.throwIfAborted();
5
+ if ((await runGit(cwd, ["rev-parse", "--is-inside-work-tree"], signal))?.trim() !== "true") return null;
6
+ const [ref, sha, status] = await Promise.all([
7
+ runGit(cwd, [
8
+ "symbolic-ref",
9
+ "--quiet",
10
+ "--short",
11
+ "HEAD"
12
+ ], signal),
13
+ runGit(cwd, [
14
+ "rev-parse",
15
+ "--short",
16
+ "HEAD"
17
+ ], signal),
18
+ runGit(cwd, ["status", "--porcelain"], signal)
19
+ ]);
20
+ return {
21
+ ref: cleanGitValue(ref),
22
+ sha: cleanGitValue(sha),
23
+ dirty: status === null ? null : status.trim().length > 0
24
+ };
25
+ }
26
+ function runGit(cwd, args, signal) {
27
+ return new Promise((resolve, reject) => {
28
+ signal.throwIfAborted();
29
+ execFile("git", args, {
30
+ cwd,
31
+ signal,
32
+ timeout: 2e3
33
+ }, (error, stdout) => {
34
+ if (signal.aborted) {
35
+ reject(error);
36
+ return;
37
+ }
38
+ if (error) {
39
+ resolve(null);
40
+ return;
41
+ }
42
+ resolve(stdout);
43
+ }).on("error", (error) => {
44
+ if (signal.aborted) {
45
+ reject(error);
46
+ return;
47
+ }
48
+ resolve(null);
49
+ });
50
+ });
51
+ }
52
+ function cleanGitValue(value) {
53
+ const cleaned = value?.trim();
54
+ return cleaned ? cleaned : null;
55
+ }
56
+ //#endregion
57
+ export { readLocalGitState };
@@ -36,7 +36,7 @@ async function promptForProjectSetupChoice(options) {
36
36
  targetName: choice.project.name,
37
37
  targetNameSource: "prompt"
38
38
  };
39
- const suggestedName = await inferTargetName(options.context.runtime.cwd);
39
+ const suggestedName = await inferTargetName(options.context.runtime.cwd, options.context.runtime.signal);
40
40
  const rawName = await textPrompt({
41
41
  input: options.context.runtime.stdin,
42
42
  output: options.context.runtime.stderr,