@prisma/cli 3.0.0-dev.53.1 → 3.0.0-dev.54.1

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.
@@ -2,7 +2,7 @@ import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
  //#region src/adapters/git.ts
4
4
  const execFileAsync = promisify(execFile);
5
- async function readGitOriginRemote(cwd) {
5
+ async function readGitOriginRemote(cwd, signal) {
6
6
  try {
7
7
  const { stdout } = await execFileAsync("git", [
8
8
  "config",
@@ -10,14 +10,19 @@ async function readGitOriginRemote(cwd) {
10
10
  "remote.origin.url"
11
11
  ], {
12
12
  cwd,
13
- timeout: 5e3
13
+ timeout: 5e3,
14
+ signal
14
15
  });
15
16
  const remote = stdout.trim();
16
17
  return remote.length > 0 ? remote : null;
17
- } catch {
18
+ } catch (error) {
19
+ if (signal?.aborted || isAbortError(error)) throw error;
18
20
  return null;
19
21
  }
20
22
  }
23
+ function isAbortError(error) {
24
+ return error instanceof Error && error.name === "AbortError";
25
+ }
21
26
  function parseGitHubRepositoryUrl(value) {
22
27
  const input = value.trim();
23
28
  const shorthand = input.match(/^git@github\.com:([^/\s]+)\/([^/\s]+?)(?:\.git)?$/);
@@ -20,12 +20,17 @@ function resolveLocalStateFilePath(stateDir) {
20
20
  }
21
21
  var LocalStateStore = class {
22
22
  stateFilePath;
23
- constructor(stateDir) {
23
+ constructor(stateDir, signal) {
24
+ this.signal = signal;
24
25
  this.stateFilePath = resolveLocalStateFilePath(stateDir);
25
26
  }
26
27
  async read() {
28
+ this.signal?.throwIfAborted();
27
29
  try {
28
- const raw = await readFile(this.stateFilePath, "utf8");
30
+ const raw = await readFile(this.stateFilePath, {
31
+ encoding: "utf8",
32
+ signal: this.signal
33
+ });
29
34
  const parsed = JSON.parse(raw);
30
35
  return {
31
36
  auth: parsed.auth ?? structuredClone(DEFAULT_STATE.auth),
@@ -46,8 +51,11 @@ var LocalStateStore = class {
46
51
  }
47
52
  }
48
53
  async write(state) {
54
+ this.signal?.throwIfAborted();
49
55
  await mkdir(path.dirname(this.stateFilePath), { recursive: true });
50
- await writeFile(this.stateFilePath, `${JSON.stringify(state, null, 2)}\n`, "utf8");
56
+ this.signal?.throwIfAborted();
57
+ await writeFile(this.stateFilePath, `${JSON.stringify(state, null, 2)}\n`, { encoding: "utf8" });
58
+ this.signal?.throwIfAborted();
51
59
  }
52
60
  async setAuthSession(session) {
53
61
  const state = await this.read();
@@ -5,8 +5,12 @@ var MockApi = class MockApi {
5
5
  constructor(data) {
6
6
  this.data = data;
7
7
  }
8
- static async load(fixturePath) {
9
- const raw = await readFile(fixturePath, "utf8");
8
+ static async load(fixturePath, signal) {
9
+ signal?.throwIfAborted();
10
+ const raw = await readFile(fixturePath, {
11
+ encoding: "utf8",
12
+ signal
13
+ });
10
14
  return new MockApi(JSON.parse(raw));
11
15
  }
12
16
  listProviders() {
@@ -20,37 +20,60 @@ function findLatestValidTokens(allCredentials) {
20
20
  function tokensEqual(a, b) {
21
21
  return a?.workspaceId === b?.workspaceId && a?.accessToken === b?.accessToken && a?.refreshToken === b?.refreshToken;
22
22
  }
23
- function sleep(ms) {
24
- return new Promise((resolve) => setTimeout(resolve, ms));
23
+ function sleep(ms, signal) {
24
+ signal?.throwIfAborted();
25
+ return new Promise((resolve, reject) => {
26
+ const onAbort = () => {
27
+ clearTimeout(timeout);
28
+ reject(signal?.reason);
29
+ };
30
+ const timeout = setTimeout(() => {
31
+ signal?.removeEventListener("abort", onAbort);
32
+ resolve();
33
+ }, ms);
34
+ signal?.addEventListener("abort", onAbort, { once: true });
35
+ });
25
36
  }
26
37
  var FileTokenStorage = class {
27
38
  credentialsStore;
28
39
  lockFilePath;
29
- constructor(env = process.env) {
40
+ constructor(env = process.env, signal) {
41
+ this.signal = signal;
30
42
  const authFilePath = getAuthFilePath(env);
31
43
  this.credentialsStore = new CredentialsStore(authFilePath);
32
44
  this.lockFilePath = `${authFilePath}.lock`;
33
45
  }
34
46
  async getTokens() {
47
+ this.signal?.throwIfAborted();
35
48
  try {
36
- return findLatestValidTokens(await this.credentialsStore.getCredentials());
37
- } catch {
49
+ const all = await this.credentialsStore.getCredentials();
50
+ this.signal?.throwIfAborted();
51
+ return findLatestValidTokens(all);
52
+ } catch (error) {
53
+ if (this.signal?.aborted) throw this.signal.reason;
38
54
  return null;
39
55
  }
40
56
  }
41
57
  async setTokens(tokens) {
58
+ this.signal?.throwIfAborted();
42
59
  await this.credentialsStore.storeCredentials({
43
60
  workspaceId: tokens.workspaceId,
44
61
  token: tokens.accessToken,
45
62
  refreshToken: tokens.refreshToken
46
63
  });
64
+ this.signal?.throwIfAborted();
47
65
  }
48
66
  async clearTokens() {
49
- const tokens = findLatestValidTokens(await this.credentialsStore.getCredentials());
67
+ this.signal?.throwIfAborted();
68
+ const all = await this.credentialsStore.getCredentials();
69
+ this.signal?.throwIfAborted();
70
+ const tokens = findLatestValidTokens(all);
50
71
  if (!tokens) return;
51
72
  await this.credentialsStore.deleteCredentials(tokens.workspaceId);
73
+ this.signal?.throwIfAborted();
52
74
  }
53
75
  async clearTokensIfCurrent(tokens) {
76
+ this.signal?.throwIfAborted();
54
77
  if (!tokensEqual(await this.getTokens(), tokens)) return;
55
78
  await this.clearTokens();
56
79
  }
@@ -64,34 +87,52 @@ var FileTokenStorage = class {
64
87
  }
65
88
  async acquireRefreshLock() {
66
89
  const lockId = randomUUID();
90
+ this.signal?.throwIfAborted();
67
91
  await fs.mkdir(path.dirname(this.lockFilePath), { recursive: true });
68
- while (true) try {
69
- const handle = await fs.open(this.lockFilePath, "wx");
92
+ while (true) {
93
+ this.signal?.throwIfAborted();
94
+ let lockFileCreated = false;
70
95
  try {
71
- await handle.writeFile(lockId, "utf8");
72
- } finally {
73
- await handle.close();
74
- }
75
- return lockId;
76
- } catch (error) {
77
- if (error.code !== "EEXIST") throw error;
78
- const staleLockId = await this.getStaleRefreshLockId();
79
- if (staleLockId) {
80
- await this.releaseRefreshLock(staleLockId);
81
- continue;
96
+ const handle = await fs.open(this.lockFilePath, "wx");
97
+ lockFileCreated = true;
98
+ try {
99
+ this.signal?.throwIfAborted();
100
+ await handle.writeFile(lockId, { encoding: "utf8" });
101
+ this.signal?.throwIfAborted();
102
+ } finally {
103
+ await handle.close();
104
+ }
105
+ return lockId;
106
+ } catch (error) {
107
+ if (lockFileCreated) await fs.unlink(this.lockFilePath).catch(() => void 0);
108
+ if (error.code !== "EEXIST") throw error;
109
+ const staleLockId = await this.getStaleRefreshLockId();
110
+ if (staleLockId) {
111
+ await this.releaseRefreshLock(staleLockId);
112
+ continue;
113
+ }
114
+ await sleep(100, this.signal);
82
115
  }
83
- await sleep(100);
84
116
  }
85
117
  }
86
118
  async getStaleRefreshLockId() {
87
- const lockId = await fs.readFile(this.lockFilePath, "utf8").catch(() => null);
119
+ this.signal?.throwIfAborted();
120
+ const lockId = await fs.readFile(this.lockFilePath, {
121
+ encoding: "utf8",
122
+ signal: this.signal
123
+ }).catch((error) => {
124
+ if (this.signal?.aborted) throw error;
125
+ return null;
126
+ });
88
127
  if (lockId === null) return null;
128
+ this.signal?.throwIfAborted();
89
129
  const stats = await fs.stat(this.lockFilePath).catch(() => null);
130
+ this.signal?.throwIfAborted();
90
131
  if (!stats) return null;
91
132
  return Date.now() - stats.mtimeMs > 3e4 ? lockId : null;
92
133
  }
93
134
  async releaseRefreshLock(lockId) {
94
- if (await fs.readFile(this.lockFilePath, "utf8").catch(() => null) !== lockId) return;
135
+ if (await fs.readFile(this.lockFilePath, { encoding: "utf8" }).catch(() => null) !== lockId) return;
95
136
  await fs.unlink(this.lockFilePath).catch(() => {});
96
137
  }
97
138
  };
package/dist/cli.js CHANGED
@@ -6,8 +6,19 @@ import process from "node:process";
6
6
  if (process.env.PRISMA_CLI_RUN_UPDATE_CHECK_WORKER === "1") runUpdateDiscoveryWorker().then(() => {
7
7
  process.exitCode = 0;
8
8
  });
9
- else runCli().then((exitCode) => {
10
- process.exitCode = exitCode;
11
- });
9
+ else {
10
+ const controller = new AbortController();
11
+ const abortCli = () => {
12
+ if (!controller.signal.aborted) controller.abort(new DOMException("Command canceled", "AbortError"));
13
+ };
14
+ process.once("SIGINT", abortCli);
15
+ process.once("SIGTERM", abortCli);
16
+ runCli({ signal: controller.signal }).then((exitCode) => {
17
+ process.exitCode = exitCode;
18
+ }).finally(() => {
19
+ process.off("SIGINT", abortCli);
20
+ process.off("SIGTERM", abortCli);
21
+ });
22
+ }
12
23
  //#endregion
13
24
  export {};
package/dist/cli2.js CHANGED
@@ -106,6 +106,7 @@ function resolveRuntime(options) {
106
106
  argv: options.argv ?? process.argv.slice(2),
107
107
  cwd: options.cwd ?? process.env.INIT_CWD ?? process.cwd(),
108
108
  env: options.env ?? process.env,
109
+ signal: options.signal ?? new AbortController().signal,
109
110
  stdin: options.stdin ?? process.stdin,
110
111
  stdout: options.stdout ?? process.stdout,
111
112
  stderr: options.stderr ?? process.stderr,
@@ -19,8 +19,11 @@ async function runEnvAdd(context, rawAssignment, flags) {
19
19
  });
20
20
  if (!scope) throw usageError(`prisma-cli project env add requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env add ${key}=${value} --role production`], "app");
21
21
  const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env add");
22
- const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: true });
23
- if (await findVariableByNaturalKey(client, projectId, key, resolved)) throw new CliError({
22
+ const resolved = await resolveScopeToApi(client, projectId, scope, {
23
+ createBranchIfMissing: true,
24
+ signal: context.runtime.signal
25
+ });
26
+ if (await findVariableByNaturalKey(client, projectId, key, resolved, context.runtime.signal)) throw new CliError({
24
27
  code: "ENV_VARIABLE_ALREADY_EXISTS",
25
28
  domain: "app",
26
29
  summary: `Variable "${key}" already exists in ${formatScopeLabel(scope)}`,
@@ -42,14 +45,17 @@ async function runEnvAdd(context, rawAssignment, flags) {
42
45
  class: "preview",
43
46
  branchId: null
44
47
  }
45
- }) ? [`Variable "${key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
46
- const { data, error, response } = await client.POST("/v1/environment-variables", { body: {
47
- projectId,
48
- class: resolved.apiTarget.class,
49
- ...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {},
50
- key,
51
- value
52
- } });
48
+ }, context.runtime.signal) ? [`Variable "${key}" does not exist in preview. It will only exist on ${formatScopeLabel(scope)}.`] : [];
49
+ const { data, error, response } = await client.POST("/v1/environment-variables", {
50
+ body: {
51
+ projectId,
52
+ class: resolved.apiTarget.class,
53
+ ...resolved.apiTarget.branchId !== null ? { branchId: resolved.apiTarget.branchId } : {},
54
+ key,
55
+ value
56
+ },
57
+ signal: context.runtime.signal
58
+ });
53
59
  if (error || !data) throw apiCallError(`Failed to add ${key}`, response, error);
54
60
  return {
55
61
  command: "project.env.add",
@@ -70,8 +76,11 @@ async function runEnvUpdate(context, rawAssignment, flags) {
70
76
  });
71
77
  if (!scope) throw usageError(`prisma-cli project env update requires --role or --branch`, "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env update ${key}=${value} --role production`], "app");
72
78
  const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env update");
73
- const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
74
- const existing = await findVariableByNaturalKey(client, projectId, key, resolved);
79
+ const resolved = await resolveScopeToApi(client, projectId, scope, {
80
+ createBranchIfMissing: false,
81
+ signal: context.runtime.signal
82
+ });
83
+ const existing = await findVariableByNaturalKey(client, projectId, key, resolved, context.runtime.signal);
75
84
  if (!existing) throw new CliError({
76
85
  code: "ENV_VARIABLE_NOT_FOUND",
77
86
  domain: "app",
@@ -83,7 +92,8 @@ async function runEnvUpdate(context, rawAssignment, flags) {
83
92
  });
84
93
  const { data, error, response } = await client.PATCH("/v1/environment-variables/{envVarId}", {
85
94
  params: { path: { envVarId: existing.id } },
86
- body: { value }
95
+ body: { value },
96
+ signal: context.runtime.signal
87
97
  });
88
98
  if (error || !data) throw apiCallError(`Failed to update value for ${key}`, response, error);
89
99
  return {
@@ -103,8 +113,11 @@ async function runEnvList(context, flags) {
103
113
  command: "list"
104
114
  }) ?? defaultRoleScope();
105
115
  const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env list");
106
- const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
107
- const variables = await listVariables(client, projectId, resolved);
116
+ const resolved = await resolveScopeToApi(client, projectId, scope, {
117
+ createBranchIfMissing: false,
118
+ signal: context.runtime.signal
119
+ });
120
+ const variables = await listVariables(client, projectId, resolved, context.runtime.signal);
108
121
  return {
109
122
  command: "project.env.list",
110
123
  result: {
@@ -124,8 +137,11 @@ async function runEnvRemove(context, key, flags) {
124
137
  });
125
138
  if (!scope) throw usageError("prisma-cli project env remove requires --role or --branch", "Writing without an explicit scope is rejected.", "Pass --role production, --role preview, or --branch <git-name>.", [`prisma-cli project env remove ${key} --role production`], "app");
126
139
  const { client, projectId } = await requireClientAndProject(context, flags.projectRef, "project env remove");
127
- const resolved = await resolveScopeToApi(client, projectId, scope, { createBranchIfMissing: false });
128
- const existing = await findVariableByNaturalKey(client, projectId, key, resolved);
140
+ const resolved = await resolveScopeToApi(client, projectId, scope, {
141
+ createBranchIfMissing: false,
142
+ signal: context.runtime.signal
143
+ });
144
+ const existing = await findVariableByNaturalKey(client, projectId, key, resolved, context.runtime.signal);
129
145
  if (!existing) throw new CliError({
130
146
  code: "ENV_VARIABLE_NOT_FOUND",
131
147
  domain: "app",
@@ -135,7 +151,10 @@ async function runEnvRemove(context, key, flags) {
135
151
  exitCode: 1,
136
152
  nextSteps: [`prisma-cli project env list ${formatScopeFlag(scope)}`]
137
153
  });
138
- const { error, response } = await client.DELETE("/v1/environment-variables/{envVarId}", { params: { path: { envVarId: existing.id } } });
154
+ const { error, response } = await client.DELETE("/v1/environment-variables/{envVarId}", {
155
+ params: { path: { envVarId: existing.id } },
156
+ signal: context.runtime.signal
157
+ });
139
158
  if (error) throw apiCallError(`Failed to remove ${key}`, response, error);
140
159
  return {
141
160
  command: "project.env.remove",
@@ -150,7 +169,7 @@ async function runEnvRemove(context, key, flags) {
150
169
  }
151
170
  async function requireClientAndProject(context, explicitProject, commandName) {
152
171
  const authState = await requireAuthenticatedAuthState(context);
153
- const client = await requireComputeAuth(context.runtime.env);
172
+ const client = await requireComputeAuth(context.runtime.env, context.runtime.signal);
154
173
  if (!client) throw authRequiredError(["prisma-cli auth login"]);
155
174
  if (!authState.workspace) throw workspaceRequiredError();
156
175
  return {
@@ -159,7 +178,7 @@ async function requireClientAndProject(context, explicitProject, commandName) {
159
178
  context,
160
179
  workspace: authState.workspace,
161
180
  explicitProject,
162
- listProjects: () => listRealWorkspaceProjects(client, authState.workspace),
181
+ listProjects: () => listRealWorkspaceProjects(client, authState.workspace, context.runtime.signal),
163
182
  commandName
164
183
  })).project.id
165
184
  };
@@ -176,7 +195,7 @@ async function resolveScopeToApi(client, projectId, scope, options) {
176
195
  branchId: null
177
196
  }
178
197
  };
179
- const branch = options.createBranchIfMissing ? await resolveOrCreateBranch(client, projectId, scope.branchName) : await resolveExistingBranch(client, projectId, scope.branchName);
198
+ const branch = options.createBranchIfMissing ? await resolveOrCreateBranch(client, projectId, scope.branchName, options.signal) : await resolveExistingBranch(client, projectId, scope.branchName, options.signal);
180
199
  if (branch.isDefault) throw new CliError({
181
200
  code: "ENV_BRANCH_SCOPE_IS_PRODUCTION",
182
201
  domain: "app",
@@ -203,16 +222,19 @@ function formatScopeFlag(scope) {
203
222
  if (scope.kind === "role") return `--role ${scope.role}`;
204
223
  return `--branch ${scope.branchName}`;
205
224
  }
206
- async function listBranchesByName(client, projectId, branchName) {
207
- const { data, error, response } = await client.GET("/v1/projects/{projectId}/branches", { params: {
208
- path: { projectId },
209
- query: { gitName: branchName }
210
- } });
225
+ async function listBranchesByName(client, projectId, branchName, signal) {
226
+ const { data, error, response } = await client.GET("/v1/projects/{projectId}/branches", {
227
+ params: {
228
+ path: { projectId },
229
+ query: { gitName: branchName }
230
+ },
231
+ signal
232
+ });
211
233
  if (error || !data) throw apiCallError(`Failed to resolve branch "${branchName}"`, response, error);
212
234
  return data.data;
213
235
  }
214
- async function resolveExistingBranch(client, projectId, branchName) {
215
- const branch = (await listBranchesByName(client, projectId, branchName))[0];
236
+ async function resolveExistingBranch(client, projectId, branchName, signal) {
237
+ const branch = (await listBranchesByName(client, projectId, branchName, signal))[0];
216
238
  if (!branch) throw new CliError({
217
239
  code: "ENV_BRANCH_NOT_FOUND",
218
240
  domain: "app",
@@ -224,10 +246,10 @@ async function resolveExistingBranch(client, projectId, branchName) {
224
246
  });
225
247
  return branch;
226
248
  }
227
- async function resolveOrCreateBranch(client, projectId, branchName) {
228
- const existing = (await listBranchesByName(client, projectId, branchName))[0];
249
+ async function resolveOrCreateBranch(client, projectId, branchName, signal) {
250
+ const existing = (await listBranchesByName(client, projectId, branchName, signal))[0];
229
251
  if (existing) return existing;
230
- if (!await projectHasDefaultBranch(client, projectId)) throw new CliError({
252
+ if (!await projectHasDefaultBranch(client, projectId, signal)) throw new CliError({
231
253
  code: "ENV_BRANCH_CREATE_REQUIRES_DEFAULT_BRANCH",
232
254
  domain: "app",
233
255
  summary: `Cannot create branch "${branchName}" from project env`,
@@ -241,42 +263,49 @@ async function resolveOrCreateBranch(client, projectId, branchName) {
241
263
  body: {
242
264
  gitName: branchName,
243
265
  isDefault: false
244
- }
266
+ },
267
+ signal
245
268
  });
246
269
  if (error || !data) {
247
270
  if (response?.status === 409) {
248
- const raced = (await listBranchesByName(client, projectId, branchName))[0];
271
+ const raced = (await listBranchesByName(client, projectId, branchName, signal))[0];
249
272
  if (raced) return raced;
250
273
  }
251
274
  throw apiCallError(`Failed to create branch "${branchName}"`, response, error);
252
275
  }
253
276
  return data.data;
254
277
  }
255
- async function projectHasDefaultBranch(client, projectId) {
278
+ async function projectHasDefaultBranch(client, projectId, signal) {
256
279
  let cursor;
257
280
  while (true) {
258
281
  const query = {};
259
282
  if (cursor !== void 0) query.cursor = cursor;
260
- const result = await client.GET("/v1/projects/{projectId}/branches", { params: {
261
- path: { projectId },
262
- query
263
- } });
283
+ const result = await client.GET("/v1/projects/{projectId}/branches", {
284
+ params: {
285
+ path: { projectId },
286
+ query
287
+ },
288
+ signal
289
+ });
264
290
  if (result.error || !result.data) throw apiCallError("Failed to check project default branch", result.response, result.error);
265
291
  if (result.data.data.some((branch) => branch.isDefault)) return true;
266
292
  if (!result.data.pagination.hasMore || !result.data.pagination.nextCursor) return false;
267
293
  cursor = result.data.pagination.nextCursor;
268
294
  }
269
295
  }
270
- async function findVariableByNaturalKey(client, projectId, key, resolved) {
271
- const { data, error, response } = await client.GET("/v1/environment-variables", { params: { query: {
272
- projectId,
273
- class: resolved.apiTarget.class,
274
- key
275
- } } });
296
+ async function findVariableByNaturalKey(client, projectId, key, resolved, signal) {
297
+ const { data, error, response } = await client.GET("/v1/environment-variables", {
298
+ params: { query: {
299
+ projectId,
300
+ class: resolved.apiTarget.class,
301
+ key
302
+ } },
303
+ signal
304
+ });
276
305
  if (error || !data) throw apiCallError(`Failed to look up ${key}`, response, error);
277
306
  return data.data.filter((row) => rowMatchesExactScope(row, resolved))[0] ?? null;
278
307
  }
279
- async function listVariables(client, projectId, resolved) {
308
+ async function listVariables(client, projectId, resolved, signal) {
280
309
  const collected = [];
281
310
  let cursor;
282
311
  while (true) {
@@ -285,7 +314,10 @@ async function listVariables(client, projectId, resolved) {
285
314
  class: resolved.apiTarget.class
286
315
  };
287
316
  if (cursor !== void 0) query.cursor = cursor;
288
- const result = await client.GET("/v1/environment-variables", { params: { query } });
317
+ const result = await client.GET("/v1/environment-variables", {
318
+ params: { query },
319
+ signal
320
+ });
289
321
  if (result.error || !result.data) throw apiCallError(`Failed to list environment variables`, result.response, result.error);
290
322
  const page = result.data.data.filter((row) => rowMatchesScope(row, resolved));
291
323
  collected.push(...page);