@prisma/cli 3.0.0-beta.2 → 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.
- package/README.md +1 -1
- package/dist/adapters/git.js +8 -3
- package/dist/adapters/local-state.js +11 -3
- package/dist/adapters/mock-api.js +6 -2
- package/dist/adapters/token-storage.js +63 -22
- package/dist/cli.js +17 -2
- package/dist/cli2.js +3 -0
- package/dist/controllers/app-env.js +78 -46
- package/dist/controllers/app.js +111 -62
- package/dist/controllers/auth.js +8 -8
- package/dist/controllers/project.js +107 -67
- package/dist/lib/app/bun-project.js +12 -5
- package/dist/lib/app/local-dev.js +34 -18
- package/dist/lib/app/preview-build.js +90 -62
- package/dist/lib/app/preview-provider.js +118 -49
- package/dist/lib/auth/auth-ops.js +30 -21
- package/dist/lib/auth/guard.js +3 -2
- package/dist/lib/auth/login.js +109 -14
- package/dist/lib/project/interactive-setup.js +1 -1
- package/dist/lib/project/local-pin.js +27 -8
- package/dist/lib/project/resolution.js +14 -8
- package/dist/lib/project/setup.js +2 -2
- package/dist/shell/command-runner.js +9 -4
- package/dist/shell/errors.js +12 -1
- package/dist/shell/runtime.js +2 -2
- package/dist/shell/update-check.js +247 -0
- package/package.json +10 -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}", {
|
|
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 };
|
package/dist/lib/auth/guard.js
CHANGED
|
@@ -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,
|
package/dist/lib/auth/login.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
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}", {
|
|
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),
|
|
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`,
|
|
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,
|
|
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",
|
|
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`,
|
|
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"),
|
|
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();
|
|
@@ -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,
|
|
@@ -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",
|
package/dist/shell/errors.js
CHANGED
|
@@ -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 };
|
package/dist/shell/runtime.js
CHANGED
|
@@ -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
|