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

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.
@@ -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,87 @@ 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
+ let answer;
110
+ try {
111
+ answer = await rl.question("Paste the callback URL here: ", { signal: options.signal });
112
+ } catch (error) {
113
+ if (error?.name === "AbortError") return;
114
+ throw error;
115
+ }
116
+ const trimmed = answer.trim().replace(/^["']|["']$/g, "");
117
+ let url;
118
+ try {
119
+ if (!trimmed) throw new Error("empty input");
120
+ url = new URL(trimmed);
121
+ } catch {
122
+ options.output.write("That didn't look like a URL. Paste the full localhost callback URL and try again.\n");
123
+ continue;
124
+ }
125
+ try {
126
+ await options.complete(url);
127
+ return;
128
+ } catch (error) {
129
+ const message = error instanceof Error ? error.message : String(error);
130
+ options.output.write(`Sign-in didn't complete (${message}). Paste the callback URL to try again.\n`);
131
+ continue;
132
+ }
133
+ }
134
+ } finally {
135
+ rl.close();
136
+ }
137
+ }
62
138
  var LoginState = class {
63
139
  latestVerifier;
64
140
  latestState;
65
141
  sdk;
66
142
  openUrl;
67
143
  tokenStorage;
144
+ output;
68
145
  constructor(options) {
69
146
  this.options = options;
70
- this.tokenStorage = options.tokenStorage ?? new FileTokenStorage(options.env);
147
+ this.tokenStorage = options.tokenStorage ?? new FileTokenStorage(options.env, options.signal);
71
148
  this.sdk = createManagementApiSdk({
72
149
  clientId: options.clientId ?? "cmm3lndn701oo0uefvxzo0ivw",
73
150
  redirectUri: `http://${options.hostname}:${options.port}/auth/callback`,
@@ -76,8 +153,10 @@ var LoginState = class {
76
153
  authBaseUrl: options.authBaseUrl
77
154
  });
78
155
  this.openUrl = options.openUrl ?? open;
156
+ this.output = options.output;
79
157
  }
80
- async openLoginPage() {
158
+ async openLoginPage(interactive) {
159
+ this.options.signal?.throwIfAborted();
81
160
  const { url, state, verifier } = await this.sdk.getLoginUrl({
82
161
  scope: "workspace:admin offline_access",
83
162
  additionalParams: {
@@ -88,7 +167,19 @@ var LoginState = class {
88
167
  });
89
168
  this.latestState = state;
90
169
  this.latestVerifier = verifier;
91
- await this.openUrl(url);
170
+ this.options.signal?.throwIfAborted();
171
+ if (interactive) this.printLoginInstructions(url);
172
+ try {
173
+ await this.openUrl(url);
174
+ } catch (error) {
175
+ if (!interactive) throw error;
176
+ }
177
+ this.options.signal?.throwIfAborted();
178
+ }
179
+ printLoginInstructions(url) {
180
+ const output = this.output;
181
+ if (!output) return;
182
+ 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
183
  }
93
184
  async handleCallback(url) {
94
185
  if (url.pathname !== "/auth/callback") throw new AuthError$1("Not a callback URL");
@@ -115,10 +206,14 @@ var LoginState = class {
115
206
  try {
116
207
  const tokens = await this.tokenStorage.getTokens();
117
208
  if (!tokens?.workspaceId) return null;
118
- const { data } = await this.sdk.client.GET("/v1/workspaces/{id}", { params: { path: { id: tokens.workspaceId } } });
209
+ const { data } = await this.sdk.client.GET("/v1/workspaces/{id}", {
210
+ params: { path: { id: tokens.workspaceId } },
211
+ signal: this.options.signal
212
+ });
119
213
  const name = data?.data?.name;
120
214
  return typeof name === "string" && name.trim().length > 0 ? name.trim() : null;
121
215
  } catch {
216
+ this.options.signal?.throwIfAborted();
122
217
  return null;
123
218
  }
124
219
  }
@@ -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,
@@ -2,9 +2,13 @@ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  //#region src/lib/project/local-pin.ts
4
4
  const LOCAL_RESOLUTION_PIN_RELATIVE_PATH = ".prisma/local.json";
5
- async function readLocalResolutionPin(cwd) {
5
+ async function readLocalResolutionPin(cwd, signal) {
6
+ signal?.throwIfAborted();
6
7
  try {
7
- const raw = await readFile(path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH), "utf8");
8
+ const raw = await readFile(path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH), {
9
+ encoding: "utf8",
10
+ signal
11
+ });
8
12
  const parsed = JSON.parse(raw);
9
13
  if (!isLocalResolutionPin(parsed)) return { kind: "invalid" };
10
14
  return {
@@ -17,28 +21,43 @@ async function readLocalResolutionPin(cwd) {
17
21
  throw error;
18
22
  }
19
23
  }
20
- async function writeLocalResolutionPin(cwd, pin) {
24
+ async function writeLocalResolutionPin(cwd, pin, signal) {
21
25
  const prismaDir = path.join(cwd, ".prisma");
26
+ signal?.throwIfAborted();
22
27
  await mkdir(prismaDir, { recursive: true });
23
28
  const pinPath = path.join(cwd, LOCAL_RESOLUTION_PIN_RELATIVE_PATH);
24
29
  const tmpPath = path.join(prismaDir, `local.${process.pid}.${Date.now()}.tmp`);
25
- await writeFile(tmpPath, `${JSON.stringify(pin, null, 2)}\n`, "utf8");
30
+ await writeFile(tmpPath, `${JSON.stringify(pin, null, 2)}\n`, {
31
+ encoding: "utf8",
32
+ signal
33
+ });
34
+ signal?.throwIfAborted();
26
35
  await rename(tmpPath, pinPath);
27
36
  }
28
- async function ensureLocalResolutionPinGitignore(cwd) {
37
+ async function ensureLocalResolutionPinGitignore(cwd, signal) {
29
38
  const gitignorePath = path.join(cwd, ".gitignore");
30
39
  let existing = null;
40
+ signal?.throwIfAborted();
31
41
  try {
32
- existing = await readFile(gitignorePath, "utf8");
42
+ existing = await readFile(gitignorePath, {
43
+ encoding: "utf8",
44
+ signal
45
+ });
33
46
  } catch (error) {
34
47
  if (error.code !== "ENOENT") throw error;
35
48
  }
36
49
  if (existing === null) {
37
- await writeFile(gitignorePath, ".prisma/\n", "utf8");
50
+ await writeFile(gitignorePath, ".prisma/\n", {
51
+ encoding: "utf8",
52
+ signal
53
+ });
38
54
  return;
39
55
  }
40
56
  if (existing.split(/\r?\n/).map((line) => line.trim()).some((line) => line === ".prisma/" || line === ".prisma/local.json")) return;
41
- await writeFile(gitignorePath, existing.endsWith("\n") ? `${existing}.prisma/\n` : `${existing}\n.prisma/\n`, "utf8");
57
+ await writeFile(gitignorePath, existing.endsWith("\n") ? `${existing}.prisma/\n` : `${existing}\n.prisma/\n`, {
58
+ encoding: "utf8",
59
+ signal
60
+ });
42
61
  }
43
62
  function isLocalResolutionPin(value) {
44
63
  if (!value || typeof value !== "object") return false;
@@ -11,7 +11,8 @@ async function resolveProjectTarget(options) {
11
11
  throw await projectSetupRequiredError({
12
12
  cwd: options.context.runtime.cwd,
13
13
  projects,
14
- commandName: options.commandName
14
+ commandName: options.commandName,
15
+ signal: options.context.runtime.signal
15
16
  });
16
17
  }
17
18
  async function inspectProjectBinding(options) {
@@ -26,7 +27,8 @@ async function inspectProjectBinding(options) {
26
27
  ...await buildProjectSetupSuggestion({
27
28
  cwd: options.context.runtime.cwd,
28
29
  projects,
29
- commandName: options.commandName ?? "project show"
30
+ commandName: options.commandName ?? "project show",
31
+ signal: options.context.runtime.signal
30
32
  })
31
33
  };
32
34
  }
@@ -72,7 +74,7 @@ function localStateStaleError() {
72
74
  });
73
75
  }
74
76
  async function buildProjectSetupSuggestion(options) {
75
- const suggestedName = await inferTargetName(options.cwd);
77
+ const suggestedName = await inferTargetName(options.cwd, options.signal);
76
78
  const candidates = sortProjects(options.projects.filter((project) => projectMatchesSuggestedName(project, suggestedName.name))).map(toProjectSummary);
77
79
  return {
78
80
  suggestedProjectName: suggestedName.name,
@@ -131,9 +133,13 @@ function buildProjectSetupNextActions(options = {}) {
131
133
  });
132
134
  return actions;
133
135
  }
134
- async function readPackageName(cwd) {
136
+ async function readPackageName(cwd, signal) {
137
+ signal?.throwIfAborted();
135
138
  try {
136
- const raw = await readFile(path.join(cwd, "package.json"), "utf8");
139
+ const raw = await readFile(path.join(cwd, "package.json"), {
140
+ encoding: "utf8",
141
+ signal
142
+ });
137
143
  const parsed = JSON.parse(raw);
138
144
  if (!parsed || typeof parsed !== "object") return null;
139
145
  const packageName = "name" in parsed ? parsed.name : null;
@@ -144,8 +150,8 @@ async function readPackageName(cwd) {
144
150
  throw error;
145
151
  }
146
152
  }
147
- async function inferTargetName(cwd) {
148
- const packageName = await readPackageName(cwd);
153
+ async function inferTargetName(cwd, signal) {
154
+ const packageName = await readPackageName(cwd, signal);
149
155
  if (packageName && isValidInferredTargetName(packageName)) return {
150
156
  name: packageName,
151
157
  source: "package-name"
@@ -186,7 +192,7 @@ async function resolveBoundProjectTarget(options, projects, settings) {
186
192
  targetNameSource: "env"
187
193
  });
188
194
  }
189
- const localPin = await readLocalResolutionPin(options.context.runtime.cwd);
195
+ const localPin = await readLocalResolutionPin(options.context.runtime.cwd, options.context.runtime.signal);
190
196
  if (localPin.kind === "invalid") throw localStateStaleError();
191
197
  if (localPin.kind === "present") {
192
198
  if (localPin.pin.workspaceId !== options.workspace.id) throw localStateStaleError();
@@ -222,7 +228,8 @@ function buildProjectRecoveryCommands(commandName) {
222
228
  function toProjectSummary(project) {
223
229
  return {
224
230
  id: project.id,
225
- name: project.name
231
+ name: project.name,
232
+ ...project.url ? { url: project.url } : {}
226
233
  };
227
234
  }
228
235
  //#endregion
@@ -20,8 +20,8 @@ async function bindProjectToDirectory(context, workspace, project, action) {
20
20
  await writeLocalResolutionPin(context.runtime.cwd, {
21
21
  workspaceId: workspace.id,
22
22
  projectId: project.id
23
- });
24
- await ensureLocalResolutionPinGitignore(context.runtime.cwd);
23
+ }, context.runtime.signal);
24
+ await ensureLocalResolutionPinGitignore(context.runtime.cwd, context.runtime.signal);
25
25
  return {
26
26
  workspace,
27
27
  project,
@@ -36,7 +36,8 @@ async function bindProjectToDirectory(context, workspace, project, action) {
36
36
  function toProjectSummary(project) {
37
37
  return {
38
38
  id: project.id,
39
- name: project.name
39
+ name: project.name,
40
+ ...project.url ? { url: project.url } : {}
40
41
  };
41
42
  }
42
43
  function projectSetupNameRequiredError(command) {
@@ -1,6 +1,8 @@
1
- import { renderNextSteps, renderSummaryLine } from "../shell/ui.js";
1
+ import { padDisplay, renderNextSteps, renderSummaryLine } from "../shell/ui.js";
2
+ import { formatDescriptorLabel } from "../shell/command-meta.js";
2
3
  import { formatCommandArgument } from "../shell/command-arguments.js";
3
4
  import { renderList, renderMutate, renderShow, serializeList } from "../output/patterns.js";
5
+ import path from "node:path";
4
6
  //#region src/presenters/project.ts
5
7
  function renderProjectList(context, descriptor, result) {
6
8
  const lines = renderList({
@@ -52,24 +54,7 @@ function renderProjectShow(context, descriptor, result) {
52
54
  lines.push(...renderNextSteps(["Link an existing Project you choose: prisma-cli project link <id-or-name>", `Create a new Project: prisma-cli project create ${formatCommandArgument(result.suggestedProjectName)}`]));
53
55
  return lines;
54
56
  }
55
- return renderShow({
56
- title: "Showing this directory's Project binding.",
57
- descriptor,
58
- fields: [
59
- {
60
- key: "workspace",
61
- value: result.workspace.name
62
- },
63
- {
64
- key: "project",
65
- value: result.project.name
66
- },
67
- {
68
- key: "resolution",
69
- value: formatProjectSource(result.resolution.projectSource)
70
- }
71
- ]
72
- }, context.ui);
57
+ return renderBoundProjectShow(context, descriptor, result);
73
58
  }
74
59
  function serializeProjectShow(result) {
75
60
  return result;
@@ -133,16 +118,31 @@ function renderGitDisconnect(context, descriptor, result) {
133
118
  details: ["GitHub branch automation is no longer active for this project."]
134
119
  }, context.ui);
135
120
  }
136
- function formatProjectSource(source) {
137
- switch (source) {
138
- case "explicit": return "explicit";
139
- case "env": return "environment";
140
- case "local-pin": return "local pin";
141
- case "platform-mapping": return "platform mapping";
142
- case "created": return "created";
143
- case "prompt": return "prompt";
144
- case "unbound": return "unbound";
121
+ function renderBoundProjectShow(context, descriptor, result) {
122
+ const { ui } = context;
123
+ const rail = ui.dim("");
124
+ const keyWidth = 10;
125
+ const platform = `${result.workspace.name} / ${result.project.name}`;
126
+ const lines = [
127
+ `${ui.strong(formatDescriptorLabel(descriptor))} ${ui.dim("")} ${ui.dim("This directory is linked to the following platform project.")}`,
128
+ "",
129
+ `${rail} ${ui.accent(padDisplay("local repo", keyWidth))} ${formatLocalRepoPath(context.runtime.cwd, context.runtime.env)}`,
130
+ `${rail} ${ui.accent(padDisplay("platform", keyWidth))} ${ui.strong(platform)}`
131
+ ];
132
+ if (result.project.url) {
133
+ lines.push(rail);
134
+ lines.push(`${rail} ${ui.dim("→")} ${ui.link(result.project.url)}`);
135
+ }
136
+ return lines;
137
+ }
138
+ function formatLocalRepoPath(cwd, env) {
139
+ const resolved = path.resolve(cwd);
140
+ const home = env.HOME ? path.resolve(env.HOME) : null;
141
+ if (home && (resolved === home || resolved.startsWith(`${home}${path.sep}`))) {
142
+ const relative = path.relative(home, resolved);
143
+ return relative ? `~/${relative}` : "~";
145
144
  }
145
+ return resolved;
146
146
  }
147
147
  function formatGitConnectionDetail(status) {
148
148
  switch (status) {
@@ -1,15 +1,20 @@
1
- import { CliError, authRequiredError } from "./errors.js";
1
+ import { CliError, authRequiredError, commandCanceledError } from "./errors.js";
2
2
  import { cliErrorToJson, writeHumanError, writeHumanLines, writeJsonError, writeJsonEvent, writeJsonSuccess } from "./output.js";
3
3
  import { getCommandDescriptor } from "./command-meta.js";
4
4
  import { resolveGlobalFlags } from "./global-flags.js";
5
5
  import { createCommandContext } from "./runtime.js";
6
6
  import { AuthError } from "@prisma/management-api-sdk";
7
7
  //#region src/shell/command-runner.ts
8
- function toCliError(error) {
8
+ function toCliError(error, runtime) {
9
+ if (isCancellationError(error) || runtime.signal.aborted) return commandCanceledError();
9
10
  if (error instanceof CliError) return error;
10
11
  if (error instanceof AuthError) return authRequiredError(["prisma-cli auth login"], { debug: error.message });
11
12
  return null;
12
13
  }
14
+ function isCancellationError(error) {
15
+ if (!(error instanceof Error)) return false;
16
+ return error.name === "AbortError" || error.name === "CancelledError";
17
+ }
13
18
  async function runCommand(runtime, commandName, options, handler, presenter) {
14
19
  const flags = resolveGlobalFlags(runtime.argv, options);
15
20
  const context = await createCommandContext(runtime, flags);
@@ -26,7 +31,7 @@ async function runCommand(runtime, commandName, options, handler, presenter) {
26
31
  if (flags.quiet) return;
27
32
  writeHumanLines(context.output, presenter.renderHuman(context, descriptor, success.result));
28
33
  } catch (error) {
29
- const cliError = toCliError(error);
34
+ const cliError = toCliError(error, runtime);
30
35
  if (cliError) {
31
36
  if (flags.json) writeJsonError(context.output, commandName, cliError);
32
37
  else writeHumanError(context.output, context.ui, cliError, { trace: flags.trace });
@@ -51,7 +56,7 @@ async function runStreamingCommand(runtime, commandName, options, handler) {
51
56
  nextActions: []
52
57
  });
53
58
  } catch (error) {
54
- const cliError = toCliError(error);
59
+ const cliError = toCliError(error, runtime);
55
60
  if (cliError) {
56
61
  if (flags.json) writeJsonEvent(context.output, {
57
62
  type: "error",
@@ -56,6 +56,17 @@ function authRequiredError(nextSteps = ["prisma-cli auth login"], options = {})
56
56
  nextSteps
57
57
  });
58
58
  }
59
+ function commandCanceledError() {
60
+ return new CliError({
61
+ code: "COMMAND_CANCELED",
62
+ domain: "cli",
63
+ summary: "Command canceled",
64
+ why: null,
65
+ fix: null,
66
+ exitCode: 130,
67
+ humanLines: ["Command canceled [COMMAND_CANCELED]"]
68
+ });
69
+ }
59
70
  function workspaceRequiredError() {
60
71
  return usageError("Workspace required", "This command needs an active workspace, but the authenticated session does not have one.", "Run prisma-cli auth login and choose a workspace.", ["prisma-cli auth login"], "auth");
61
72
  }
@@ -71,4 +82,4 @@ function featureUnavailableError(summary, why, fix, nextSteps = [], domain = "cl
71
82
  });
72
83
  }
73
84
  //#endregion
74
- export { CliError, authRequiredError, featureUnavailableError, usageError, workspaceRequiredError };
85
+ export { CliError, authRequiredError, commandCanceledError, featureUnavailableError, usageError, workspaceRequiredError };
@@ -22,13 +22,13 @@ async function createCommandContext(runtime, flags) {
22
22
  const fixturePath = runtime.fixturePath ?? runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
23
23
  const stateDir = resolveStateDir(runtime);
24
24
  let loadedApi;
25
- if (fixturePath) loadedApi = await MockApi.load(fixturePath);
25
+ if (fixturePath) loadedApi = await MockApi.load(fixturePath, runtime.signal);
26
26
  return {
27
27
  get api() {
28
28
  if (!loadedApi) throw new Error("context.api accessed in real mode. Set runtime.fixturePath or PRISMA_CLI_MOCK_FIXTURE_PATH to use fixture mode.");
29
29
  return loadedApi;
30
30
  },
31
- stateStore: new LocalStateStore(stateDir),
31
+ stateStore: new LocalStateStore(stateDir, runtime.signal),
32
32
  output: {
33
33
  stdout: runtime.stdout,
34
34
  stderr: runtime.stderr