@prisma/cli 3.0.0-dev.97.1 → 8.0.0-rc.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.
- package/README.md +3 -14
- package/dist/adapters/local-state.js +14 -3
- package/dist/{lib/auth → auth}/client.js +11 -3
- package/dist/auth/errors.js +50 -0
- package/dist/{lib/auth → auth}/guard.js +7 -6
- package/dist/{lib/auth → auth}/login.js +147 -6
- package/dist/{lib/auth/auth-ops.js → auth/operations.js} +63 -13
- package/dist/auth/recipient.js +42 -0
- package/dist/{adapters → auth}/token-storage.js +28 -7
- package/dist/auth/workspaces.js +171 -0
- package/dist/cli-command.js +14 -0
- package/dist/cli-name.js +12 -0
- package/dist/cli.js +1 -1
- package/dist/cli2.js +25 -8
- package/dist/command-arguments.js +12 -0
- package/dist/commands/agent/index.js +60 -0
- package/dist/commands/app/index.js +11 -6
- package/dist/commands/auth/index.js +2 -3
- package/dist/commands/bucket/index.js +123 -0
- package/dist/commands/build/index.js +29 -0
- package/dist/commands/database/index.js +92 -2
- package/dist/commands/feedback/index.js +20 -0
- package/dist/commands/git/index.js +1 -1
- package/dist/commands/init/index.js +33 -0
- package/dist/commands/project/index.js +54 -5
- package/dist/controllers/agent-setup.js +52 -0
- package/dist/controllers/agent.js +228 -0
- package/dist/controllers/app-env-api.js +3 -2
- package/dist/controllers/app-env-file.js +1 -1
- package/dist/controllers/app-env.js +5 -5
- package/dist/controllers/app.js +423 -358
- package/dist/controllers/auth.js +62 -254
- package/dist/controllers/branch.js +5 -16
- package/dist/controllers/bucket.js +184 -0
- package/dist/controllers/build.js +88 -0
- package/dist/controllers/database.js +230 -85
- package/dist/controllers/feedback.js +90 -0
- package/dist/controllers/init.js +814 -0
- package/dist/controllers/project.js +345 -184
- package/dist/controllers/select-prompt-port.js +1 -0
- package/dist/{shell/errors.js → errors.js} +8 -40
- package/dist/lib/agent/cli-command.js +20 -0
- package/dist/lib/agent/constants.js +12 -0
- package/dist/lib/agent/package-manager.js +99 -0
- package/dist/lib/agent/setup-status.js +83 -0
- package/dist/lib/app/app-provider.js +168 -95
- package/dist/lib/app/branch-database-deploy.js +70 -42
- package/dist/lib/app/branch-database.js +39 -20
- package/dist/lib/app/build-settings.js +8 -3
- package/dist/lib/app/build.js +16 -14
- package/dist/lib/app/bun-project.js +1 -1
- package/dist/lib/app/compute-config.js +29 -31
- package/dist/lib/app/deploy-plan.js +1 -0
- package/dist/lib/app/deploy-progress.js +7 -7
- package/dist/lib/app/env-config.js +1 -1
- package/dist/lib/app/env-file.js +2 -2
- package/dist/lib/app/env-vars.js +1 -1
- package/dist/lib/app/local-dev.js +1 -1
- package/dist/lib/app/production-deploy-gate.js +3 -2
- package/dist/lib/bucket/provider.js +139 -0
- package/dist/lib/database/provider.js +235 -17
- package/dist/lib/diagnostics.js +2 -1
- package/dist/lib/feedback.js +15 -0
- package/dist/lib/git/local-branch.js +1 -1
- package/dist/lib/project/interactive-setup.js +6 -5
- package/dist/lib/project/local-pin.js +1 -1
- package/dist/lib/project/provider.js +93 -0
- package/dist/lib/project/resolution.js +11 -8
- package/dist/lib/project/setup.js +9 -6
- package/dist/lib/version.js +3 -2
- package/dist/lib/workspace-id.js +18 -0
- package/dist/payload-B88Qvzxk-CrtTXSOe.js +34 -0
- package/dist/presenters/agent.js +74 -0
- package/dist/presenters/app.js +33 -7
- package/dist/presenters/auth.js +3 -2
- package/dist/presenters/bucket.js +174 -0
- package/dist/presenters/database.js +193 -5
- package/dist/presenters/feedback.js +26 -0
- package/dist/presenters/init.js +30 -0
- package/dist/presenters/project.js +98 -4
- package/dist/shell/cli-command.js +2 -0
- package/dist/shell/command-meta.js +303 -4
- package/dist/shell/command-runner.js +13 -6
- package/dist/shell/help.js +6 -5
- package/dist/shell/output.js +65 -3
- package/dist/shell/prompt.js +12 -5
- package/dist/shell/runtime.js +4 -21
- package/dist/state-dir.js +12 -0
- package/dist/{shell/update-check.js → update-check.js} +6 -5
- package/dist/v8/cli.js +14699 -0
- package/dist/v8/sender.js +192 -0
- package/package.json +21 -13
- package/dist/adapters/mock-api.js +0 -136
- package/dist/lib/app/app-interaction.js +0 -5
- package/dist/shell/command-arguments.js +0 -6
- package/dist/use-cases/auth.js +0 -145
- package/dist/use-cases/branch.js +0 -47
- package/dist/use-cases/create-cli-gateways.js +0 -68
- package/dist/use-cases/project.js +0 -30
package/README.md
CHANGED
|
@@ -76,6 +76,8 @@ The beta package exposes `prisma-cli` so it can coexist with the existing
|
|
|
76
76
|
| `project` | List projects, show the resolved project, and manage project environment variables. |
|
|
77
77
|
| `git` | Connect or disconnect a project from a GitHub repository. |
|
|
78
78
|
| `branch` | List Prisma branches for the resolved project. |
|
|
79
|
+
| `database` | Create, inspect, and remove Prisma Postgres databases and their connection strings. |
|
|
80
|
+
| `bucket` | Create, list, and delete Tigris object-store buckets and their access keys. |
|
|
79
81
|
| `app` | Build, run, deploy, inspect, open, stream logs, promote, roll back, and remove apps. |
|
|
80
82
|
|
|
81
83
|
Common examples:
|
|
@@ -98,24 +100,11 @@ npx prisma-cli app promote <deployment-id>
|
|
|
98
100
|
- Stable command groups, flags, and error codes for scripts and agents.
|
|
99
101
|
- Environment variable values are not printed back to the terminal.
|
|
100
102
|
|
|
101
|
-
### Agent skills
|
|
102
|
-
|
|
103
|
-
For agent-guided Next.js deploys, install the Prisma CLI skill cluster at the
|
|
104
|
-
project level. Match the skill ref to the installed CLI version:
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
npx prisma-cli version
|
|
108
|
-
pnpm dlx skills@latest add prisma/prisma-cli/skills#cli-v<cli-version> --all
|
|
109
|
-
```
|
|
110
|
-
|
|
111
|
-
The skills teach agents how to prepare a Next.js app, run `app deploy`, verify
|
|
112
|
-
the result, and route CLI / Compute feedback to the right Prisma channel.
|
|
113
|
-
|
|
114
103
|
---
|
|
115
104
|
|
|
116
105
|
## Beta notes
|
|
117
106
|
|
|
118
|
-
- Requires Node.js 22.
|
|
107
|
+
- Requires Node.js 22.18 or newer.
|
|
119
108
|
- This is a beta package and may change quickly.
|
|
120
109
|
- Official beta releases publish as `@prisma/cli`.
|
|
121
110
|
- The package binary is `prisma-cli`, not `prisma`, during beta.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
1
|
import path from "node:path";
|
|
2
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
//#region src/adapters/local-state.ts
|
|
4
4
|
const DEFAULT_STATE = {
|
|
5
5
|
auth: null,
|
|
@@ -12,7 +12,8 @@ const DEFAULT_STATE = {
|
|
|
12
12
|
app: {
|
|
13
13
|
selectedByProject: {},
|
|
14
14
|
knownLiveDeploymentByProject: {}
|
|
15
|
-
}
|
|
15
|
+
},
|
|
16
|
+
agent: { setupPromptDismissedAt: null }
|
|
16
17
|
};
|
|
17
18
|
const DEFAULT_STATE_FILE_NAME = "state.json";
|
|
18
19
|
function resolveLocalStateFilePath(stateDir) {
|
|
@@ -43,7 +44,8 @@ var LocalStateStore = class {
|
|
|
43
44
|
app: {
|
|
44
45
|
selectedByProject: parsed.app?.selectedByProject ?? {},
|
|
45
46
|
knownLiveDeploymentByProject: parsed.app?.knownLiveDeploymentByProject ?? {}
|
|
46
|
-
}
|
|
47
|
+
},
|
|
48
|
+
agent: { setupPromptDismissedAt: parsed.agent?.setupPromptDismissedAt ?? null }
|
|
47
49
|
};
|
|
48
50
|
} catch (error) {
|
|
49
51
|
if (error.code === "ENOENT") return structuredClone(DEFAULT_STATE);
|
|
@@ -139,6 +141,15 @@ var LocalStateStore = class {
|
|
|
139
141
|
await this.write(state);
|
|
140
142
|
return state;
|
|
141
143
|
}
|
|
144
|
+
async readAgentSetupPromptDismissedAt() {
|
|
145
|
+
return (await this.read()).agent.setupPromptDismissedAt;
|
|
146
|
+
}
|
|
147
|
+
async setAgentSetupPromptDismissedAt(dismissedAt) {
|
|
148
|
+
const state = await this.read();
|
|
149
|
+
state.agent.setupPromptDismissedAt = dismissedAt;
|
|
150
|
+
await this.write(state);
|
|
151
|
+
return state;
|
|
152
|
+
}
|
|
142
153
|
};
|
|
143
154
|
//#endregion
|
|
144
155
|
export { LocalStateStore, resolveLocalStateFilePath };
|
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import os from "node:os";
|
|
3
|
-
//#region src/
|
|
3
|
+
//#region src/auth/client.ts
|
|
4
4
|
const CLIENT_ID = "cmm3lndn701oo0uefvxzo0ivw";
|
|
5
|
-
const SERVICE_TOKEN_ENV_VAR = "PRISMA_SERVICE_TOKEN";
|
|
6
5
|
const AUTH_FILE_ENV_VAR = "PRISMA_COMPUTE_AUTH_FILE";
|
|
6
|
+
/**
|
|
7
|
+
* The SDK's config demands a redirect URI even for a client that only
|
|
8
|
+
* ever makes API calls with tokens it already has. Port 0 is the
|
|
9
|
+
* honest value: no browser is ever sent here, and nothing listens.
|
|
10
|
+
*/
|
|
11
|
+
const UNUSED_REDIRECT_URI = "http://localhost:0/auth/callback";
|
|
7
12
|
function getApiBaseUrl(env = process.env) {
|
|
8
13
|
return env.PRISMA_MANAGEMENT_API_URL?.trim() || "https://api.prisma.io";
|
|
9
14
|
}
|
|
10
15
|
function getAuthFilePath(env = process.env) {
|
|
11
16
|
const configured = env[AUTH_FILE_ENV_VAR];
|
|
12
17
|
if (configured?.trim()) return path.resolve(configured);
|
|
18
|
+
return defaultAuthFilePath(env);
|
|
19
|
+
}
|
|
20
|
+
function defaultAuthFilePath(env = process.env) {
|
|
13
21
|
if (process.platform === "darwin") return path.join(os.homedir(), "Library", "Application Support", "prisma", "auth.json");
|
|
14
22
|
if (process.platform === "win32") {
|
|
15
23
|
const appData = env.APPDATA ?? path.join(os.homedir(), "AppData", "Roaming");
|
|
@@ -19,4 +27,4 @@ function getAuthFilePath(env = process.env) {
|
|
|
19
27
|
return path.join(xdgConfigHome, "prisma", "auth.json");
|
|
20
28
|
}
|
|
21
29
|
//#endregion
|
|
22
|
-
export { CLIENT_ID,
|
|
30
|
+
export { CLIENT_ID, UNUSED_REDIRECT_URI, getApiBaseUrl, getAuthFilePath };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { CliError } from "../errors.js";
|
|
2
|
+
//#region src/auth/errors.ts
|
|
3
|
+
/**
|
|
4
|
+
* Auth-specific error constructors, owned by the auth module. The
|
|
5
|
+
* CliError base class still lives in the legacy shell (`shell/errors`);
|
|
6
|
+
* it is the one remaining legacy dependency of this module and is
|
|
7
|
+
* named as an S2d survivor until the operations layer throws structured
|
|
8
|
+
* errors directly. The shell re-exports these constructors so legacy
|
|
9
|
+
* imports keep working.
|
|
10
|
+
*/
|
|
11
|
+
function workspaceSwitchUnavailableError() {
|
|
12
|
+
return new CliError({
|
|
13
|
+
code: "WORKSPACE_SWITCH_UNAVAILABLE",
|
|
14
|
+
domain: "auth",
|
|
15
|
+
summary: "Workspace switching is unavailable",
|
|
16
|
+
why: "PRISMA_SERVICE_TOKEN is set, so authenticated commands use that token instead of local OAuth workspaces.",
|
|
17
|
+
fix: "Unset PRISMA_SERVICE_TOKEN to switch between local OAuth workspaces, or use a token for the workspace you want.",
|
|
18
|
+
exitCode: 1,
|
|
19
|
+
nextSteps: ["unset PRISMA_SERVICE_TOKEN", "prisma-cli auth workspace list"]
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function workspaceNotAuthenticatedError(workspaceRef) {
|
|
23
|
+
return new CliError({
|
|
24
|
+
code: "WORKSPACE_NOT_AUTHENTICATED",
|
|
25
|
+
domain: "auth",
|
|
26
|
+
summary: "Workspace is not authenticated",
|
|
27
|
+
why: `No stored OAuth session matched "${workspaceRef}".`,
|
|
28
|
+
fix: "Run prisma-cli auth login and authorize that workspace, then switch to it.",
|
|
29
|
+
meta: { workspaceRef },
|
|
30
|
+
exitCode: 1,
|
|
31
|
+
nextSteps: ["prisma-cli auth workspace list", "prisma-cli auth login"]
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
function workspaceAmbiguousError(workspaceRef, matches) {
|
|
35
|
+
return new CliError({
|
|
36
|
+
code: "WORKSPACE_AMBIGUOUS",
|
|
37
|
+
domain: "auth",
|
|
38
|
+
summary: "Workspace name is ambiguous",
|
|
39
|
+
why: `Multiple authenticated workspaces matched "${workspaceRef}".`,
|
|
40
|
+
fix: "Run prisma-cli auth workspace list and switch by workspace id.",
|
|
41
|
+
meta: {
|
|
42
|
+
workspaceRef,
|
|
43
|
+
matches
|
|
44
|
+
},
|
|
45
|
+
exitCode: 2,
|
|
46
|
+
nextSteps: ["prisma-cli auth workspace list"]
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { workspaceAmbiguousError, workspaceNotAuthenticatedError, workspaceSwitchUnavailableError };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { CLIENT_ID,
|
|
2
|
-
import { FileTokenStorage } from "
|
|
1
|
+
import { CLIENT_ID, UNUSED_REDIRECT_URI, getApiBaseUrl } from "./client.js";
|
|
2
|
+
import { FileTokenStorage } from "./token-storage.js";
|
|
3
3
|
import { createManagementApiClient, createManagementApiSdk } from "@prisma/management-api-sdk";
|
|
4
|
-
|
|
4
|
+
import { SERVICE_TOKEN_ENV_VAR } from "@prisma/cli-engine";
|
|
5
|
+
//#region src/auth/guard.ts
|
|
5
6
|
/**
|
|
6
7
|
* Resolve authentication and return a ManagementApiClient.
|
|
7
8
|
*
|
|
@@ -11,7 +12,7 @@ import { createManagementApiClient, createManagementApiSdk } from "@prisma/manag
|
|
|
11
12
|
*
|
|
12
13
|
* Returns null if not authenticated.
|
|
13
14
|
*/
|
|
14
|
-
async function
|
|
15
|
+
async function authenticatedManagementApiClient(env = process.env, signal) {
|
|
15
16
|
signal?.throwIfAborted();
|
|
16
17
|
const rawToken = env[SERVICE_TOKEN_ENV_VAR];
|
|
17
18
|
if (rawToken !== void 0) {
|
|
@@ -29,10 +30,10 @@ async function requireComputeAuth(env = process.env, signal) {
|
|
|
29
30
|
if (!await tokenStorage.getTokens()) return null;
|
|
30
31
|
return createManagementApiSdk({
|
|
31
32
|
clientId: CLIENT_ID,
|
|
32
|
-
redirectUri:
|
|
33
|
+
redirectUri: UNUSED_REDIRECT_URI,
|
|
33
34
|
tokenStorage,
|
|
34
35
|
apiBaseUrl: getApiBaseUrl(env)
|
|
35
36
|
}).client;
|
|
36
37
|
}
|
|
37
38
|
//#endregion
|
|
38
|
-
export {
|
|
39
|
+
export { authenticatedManagementApiClient };
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { getApiBaseUrl } from "./client.js";
|
|
2
|
-
import { FileTokenStorage } from "
|
|
3
|
-
import open from "open";
|
|
2
|
+
import { FileTokenStorage } from "./token-storage.js";
|
|
4
3
|
import { AuthError, createManagementApiSdk } from "@prisma/management-api-sdk";
|
|
5
4
|
import events from "node:events";
|
|
6
5
|
import http from "node:http";
|
|
7
6
|
import readline from "node:readline/promises";
|
|
8
|
-
|
|
7
|
+
import open from "open";
|
|
8
|
+
//#region src/auth/login.ts
|
|
9
9
|
var AuthError$1 = class extends Error {
|
|
10
10
|
constructor(message) {
|
|
11
11
|
super(message);
|
|
@@ -33,6 +33,7 @@ async function login(options = {}) {
|
|
|
33
33
|
apiBaseUrl: options.apiBaseUrl,
|
|
34
34
|
authBaseUrl: options.authBaseUrl,
|
|
35
35
|
openUrl: options.openUrl,
|
|
36
|
+
onVerificationUrl: options.onVerificationUrl,
|
|
36
37
|
env: options.env,
|
|
37
38
|
signal: options.signal,
|
|
38
39
|
output
|
|
@@ -78,8 +79,8 @@ async function login(options = {}) {
|
|
|
78
79
|
settle(resolve);
|
|
79
80
|
} catch (error) {
|
|
80
81
|
res.statusCode = 400;
|
|
81
|
-
|
|
82
|
-
res.end(
|
|
82
|
+
res.setHeader("Content-Type", "text/plain; charset=utf-8");
|
|
83
|
+
res.end("Sign-in could not be completed. Return to your terminal.");
|
|
83
84
|
settle(() => reject(error));
|
|
84
85
|
return;
|
|
85
86
|
}
|
|
@@ -174,6 +175,9 @@ var LoginState = class {
|
|
|
174
175
|
});
|
|
175
176
|
this.latestState = state;
|
|
176
177
|
this.latestVerifier = verifier;
|
|
178
|
+
try {
|
|
179
|
+
this.options.onVerificationUrl?.(url);
|
|
180
|
+
} catch {}
|
|
177
181
|
this.options.signal?.throwIfAborted();
|
|
178
182
|
if (interactive) this.printLoginInstructions(url);
|
|
179
183
|
try {
|
|
@@ -242,6 +246,9 @@ function renderSuccessPage(workspaceName) {
|
|
|
242
246
|
--foreground: #1f2430;
|
|
243
247
|
--muted: #4f5665;
|
|
244
248
|
--mark-color: #050812;
|
|
249
|
+
--surface: #f6f7fb;
|
|
250
|
+
--border: #e4e7ee;
|
|
251
|
+
--success: #15803d;
|
|
245
252
|
}
|
|
246
253
|
|
|
247
254
|
@media (prefers-color-scheme: dark) {
|
|
@@ -250,6 +257,9 @@ function renderSuccessPage(workspaceName) {
|
|
|
250
257
|
--foreground: #f6f7fb;
|
|
251
258
|
--muted: #c5cad6;
|
|
252
259
|
--mark-color: #ffffff;
|
|
260
|
+
--surface: #0d1322;
|
|
261
|
+
--border: #232a3d;
|
|
262
|
+
--success: #4ade80;
|
|
253
263
|
}
|
|
254
264
|
}
|
|
255
265
|
|
|
@@ -303,6 +313,93 @@ function renderSuccessPage(workspaceName) {
|
|
|
303
313
|
line-height: 1.55;
|
|
304
314
|
letter-spacing: 0;
|
|
305
315
|
}
|
|
316
|
+
|
|
317
|
+
.skills {
|
|
318
|
+
margin-top: 40px;
|
|
319
|
+
padding-top: 28px;
|
|
320
|
+
border-top: 1px solid var(--border);
|
|
321
|
+
text-align: left;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
.skills-lead {
|
|
325
|
+
display: flex;
|
|
326
|
+
align-items: center;
|
|
327
|
+
gap: 8px;
|
|
328
|
+
margin: 0 0 12px;
|
|
329
|
+
font-size: 15px;
|
|
330
|
+
color: var(--foreground);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
.skills-lead svg {
|
|
334
|
+
flex: none;
|
|
335
|
+
color: var(--muted);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.command {
|
|
339
|
+
display: flex;
|
|
340
|
+
align-items: center;
|
|
341
|
+
gap: 8px;
|
|
342
|
+
padding: 10px 8px 10px 16px;
|
|
343
|
+
background: var(--surface);
|
|
344
|
+
border: 1px solid var(--border);
|
|
345
|
+
border-radius: 8px;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.command code {
|
|
349
|
+
flex: 1;
|
|
350
|
+
overflow-x: auto;
|
|
351
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
352
|
+
font-size: 13.5px;
|
|
353
|
+
white-space: nowrap;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.command .prompt {
|
|
357
|
+
color: var(--muted);
|
|
358
|
+
user-select: none;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.copy {
|
|
362
|
+
flex: none;
|
|
363
|
+
display: grid;
|
|
364
|
+
place-items: center;
|
|
365
|
+
padding: 6px;
|
|
366
|
+
border: 0;
|
|
367
|
+
border-radius: 6px;
|
|
368
|
+
background: transparent;
|
|
369
|
+
color: var(--muted);
|
|
370
|
+
cursor: pointer;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
.copy:hover {
|
|
374
|
+
background: var(--border);
|
|
375
|
+
color: var(--foreground);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
.copy .icon-check,
|
|
379
|
+
.copy.copied .icon-copy {
|
|
380
|
+
display: none;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.copy.copied .icon-check {
|
|
384
|
+
display: block;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
.copy.copied,
|
|
388
|
+
.copy.copied:hover {
|
|
389
|
+
color: var(--success);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
.visually-hidden {
|
|
393
|
+
position: absolute;
|
|
394
|
+
width: 1px;
|
|
395
|
+
height: 1px;
|
|
396
|
+
margin: -1px;
|
|
397
|
+
padding: 0;
|
|
398
|
+
overflow: hidden;
|
|
399
|
+
clip: rect(0 0 0 0);
|
|
400
|
+
white-space: nowrap;
|
|
401
|
+
border: 0;
|
|
402
|
+
}
|
|
306
403
|
</style>
|
|
307
404
|
</head>
|
|
308
405
|
<body>
|
|
@@ -310,7 +407,51 @@ function renderSuccessPage(workspaceName) {
|
|
|
310
407
|
<main>
|
|
311
408
|
<h1>You're all set.</h1>
|
|
312
409
|
<p>${workspaceName ? `Your terminal is now connected to your ${escapeHtml(workspaceName)} workspace. Head back to your terminal to continue.` : "Your terminal is now connected to your Prisma workspace. Head back to your terminal to continue."}</p>
|
|
410
|
+
<section class="skills">
|
|
411
|
+
<div class="skills-lead">
|
|
412
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/></svg>
|
|
413
|
+
Using an AI coding agent? Add the Prisma skills:
|
|
414
|
+
</div>
|
|
415
|
+
<div class="command">
|
|
416
|
+
<code><span class="prompt">$ </span><span class="command-text">npx skills add prisma/skills</span></code>
|
|
417
|
+
<button class="copy" type="button" aria-label="Copy command to clipboard">
|
|
418
|
+
<svg class="icon-copy" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2" ry="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg>
|
|
419
|
+
<svg class="icon-check" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="20 6 9 17 4 12"/></svg>
|
|
420
|
+
</button>
|
|
421
|
+
</div>
|
|
422
|
+
<span class="visually-hidden skills-status" role="status"></span>
|
|
423
|
+
</section>
|
|
313
424
|
</main>
|
|
425
|
+
<script>
|
|
426
|
+
(() => {
|
|
427
|
+
const command = "npx skills add prisma/skills";
|
|
428
|
+
const button = document.querySelector(".copy");
|
|
429
|
+
const status = document.querySelector(".skills-status");
|
|
430
|
+
let timer;
|
|
431
|
+
button.addEventListener("click", async () => {
|
|
432
|
+
try {
|
|
433
|
+
await navigator.clipboard.writeText(command);
|
|
434
|
+
} catch {
|
|
435
|
+
// Clipboard access denied: select the command so Cmd/Ctrl+C works.
|
|
436
|
+
const range = document.createRange();
|
|
437
|
+
range.selectNodeContents(document.querySelector(".command-text"));
|
|
438
|
+
const selection = window.getSelection();
|
|
439
|
+
selection.removeAllRanges();
|
|
440
|
+
selection.addRange(range);
|
|
441
|
+
status.textContent =
|
|
442
|
+
"Copying failed. The command is selected; press Ctrl+C or Cmd+C to copy it.";
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
button.classList.add("copied");
|
|
446
|
+
status.textContent = "Command copied to clipboard.";
|
|
447
|
+
clearTimeout(timer);
|
|
448
|
+
timer = setTimeout(() => {
|
|
449
|
+
button.classList.remove("copied");
|
|
450
|
+
status.textContent = "";
|
|
451
|
+
}, 2000);
|
|
452
|
+
});
|
|
453
|
+
})();
|
|
454
|
+
<\/script>
|
|
314
455
|
</body>
|
|
315
456
|
</html>`;
|
|
316
457
|
}
|
|
@@ -327,4 +468,4 @@ function escapeHtml(value) {
|
|
|
327
468
|
});
|
|
328
469
|
}
|
|
329
470
|
//#endregion
|
|
330
|
-
export { login };
|
|
471
|
+
export { AuthError$1 as AuthError, login };
|
|
@@ -1,9 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
//#region src/
|
|
1
|
+
import { FileTokenStorage } from "./token-storage.js";
|
|
2
|
+
import { authenticatedManagementApiClient } from "./guard.js";
|
|
3
|
+
import { AuthError, login } from "./login.js";
|
|
4
|
+
import { SERVICE_TOKEN_ENV_VAR, claimedExpiresAt, credentialWorkspaceId } from "@prisma/cli-engine";
|
|
5
|
+
//#region src/auth/operations.ts
|
|
6
6
|
const WORKSPACE_SUB_PREFIX = "workspace:";
|
|
7
|
+
/** Thrown when PRISMA_SERVICE_TOKEN is set to an empty/blank value. */
|
|
8
|
+
var EmptyServiceTokenError = class extends Error {
|
|
9
|
+
code = "EMPTY_SERVICE_TOKEN";
|
|
10
|
+
constructor() {
|
|
11
|
+
super(`${SERVICE_TOKEN_ENV_VAR} is set but empty. Provide a valid token or unset the variable.`);
|
|
12
|
+
this.name = "EmptyServiceTokenError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/** Matches by the code discriminator, not instanceof, so the check
|
|
16
|
+
* survives duplicate module instances. */
|
|
17
|
+
function isEmptyServiceTokenError(error) {
|
|
18
|
+
return typeof error === "object" && error !== null && error.code === "EMPTY_SERVICE_TOKEN";
|
|
19
|
+
}
|
|
7
20
|
function decodeJwtPayload(token) {
|
|
8
21
|
try {
|
|
9
22
|
const payload = token.split(".")[1];
|
|
@@ -24,18 +37,55 @@ function workspaceIdFromClaims(claims) {
|
|
|
24
37
|
const id = sub.slice(10).trim();
|
|
25
38
|
return id.length > 0 ? id : null;
|
|
26
39
|
}
|
|
27
|
-
|
|
40
|
+
/** Holds the tokens the SDK writes at callback time and nothing else:
|
|
41
|
+
* minting and custody stay separate, so a login never writes through
|
|
42
|
+
* the credential manager. */
|
|
43
|
+
var ThrowawayTokenStorage = class {
|
|
44
|
+
tokens = null;
|
|
45
|
+
async getTokens() {
|
|
46
|
+
return this.tokens;
|
|
47
|
+
}
|
|
48
|
+
async setTokens(tokens) {
|
|
49
|
+
this.tokens = tokens;
|
|
50
|
+
}
|
|
51
|
+
async clearTokens() {
|
|
52
|
+
this.tokens = null;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
/** Runs the browser consent flow and RETURNS the minted credential.
|
|
56
|
+
* Storing it is the caller's job. */
|
|
57
|
+
async function performLogin(env, signal, options) {
|
|
58
|
+
const tokenStorage = new ThrowawayTokenStorage();
|
|
28
59
|
await login({
|
|
29
|
-
tokenStorage
|
|
60
|
+
tokenStorage,
|
|
30
61
|
env,
|
|
31
|
-
signal
|
|
62
|
+
signal,
|
|
63
|
+
onVerificationUrl: options?.onVerificationUrl
|
|
64
|
+
});
|
|
65
|
+
const tokens = tokenStorage.tokens;
|
|
66
|
+
if (!tokens) throw new AuthError("Sign-in finished without producing a credential.");
|
|
67
|
+
return {
|
|
68
|
+
token: tokens.accessToken,
|
|
69
|
+
refreshToken: tokens.refreshToken,
|
|
70
|
+
expiresAt: claimedExpiresAt(tokens.accessToken)
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Writes a minted credential into the legacy store, which the legacy
|
|
74
|
+
* shell still reads. Dies with the legacy shell. */
|
|
75
|
+
async function storeLegacyCredential(env, credential, signal) {
|
|
76
|
+
const workspaceId = credentialWorkspaceId(credential.token);
|
|
77
|
+
if (workspaceId === void 0) return;
|
|
78
|
+
await new FileTokenStorage(env, signal, { activateOnSetTokens: true }).setTokens({
|
|
79
|
+
workspaceId,
|
|
80
|
+
accessToken: credential.token,
|
|
81
|
+
...credential.refreshToken === void 0 ? {} : { refreshToken: credential.refreshToken }
|
|
32
82
|
});
|
|
33
83
|
}
|
|
34
84
|
async function readAuthState(env, signal) {
|
|
35
85
|
const rawServiceToken = env[SERVICE_TOKEN_ENV_VAR];
|
|
36
86
|
if (rawServiceToken !== void 0) {
|
|
37
87
|
const serviceToken = rawServiceToken.trim();
|
|
38
|
-
if (serviceToken.length === 0) throw new
|
|
88
|
+
if (serviceToken.length === 0) throw new EmptyServiceTokenError();
|
|
39
89
|
return readServiceTokenAuthState(serviceToken, env, signal);
|
|
40
90
|
}
|
|
41
91
|
const tokenStorage = new FileTokenStorage(env, signal);
|
|
@@ -47,7 +97,7 @@ async function readAuthState(env, signal) {
|
|
|
47
97
|
workspace: null,
|
|
48
98
|
credential: null
|
|
49
99
|
};
|
|
50
|
-
const client = await
|
|
100
|
+
const client = await authenticatedManagementApiClient(env, signal);
|
|
51
101
|
const currentPrincipal = await readCurrentPrincipalAuthState(client, signal);
|
|
52
102
|
if (currentPrincipal) {
|
|
53
103
|
if (currentPrincipal.authenticated && currentPrincipal.workspace) await tokenStorage.rememberWorkspace?.(tokens.workspaceId, currentPrincipal.workspace);
|
|
@@ -65,7 +115,7 @@ async function readAuthState(env, signal) {
|
|
|
65
115
|
return authState;
|
|
66
116
|
}
|
|
67
117
|
async function readServiceTokenAuthState(token, env, signal) {
|
|
68
|
-
const client = await
|
|
118
|
+
const client = await authenticatedManagementApiClient(env, signal);
|
|
69
119
|
const currentPrincipal = await readCurrentPrincipalAuthState(client, signal);
|
|
70
120
|
if (currentPrincipal) return currentPrincipal;
|
|
71
121
|
const claims = decodeJwtPayload(token);
|
|
@@ -88,7 +138,7 @@ async function readServiceTokenAuthState(token, env, signal) {
|
|
|
88
138
|
async function buildAuthState({ workspaceIdFromCredential, claims, env, client, signal }) {
|
|
89
139
|
let workspaceId = workspaceIdFromCredential;
|
|
90
140
|
let workspaceName = workspaceIdFromCredential;
|
|
91
|
-
client ??= await
|
|
141
|
+
client ??= await authenticatedManagementApiClient(env, signal);
|
|
92
142
|
if (client) try {
|
|
93
143
|
const { data, response } = await client.GET("/v1/workspaces/{id}", {
|
|
94
144
|
params: { path: { id: workspaceIdFromCredential } },
|
|
@@ -155,4 +205,4 @@ async function performLogout(env, signal) {
|
|
|
155
205
|
await new FileTokenStorage(env, signal).clearTokens();
|
|
156
206
|
}
|
|
157
207
|
//#endregion
|
|
158
|
-
export { performLogin, performLogout, readAuthState };
|
|
208
|
+
export { isEmptyServiceTokenError, performLogin, performLogout, readAuthState, storeLegacyCredential };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { getApiBaseUrl } from "./client.js";
|
|
2
|
+
import { FileTokenStorage } from "./token-storage.js";
|
|
3
|
+
import { createManagementApiSdk } from "@prisma/management-api-sdk";
|
|
4
|
+
//#region src/auth/recipient.ts
|
|
5
|
+
var RecipientSessionInvalidError = class extends Error {
|
|
6
|
+
constructor(workspaceRef) {
|
|
7
|
+
super(`The stored session for workspace "${workspaceRef}" could not be validated.`);
|
|
8
|
+
this.workspaceRef = workspaceRef;
|
|
9
|
+
this.name = "RecipientSessionInvalidError";
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Resolve a locally stored OAuth workspace session and return a validated
|
|
14
|
+
* access token for it, refreshing through the SDK when the stored token has
|
|
15
|
+
* expired. The active workspace pointer is never touched.
|
|
16
|
+
*
|
|
17
|
+
* Throws WorkspaceSelectionError when the ref does not match exactly one
|
|
18
|
+
* stored session, and RecipientSessionInvalidError when the session cannot
|
|
19
|
+
* be validated or refreshed.
|
|
20
|
+
*/
|
|
21
|
+
async function resolveRecipientWorkspaceSession(workspaceRef, env = process.env, signal) {
|
|
22
|
+
const workspace = await new FileTokenStorage(env, signal).resolveWorkspace(workspaceRef);
|
|
23
|
+
const pinnedStorage = new FileTokenStorage(env, signal, {
|
|
24
|
+
activateOnSetTokens: false,
|
|
25
|
+
lockSetTokens: false,
|
|
26
|
+
pinnedWorkspaceId: workspace.credentialWorkspaceId
|
|
27
|
+
});
|
|
28
|
+
if ((await createManagementApiSdk({
|
|
29
|
+
clientId: "cmm3lndn701oo0uefvxzo0ivw",
|
|
30
|
+
redirectUri: "http://localhost:0/auth/callback",
|
|
31
|
+
tokenStorage: pinnedStorage,
|
|
32
|
+
apiBaseUrl: getApiBaseUrl(env)
|
|
33
|
+
}).client.GET("/v1/workspaces", { signal })).error) throw new RecipientSessionInvalidError(workspaceRef);
|
|
34
|
+
const tokens = await pinnedStorage.getTokens();
|
|
35
|
+
if (!tokens) throw new RecipientSessionInvalidError(workspaceRef);
|
|
36
|
+
return {
|
|
37
|
+
workspace,
|
|
38
|
+
accessToken: tokens.accessToken
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
//#endregion
|
|
42
|
+
export { RecipientSessionInvalidError, resolveRecipientWorkspaceSession };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { getAuthFilePath } from "
|
|
2
|
-
import
|
|
1
|
+
import { getAuthFilePath } from "./client.js";
|
|
2
|
+
import { sameWorkspaceId } from "../lib/workspace-id.js";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import fs from "node:fs/promises";
|
|
4
5
|
import { randomUUID } from "node:crypto";
|
|
5
6
|
import { CredentialsStore } from "@prisma/credentials-store";
|
|
6
|
-
//#region src/
|
|
7
|
+
//#region src/auth/token-storage.ts
|
|
7
8
|
const REFRESH_LOCK_RETRY_MS = 100;
|
|
8
9
|
const REFRESH_LOCK_STALE_MS = 3e4;
|
|
9
10
|
const REFRESH_LOCK_WAIT_TIMEOUT_MS = 25e3;
|
|
@@ -77,6 +78,7 @@ var FileTokenStorage = class {
|
|
|
77
78
|
this.signal?.throwIfAborted();
|
|
78
79
|
try {
|
|
79
80
|
const credentials = await this.readCredentialsFromDisk();
|
|
81
|
+
if (this.options.pinnedWorkspaceId) return findTokensForWorkspace(credentials, this.options.pinnedWorkspaceId);
|
|
80
82
|
const context = await this.readAuthContext();
|
|
81
83
|
if (context.state.activeWorkspaceId) return findTokensForWorkspace(credentials, context.state.activeWorkspaceId);
|
|
82
84
|
const latest = findLatestValidTokens(credentials);
|
|
@@ -138,6 +140,9 @@ var FileTokenStorage = class {
|
|
|
138
140
|
this.signal?.throwIfAborted();
|
|
139
141
|
const credentials = await this.readCredentialsFromDisk();
|
|
140
142
|
const context = await this.ensureMigratedAuthContext(credentials);
|
|
143
|
+
return this.workspacesFromState(credentials, context);
|
|
144
|
+
}
|
|
145
|
+
workspacesFromState(credentials, context) {
|
|
141
146
|
return credentials.map((credential) => storedCredentialToTokens(credential)).filter((tokens) => tokens !== null).map((tokens) => {
|
|
142
147
|
const cached = context.state.workspaces[tokens.workspaceId];
|
|
143
148
|
const id = typeof cached?.id === "string" && cached.id.trim().length > 0 ? cached.id.trim() : tokens.workspaceId;
|
|
@@ -159,6 +164,24 @@ var FileTokenStorage = class {
|
|
|
159
164
|
async useWorkspace(workspaceRef) {
|
|
160
165
|
return this.withRefreshLock(() => this.useWorkspaceUnlocked(workspaceRef));
|
|
161
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Resolve a workspace ref (id, credential workspace id, or cached name)
|
|
169
|
+
* against the locally stored sessions without changing the active
|
|
170
|
+
* workspace. Read-only counterpart of useWorkspace: it reads the auth
|
|
171
|
+
* context as-is and never runs the legacy-state migration, so it writes
|
|
172
|
+
* nothing.
|
|
173
|
+
*/
|
|
174
|
+
async resolveWorkspace(workspaceRef) {
|
|
175
|
+
const ref = workspaceRef.trim();
|
|
176
|
+
if (!ref) throw new WorkspaceSelectionError("missing");
|
|
177
|
+
this.signal?.throwIfAborted();
|
|
178
|
+
const credentials = await this.readCredentialsFromDisk();
|
|
179
|
+
const context = await this.readAuthContext();
|
|
180
|
+
const matches = this.workspacesFromState(credentials, context).filter((workspace) => workspaceMatchesRef(workspace, ref));
|
|
181
|
+
if (matches.length === 0) throw new WorkspaceSelectionError("not-found", ref);
|
|
182
|
+
if (matches.length > 1) throw new WorkspaceSelectionError("ambiguous", ref, matches);
|
|
183
|
+
return matches[0];
|
|
184
|
+
}
|
|
162
185
|
async useWorkspaceUnlocked(workspaceRef) {
|
|
163
186
|
const ref = workspaceRef.trim();
|
|
164
187
|
if (!ref) throw new WorkspaceSelectionError("missing");
|
|
@@ -327,6 +350,7 @@ var FileTokenStorage = class {
|
|
|
327
350
|
await this.writeAuthContext(context.state);
|
|
328
351
|
}
|
|
329
352
|
async maybeActivateWorkspaceId(workspaceId) {
|
|
353
|
+
if (this.options.pinnedWorkspaceId) return;
|
|
330
354
|
const context = await this.readAuthContext();
|
|
331
355
|
if (this.options.activateOnSetTokens === false && context.exists && context.state.activeWorkspaceId && context.state.activeWorkspaceId !== workspaceId) return;
|
|
332
356
|
context.state.activeWorkspaceId = workspaceId;
|
|
@@ -406,10 +430,7 @@ var RefreshLockTimeoutError = class extends Error {
|
|
|
406
430
|
}
|
|
407
431
|
};
|
|
408
432
|
function workspaceMatchesRef(workspace, ref) {
|
|
409
|
-
return
|
|
410
|
-
}
|
|
411
|
-
function stripWorkspacePrefix(value) {
|
|
412
|
-
return value.startsWith("wksp_") ? value.slice(5) : value;
|
|
433
|
+
return sameWorkspaceId(workspace.credentialWorkspaceId, ref) || sameWorkspaceId(workspace.id, ref) || workspace.name.toLowerCase() === ref.toLowerCase();
|
|
413
434
|
}
|
|
414
435
|
function workspaceDisplayName(name, credentialWorkspaceId) {
|
|
415
436
|
return name === credentialWorkspaceId ? UNKNOWN_WORKSPACE_NAME : name;
|