@prisma/cli 3.0.0-beta.2 → 3.0.0-beta.4
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 +4 -3
- 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/commands/app/index.js +4 -2
- package/dist/commands/branch/index.js +2 -27
- package/dist/controllers/app-env.js +227 -64
- package/dist/controllers/app.js +116 -91
- package/dist/controllers/auth.js +8 -8
- package/dist/controllers/branch.js +73 -47
- 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 +143 -54
- package/dist/lib/app/production-deploy-gate.js +160 -0
- 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/git/local-branch.js +41 -0
- 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/presenters/app-env.js +14 -3
- package/dist/presenters/branch.js +29 -101
- package/dist/shell/command-meta.js +6 -24
- 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/ui.js +4 -1
- package/dist/shell/update-check.js +247 -0
- package/dist/use-cases/branch.js +20 -68
- package/dist/use-cases/create-cli-gateways.js +2 -17
- package/package.json +10 -10
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
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { access, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
//#region src/lib/git/local-branch.ts
|
|
4
|
+
async function readLocalGitBranch(cwd, signal) {
|
|
5
|
+
const headPath = await resolveGitHeadPath(path.join(cwd, ".git"), signal);
|
|
6
|
+
if (!headPath) return null;
|
|
7
|
+
try {
|
|
8
|
+
const head = (await readFile(headPath, {
|
|
9
|
+
encoding: "utf8",
|
|
10
|
+
signal
|
|
11
|
+
})).trim();
|
|
12
|
+
if (head.startsWith("ref: refs/heads/")) return head.slice(16);
|
|
13
|
+
} catch (error) {
|
|
14
|
+
if (signal.aborted) throw error;
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
async function resolveGitHeadPath(gitPath, signal) {
|
|
20
|
+
signal.throwIfAborted();
|
|
21
|
+
try {
|
|
22
|
+
const raw = await readFile(gitPath, {
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
signal
|
|
25
|
+
});
|
|
26
|
+
if (raw.startsWith("gitdir:")) return path.join(path.resolve(path.dirname(gitPath), raw.slice(7).trim()), "HEAD");
|
|
27
|
+
} catch (error) {
|
|
28
|
+
if (signal.aborted) throw error;
|
|
29
|
+
}
|
|
30
|
+
signal.throwIfAborted();
|
|
31
|
+
try {
|
|
32
|
+
await access(path.join(gitPath, "HEAD"));
|
|
33
|
+
signal.throwIfAborted();
|
|
34
|
+
return path.join(gitPath, "HEAD");
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (signal.aborted) throw error;
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { readLocalGitBranch };
|
|
@@ -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,
|
|
@@ -2,8 +2,18 @@ import { renderList, renderShow, serializeList } from "../output/patterns.js";
|
|
|
2
2
|
//#region src/presenters/app-env.ts
|
|
3
3
|
function scopeLabel(scope) {
|
|
4
4
|
if (scope.kind === "role") return scope.role ?? "unknown";
|
|
5
|
+
if (scope.kind === "overview") return "overview";
|
|
5
6
|
return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
|
|
6
7
|
}
|
|
8
|
+
function listTargetLabel(result) {
|
|
9
|
+
const target = result.target;
|
|
10
|
+
if (target.source === "overview") return "overview";
|
|
11
|
+
if (target.branchName) {
|
|
12
|
+
const suffix = target.branchExists === false ? " (not created yet)" : "";
|
|
13
|
+
return `branch:${target.branchName} -> ${target.envMap}${suffix}`;
|
|
14
|
+
}
|
|
15
|
+
return scopeLabel(result.scope);
|
|
16
|
+
}
|
|
7
17
|
function renderEnvAdd(context, descriptor, result) {
|
|
8
18
|
return renderShow({
|
|
9
19
|
title: "Setting a new environment variable.",
|
|
@@ -75,8 +85,8 @@ function renderEnvList(context, descriptor, result) {
|
|
|
75
85
|
title: "Listing environment variables for the selected scope.",
|
|
76
86
|
descriptor,
|
|
77
87
|
parentContext: {
|
|
78
|
-
key: "
|
|
79
|
-
value:
|
|
88
|
+
key: "target",
|
|
89
|
+
value: listTargetLabel(result)
|
|
80
90
|
},
|
|
81
91
|
items: result.variables.map((variable) => ({
|
|
82
92
|
noun: "variable",
|
|
@@ -91,8 +101,9 @@ function serializeEnvList(result) {
|
|
|
91
101
|
return {
|
|
92
102
|
projectId: result.projectId,
|
|
93
103
|
scope: result.scope,
|
|
104
|
+
target: result.target,
|
|
94
105
|
...serializeList({
|
|
95
|
-
context: {
|
|
106
|
+
context: { target: listTargetLabel(result) },
|
|
96
107
|
items: result.variables.map((variable) => ({
|
|
97
108
|
noun: "variable",
|
|
98
109
|
label: `${variable.key} (${variable.source})`,
|
|
@@ -1,111 +1,39 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { formatColumns } from "../shell/ui.js";
|
|
2
|
+
import { formatDescriptorLabel } from "../shell/command-meta.js";
|
|
2
3
|
//#region src/presenters/branch.ts
|
|
3
4
|
function renderBranchList(context, descriptor, result) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
5
|
+
const ui = context.ui;
|
|
6
|
+
const lines = [`${ui.strong(formatDescriptorLabel(descriptor))} ${ui.dim("→")} ${ui.dim("Listing branches for the resolved project.")}`, ""];
|
|
7
|
+
const rail = ui.dim("│");
|
|
8
|
+
lines.push(`${rail} ${ui.accent("project:")} ${result.projectName}`);
|
|
9
|
+
lines.push(rail);
|
|
10
|
+
if (result.branches.length === 0) {
|
|
11
|
+
lines.push(`${rail} ${ui.dim("No branches found.")}`);
|
|
12
|
+
return lines;
|
|
13
|
+
}
|
|
14
|
+
const widths = [
|
|
15
|
+
Math.max(4, ...result.branches.map((branch) => branch.name.length)),
|
|
16
|
+
Math.max(4, ...result.branches.map((branch) => branch.role.length)),
|
|
17
|
+
Math.max(7, ...result.branches.map((branch) => branch.envMap.length))
|
|
18
|
+
];
|
|
19
|
+
lines.push(`${rail} ${ui.accent(formatColumns([
|
|
20
|
+
"Name",
|
|
21
|
+
"Role",
|
|
22
|
+
"Env map"
|
|
23
|
+
], widths))}`);
|
|
24
|
+
for (const branch of result.branches) lines.push(`${rail} ${formatColumns([
|
|
25
|
+
branch.name,
|
|
26
|
+
branch.role,
|
|
27
|
+
branch.envMap
|
|
28
|
+
], widths)}`);
|
|
29
|
+
return lines;
|
|
19
30
|
}
|
|
20
31
|
function serializeBranchList(result) {
|
|
21
|
-
return serializeList({
|
|
22
|
-
context: { project: result.projectName ?? "not resolved" },
|
|
23
|
-
items: result.branches.map((branch) => ({
|
|
24
|
-
noun: "branch",
|
|
25
|
-
label: branch.name,
|
|
26
|
-
id: branch.id,
|
|
27
|
-
status: branch.active ? "active" : null
|
|
28
|
-
}))
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
function serializeBranchShow(result) {
|
|
32
32
|
return {
|
|
33
33
|
projectId: result.projectId,
|
|
34
34
|
projectName: result.projectName,
|
|
35
|
-
|
|
36
|
-
name: result.branch.name,
|
|
37
|
-
kind: result.branch.kind,
|
|
38
|
-
active: result.branch.active,
|
|
39
|
-
remoteState: result.branch.remoteState,
|
|
40
|
-
liveDeployment: result.branch.liveDeployment
|
|
41
|
-
}
|
|
35
|
+
branches: result.branches
|
|
42
36
|
};
|
|
43
37
|
}
|
|
44
|
-
function renderBranchShow(context, descriptor, result) {
|
|
45
|
-
const fields = [
|
|
46
|
-
{
|
|
47
|
-
key: "project",
|
|
48
|
-
value: result.projectName ?? "not resolved",
|
|
49
|
-
tone: result.projectName ? "default" : "dim"
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
key: "branch",
|
|
53
|
-
value: result.branch.name,
|
|
54
|
-
tone: result.branch.active ? "success" : "default"
|
|
55
|
-
},
|
|
56
|
-
{
|
|
57
|
-
key: "kind",
|
|
58
|
-
value: result.branch.kind
|
|
59
|
-
}
|
|
60
|
-
];
|
|
61
|
-
if (result.branch.liveDeployment) {
|
|
62
|
-
fields.push({
|
|
63
|
-
key: "status",
|
|
64
|
-
value: result.branch.liveDeployment.status,
|
|
65
|
-
tone: toneForDeploymentStatus(result.branch.liveDeployment.status)
|
|
66
|
-
});
|
|
67
|
-
if (result.branch.liveDeployment.url) fields.push({
|
|
68
|
-
key: "url",
|
|
69
|
-
value: result.branch.liveDeployment.url,
|
|
70
|
-
tone: "link"
|
|
71
|
-
});
|
|
72
|
-
} else if (!result.branch.remoteState) fields.push({
|
|
73
|
-
key: "remote state",
|
|
74
|
-
value: "not created yet",
|
|
75
|
-
tone: "dim"
|
|
76
|
-
});
|
|
77
|
-
return renderShow({
|
|
78
|
-
title: "Showing the current active branch context.",
|
|
79
|
-
descriptor,
|
|
80
|
-
fields
|
|
81
|
-
}, context.ui);
|
|
82
|
-
}
|
|
83
|
-
function toneForDeploymentStatus(status) {
|
|
84
|
-
if (status === "ready" || status === "active" || status === "healthy") return "success";
|
|
85
|
-
if (status === "pending" || status === "building" || status === "starting") return "warning";
|
|
86
|
-
if (status === "failed" || status === "error") return "error";
|
|
87
|
-
return "default";
|
|
88
|
-
}
|
|
89
|
-
function renderBranchUse(context, descriptor, result) {
|
|
90
|
-
return renderMutate({
|
|
91
|
-
title: "Changing the local default branch context.",
|
|
92
|
-
descriptor,
|
|
93
|
-
context: [{
|
|
94
|
-
key: "project",
|
|
95
|
-
value: result.projectName ?? "not resolved",
|
|
96
|
-
tone: result.projectName ? "default" : "dim"
|
|
97
|
-
}, {
|
|
98
|
-
key: "branch",
|
|
99
|
-
value: result.branch.name
|
|
100
|
-
}],
|
|
101
|
-
operationDescription: "Applying active branch change",
|
|
102
|
-
operationCount: 1,
|
|
103
|
-
details: ["Active branch updated in local CLI state for this repo."],
|
|
104
|
-
alerts: result.branch.kind === "production" ? [{
|
|
105
|
-
tone: "warning",
|
|
106
|
-
text: "Production is protected and durable. Use with care"
|
|
107
|
-
}] : void 0
|
|
108
|
-
}, context.ui);
|
|
109
|
-
}
|
|
110
38
|
//#endregion
|
|
111
|
-
export { renderBranchList,
|
|
39
|
+
export { renderBranchList, serializeBranchList };
|
|
@@ -69,8 +69,8 @@ const DESCRIPTORS = [
|
|
|
69
69
|
{
|
|
70
70
|
id: "branch",
|
|
71
71
|
path: ["prisma", "branch"],
|
|
72
|
-
description: "View your
|
|
73
|
-
examples: ["prisma-cli branch list"
|
|
72
|
+
description: "View your Platform branches",
|
|
73
|
+
examples: ["prisma-cli branch list"]
|
|
74
74
|
},
|
|
75
75
|
{
|
|
76
76
|
id: "git",
|
|
@@ -153,29 +153,9 @@ const DESCRIPTORS = [
|
|
|
153
153
|
"branch",
|
|
154
154
|
"list"
|
|
155
155
|
],
|
|
156
|
-
description: "List
|
|
156
|
+
description: "List Platform branches for the resolved project",
|
|
157
157
|
examples: ["prisma-cli branch list", "prisma-cli branch list --json"]
|
|
158
158
|
},
|
|
159
|
-
{
|
|
160
|
-
id: "branch.show",
|
|
161
|
-
path: [
|
|
162
|
-
"prisma",
|
|
163
|
-
"branch",
|
|
164
|
-
"show"
|
|
165
|
-
],
|
|
166
|
-
description: "Show the Platform branch matching your current Git branch",
|
|
167
|
-
examples: ["prisma-cli branch show", "prisma-cli branch show --json"]
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
id: "branch.use",
|
|
171
|
-
path: [
|
|
172
|
-
"prisma",
|
|
173
|
-
"branch",
|
|
174
|
-
"use"
|
|
175
|
-
],
|
|
176
|
-
description: "Change the local default branch context.",
|
|
177
|
-
examples: ["prisma-cli branch use", "prisma-cli branch use production"]
|
|
178
|
-
},
|
|
179
159
|
{
|
|
180
160
|
id: "app.build",
|
|
181
161
|
path: [
|
|
@@ -216,6 +196,7 @@ const DESCRIPTORS = [
|
|
|
216
196
|
"prisma-cli app deploy --app my-app --env DATABASE_URL=postgresql://example",
|
|
217
197
|
"prisma-cli app deploy --app my-app --framework nextjs --http-port 3000",
|
|
218
198
|
"prisma-cli app deploy --branch feat-login --framework hono",
|
|
199
|
+
"prisma-cli app deploy --prod --yes",
|
|
219
200
|
"pnpm dlx skills@latest add prisma/prisma-cli/skills#cli-v<cli-version> --all",
|
|
220
201
|
"prisma-cli app deploy --framework bun --entry src/server.ts"
|
|
221
202
|
]
|
|
@@ -378,7 +359,7 @@ const DESCRIPTORS = [
|
|
|
378
359
|
],
|
|
379
360
|
description: "Manage environment variables for the active project",
|
|
380
361
|
examples: [
|
|
381
|
-
"prisma-cli project env list
|
|
362
|
+
"prisma-cli project env list",
|
|
382
363
|
"prisma-cli project env add STRIPE_KEY=sk_test_xxx --role production",
|
|
383
364
|
"prisma-cli project env add DATABASE_URL=postgresql://branch --branch feature/foo",
|
|
384
365
|
"prisma-cli project env remove STRIPE_KEY --role preview"
|
|
@@ -425,6 +406,7 @@ const DESCRIPTORS = [
|
|
|
425
406
|
],
|
|
426
407
|
description: "List environment variable metadata for a scope (no values).",
|
|
427
408
|
examples: [
|
|
409
|
+
"prisma-cli project env list",
|
|
428
410
|
"prisma-cli project env list --role production",
|
|
429
411
|
"prisma-cli project env list --role preview",
|
|
430
412
|
"prisma-cli project env list --branch feature/foo"
|
|
@@ -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",
|