@prisma/cli 3.0.0-alpha.9 → 3.0.0-beta.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 +132 -14
- package/dist/adapters/token-storage.js +57 -1
- package/dist/commands/app/index.js +119 -35
- package/dist/commands/env.js +17 -9
- package/dist/commands/project/index.js +28 -2
- package/dist/controllers/app-env.js +156 -32
- package/dist/controllers/app.js +682 -396
- package/dist/controllers/project.js +177 -21
- package/dist/lib/app/domain-guidance.js +14 -0
- package/dist/lib/app/env-config.js +16 -6
- package/dist/lib/app/preview-build.js +50 -5
- package/dist/lib/app/preview-progress.js +1 -25
- package/dist/lib/app/preview-provider.js +99 -1
- package/dist/lib/auth/auth-ops.js +49 -8
- package/dist/lib/project/interactive-setup.js +56 -0
- package/dist/lib/project/resolution.js +124 -96
- package/dist/lib/project/setup.js +88 -0
- package/dist/presenters/app-env.js +4 -3
- package/dist/presenters/app.js +172 -73
- package/dist/presenters/auth.js +19 -6
- package/dist/presenters/project.js +44 -15
- package/dist/shell/command-arguments.js +6 -0
- package/dist/shell/command-meta.js +120 -24
- package/dist/shell/command-runner.js +21 -11
- package/dist/shell/errors.js +4 -1
- package/dist/shell/output.js +3 -1
- package/dist/use-cases/auth.js +15 -4
- package/package.json +4 -4
|
@@ -42,41 +42,52 @@ async function readAuthState(env) {
|
|
|
42
42
|
authenticated: false,
|
|
43
43
|
provider: null,
|
|
44
44
|
user: null,
|
|
45
|
-
workspace: null
|
|
45
|
+
workspace: null,
|
|
46
|
+
credential: null
|
|
46
47
|
};
|
|
48
|
+
const client = await requireComputeAuth(env);
|
|
49
|
+
const currentPrincipal = await readCurrentPrincipalAuthState(client);
|
|
50
|
+
if (currentPrincipal) return currentPrincipal;
|
|
47
51
|
const claims = decodeJwtPayload(tokens.accessToken);
|
|
48
52
|
return buildAuthState({
|
|
49
53
|
workspaceIdFromCredential: tokens.workspaceId,
|
|
50
54
|
claims,
|
|
51
|
-
env
|
|
55
|
+
env,
|
|
56
|
+
client
|
|
52
57
|
});
|
|
53
58
|
}
|
|
54
59
|
async function readServiceTokenAuthState(token, env) {
|
|
60
|
+
const client = await requireComputeAuth(env);
|
|
61
|
+
const currentPrincipal = await readCurrentPrincipalAuthState(client);
|
|
62
|
+
if (currentPrincipal) return currentPrincipal;
|
|
55
63
|
const claims = decodeJwtPayload(token);
|
|
56
64
|
const workspaceId = workspaceIdFromClaims(claims);
|
|
57
65
|
if (!workspaceId) return {
|
|
58
66
|
authenticated: false,
|
|
59
67
|
provider: null,
|
|
60
68
|
user: null,
|
|
61
|
-
workspace: null
|
|
69
|
+
workspace: null,
|
|
70
|
+
credential: null
|
|
62
71
|
};
|
|
63
72
|
return buildAuthState({
|
|
64
73
|
workspaceIdFromCredential: workspaceId,
|
|
65
74
|
claims,
|
|
66
|
-
env
|
|
75
|
+
env,
|
|
76
|
+
client
|
|
67
77
|
});
|
|
68
78
|
}
|
|
69
|
-
async function buildAuthState({ workspaceIdFromCredential, claims, env }) {
|
|
79
|
+
async function buildAuthState({ workspaceIdFromCredential, claims, env, client }) {
|
|
70
80
|
let workspaceId = workspaceIdFromCredential;
|
|
71
81
|
let workspaceName = workspaceIdFromCredential;
|
|
72
|
-
|
|
82
|
+
client ??= await requireComputeAuth(env);
|
|
73
83
|
if (client) try {
|
|
74
84
|
const { data, response } = await client.GET("/v1/workspaces/{id}", { params: { path: { id: workspaceIdFromCredential } } });
|
|
75
85
|
if (response?.status === 401) return {
|
|
76
86
|
authenticated: false,
|
|
77
87
|
provider: null,
|
|
78
88
|
user: null,
|
|
79
|
-
workspace: null
|
|
89
|
+
workspace: null,
|
|
90
|
+
credential: null
|
|
80
91
|
};
|
|
81
92
|
if (data?.data?.id) {
|
|
82
93
|
workspaceId = data.data.id;
|
|
@@ -92,9 +103,39 @@ async function buildAuthState({ workspaceIdFromCredential, claims, env }) {
|
|
|
92
103
|
workspace: {
|
|
93
104
|
id: workspaceId,
|
|
94
105
|
name: workspaceName
|
|
95
|
-
}
|
|
106
|
+
},
|
|
107
|
+
credential: null
|
|
96
108
|
};
|
|
97
109
|
}
|
|
110
|
+
async function readCurrentPrincipalAuthState(client) {
|
|
111
|
+
if (!client) return null;
|
|
112
|
+
try {
|
|
113
|
+
const { data, response } = await client.GET("/v1/me");
|
|
114
|
+
if (response?.status === 401) return {
|
|
115
|
+
authenticated: false,
|
|
116
|
+
provider: null,
|
|
117
|
+
user: null,
|
|
118
|
+
workspace: null,
|
|
119
|
+
credential: null
|
|
120
|
+
};
|
|
121
|
+
const principal = data?.data;
|
|
122
|
+
if (!principal) return null;
|
|
123
|
+
if (!principal.credential) return null;
|
|
124
|
+
return {
|
|
125
|
+
authenticated: true,
|
|
126
|
+
provider: null,
|
|
127
|
+
user: principal.user ? {
|
|
128
|
+
id: principal.user.id,
|
|
129
|
+
email: principal.user.email,
|
|
130
|
+
name: principal.user.name
|
|
131
|
+
} : null,
|
|
132
|
+
workspace: principal.workspace,
|
|
133
|
+
credential: principal.credential
|
|
134
|
+
};
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
98
139
|
async function performLogout(env) {
|
|
99
140
|
await new FileTokenStorage(env).clearTokens();
|
|
100
141
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { usageError } from "../../shell/errors.js";
|
|
2
|
+
import { selectPrompt, textPrompt } from "../../shell/prompt.js";
|
|
3
|
+
import { inferTargetName, sortProjects } from "./resolution.js";
|
|
4
|
+
import { toProjectSummary, validateProjectSetupNameText } from "./setup.js";
|
|
5
|
+
//#region src/lib/project/interactive-setup.ts
|
|
6
|
+
async function promptForProjectSetupChoice(options) {
|
|
7
|
+
const sortedProjects = sortProjects(options.projects);
|
|
8
|
+
const projectNames = sortedProjects.map((project) => project.name);
|
|
9
|
+
const duplicateNames = new Set(projectNames.filter((name, index) => projectNames.indexOf(name) !== index));
|
|
10
|
+
const choice = await selectPrompt({
|
|
11
|
+
input: options.context.runtime.stdin,
|
|
12
|
+
output: options.context.runtime.stderr,
|
|
13
|
+
message: "Which Project should this directory use?",
|
|
14
|
+
choices: [
|
|
15
|
+
...sortedProjects.map((project) => ({
|
|
16
|
+
label: duplicateNames.has(project.name) ? `${project.name} (${project.id})` : project.name,
|
|
17
|
+
value: {
|
|
18
|
+
kind: "project",
|
|
19
|
+
project
|
|
20
|
+
}
|
|
21
|
+
})),
|
|
22
|
+
{
|
|
23
|
+
label: "Create a new Project",
|
|
24
|
+
value: { kind: "create" }
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: "Cancel",
|
|
28
|
+
value: { kind: "cancel" }
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
});
|
|
32
|
+
if (choice.kind === "cancel") throw usageError("Project setup canceled", options.cancel.why, options.cancel.fix, options.cancel.nextSteps, "project");
|
|
33
|
+
if (choice.kind === "project") return {
|
|
34
|
+
project: toProjectSummary(choice.project),
|
|
35
|
+
action: "linked",
|
|
36
|
+
targetName: choice.project.name,
|
|
37
|
+
targetNameSource: "prompt"
|
|
38
|
+
};
|
|
39
|
+
const suggestedName = await inferTargetName(options.context.runtime.cwd);
|
|
40
|
+
const rawName = await textPrompt({
|
|
41
|
+
input: options.context.runtime.stdin,
|
|
42
|
+
output: options.context.runtime.stderr,
|
|
43
|
+
message: "Project name",
|
|
44
|
+
placeholder: suggestedName.name,
|
|
45
|
+
validate: (value) => validateProjectSetupNameText(value, suggestedName.name)
|
|
46
|
+
});
|
|
47
|
+
const projectName = rawName.trim() || suggestedName.name;
|
|
48
|
+
return {
|
|
49
|
+
project: toProjectSummary(await options.createProject(projectName)),
|
|
50
|
+
action: "created",
|
|
51
|
+
targetName: projectName,
|
|
52
|
+
targetNameSource: rawName.trim() ? "prompt" : suggestedName.source
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
//#endregion
|
|
56
|
+
export { promptForProjectSetupChoice };
|
|
@@ -1,73 +1,33 @@
|
|
|
1
1
|
import { CliError } from "../../shell/errors.js";
|
|
2
|
-
import {
|
|
2
|
+
import { formatCommandArgument } from "../../shell/command-arguments.js";
|
|
3
|
+
import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, readLocalResolutionPin } from "./local-pin.js";
|
|
3
4
|
import { readFile } from "node:fs/promises";
|
|
4
5
|
import path from "node:path";
|
|
5
6
|
//#region src/lib/project/resolution.ts
|
|
6
7
|
async function resolveProjectTarget(options) {
|
|
7
8
|
const projects = await options.listProjects();
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const target = await resolveBoundProjectTarget(options, projects, { allowEnvProjectId: true });
|
|
10
|
+
if (target) return target;
|
|
11
|
+
throw await projectSetupRequiredError({
|
|
12
|
+
cwd: options.context.runtime.cwd,
|
|
13
|
+
projects,
|
|
14
|
+
commandName: options.commandName
|
|
12
15
|
});
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
if (rememberedResult.target) return rememberedResult.target;
|
|
19
|
-
staleRemembered = rememberedResult.stale;
|
|
20
|
-
}
|
|
21
|
-
const packageName = inferredName.source === "package-name" ? inferredName.name : null;
|
|
22
|
-
if (packageName) {
|
|
23
|
-
const matches = projects.filter((project) => projectMatchesPackageName(project, packageName));
|
|
24
|
-
if (matches.length === 1) return rememberIfRequested(options, matches[0], "package-name", {
|
|
25
|
-
targetName: packageName,
|
|
26
|
-
targetNameSource: "package-name"
|
|
27
|
-
});
|
|
28
|
-
if (matches.length > 1) return resolveAmbiguousProject(options, matches, packageName, "package-name");
|
|
29
|
-
}
|
|
30
|
-
if (options.allowCreate && options.createProject) {
|
|
31
|
-
if (inferredName.name) {
|
|
32
|
-
const existing = projects.filter((project) => projectMatchesPackageName(project, inferredName.name));
|
|
33
|
-
if (existing.length === 1) return rememberIfRequested(options, existing[0], inferredName.source, {
|
|
34
|
-
targetName: inferredName.name,
|
|
35
|
-
targetNameSource: inferredName.source
|
|
36
|
-
});
|
|
37
|
-
if (existing.length > 1) return resolveAmbiguousProject(options, existing, inferredName.name, inferredName.source);
|
|
38
|
-
return rememberIfRequested(options, await options.createProject(inferredName.name), "created", {
|
|
39
|
-
targetName: inferredName.name,
|
|
40
|
-
targetNameSource: inferredName.source
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
if (options.prompt && canPrompt(options.context) && projects.length > 0) return rememberIfRequested(options, await options.prompt.select({
|
|
45
|
-
message: "Select a project",
|
|
46
|
-
choices: sortProjects(projects).map((project) => ({
|
|
47
|
-
label: `${project.name} (${project.id})`,
|
|
48
|
-
value: project
|
|
49
|
-
}))
|
|
50
|
-
}), "prompt");
|
|
51
|
-
if (staleRemembered && projects.length > 1) throw localStateStaleError();
|
|
52
|
-
throw projectUnresolvedError();
|
|
53
|
-
}
|
|
54
|
-
async function resolveRememberedProject(options, projects) {
|
|
55
|
-
const remembered = await options.context.stateStore.readRememberedProject(options.workspace.id);
|
|
56
|
-
if (!remembered) return {
|
|
57
|
-
target: null,
|
|
58
|
-
stale: false
|
|
59
|
-
};
|
|
60
|
-
const matched = projects.find((project) => project.id === remembered.id);
|
|
61
|
-
if (!matched) return {
|
|
62
|
-
target: null,
|
|
63
|
-
stale: true
|
|
64
|
-
};
|
|
16
|
+
}
|
|
17
|
+
async function inspectProjectBinding(options) {
|
|
18
|
+
const projects = await options.listProjects();
|
|
19
|
+
const target = await resolveBoundProjectTarget(options, projects, { allowEnvProjectId: false });
|
|
20
|
+
if (target) return target;
|
|
65
21
|
return {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
|
|
22
|
+
workspace: options.workspace,
|
|
23
|
+
project: null,
|
|
24
|
+
localBinding: { status: "not-linked" },
|
|
25
|
+
resolution: { projectSource: "unbound" },
|
|
26
|
+
...await buildProjectSetupSuggestion({
|
|
27
|
+
cwd: options.context.runtime.cwd,
|
|
28
|
+
projects,
|
|
29
|
+
commandName: options.commandName ?? "project show"
|
|
30
|
+
})
|
|
71
31
|
};
|
|
72
32
|
}
|
|
73
33
|
function projectNotFoundError(projectRef, workspace) {
|
|
@@ -99,27 +59,77 @@ function projectAmbiguousError(projectRef, matches) {
|
|
|
99
59
|
nextSteps
|
|
100
60
|
});
|
|
101
61
|
}
|
|
102
|
-
function
|
|
62
|
+
function localStateStaleError() {
|
|
103
63
|
return new CliError({
|
|
104
|
-
code: "
|
|
64
|
+
code: "LOCAL_STATE_STALE",
|
|
105
65
|
domain: "project",
|
|
106
|
-
summary: "
|
|
107
|
-
why:
|
|
108
|
-
fix:
|
|
66
|
+
summary: "Local project binding is stale",
|
|
67
|
+
why: `The target recorded in ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH} is no longer available in the selected workspace.`,
|
|
68
|
+
fix: `Delete ${LOCAL_RESOLUTION_PIN_RELATIVE_PATH}, then choose a Project explicitly.`,
|
|
69
|
+
meta: { pinPath: LOCAL_RESOLUTION_PIN_RELATIVE_PATH },
|
|
109
70
|
exitCode: 1,
|
|
110
|
-
nextSteps: ["prisma-cli project list", "prisma-cli project
|
|
71
|
+
nextSteps: ["prisma-cli project list", "prisma-cli project link <id-or-name>"]
|
|
111
72
|
});
|
|
112
73
|
}
|
|
113
|
-
function
|
|
74
|
+
async function buildProjectSetupSuggestion(options) {
|
|
75
|
+
const suggestedName = await inferTargetName(options.cwd);
|
|
76
|
+
const candidates = sortProjects(options.projects.filter((project) => projectMatchesSuggestedName(project, suggestedName.name))).map(toProjectSummary);
|
|
77
|
+
return {
|
|
78
|
+
suggestedProjectName: suggestedName.name,
|
|
79
|
+
suggestedProjectNameSource: suggestedName.source,
|
|
80
|
+
candidates,
|
|
81
|
+
recoveryCommands: buildProjectRecoveryCommands(options.commandName)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
async function projectSetupRequiredError(options) {
|
|
85
|
+
const suggestion = await buildProjectSetupSuggestion(options);
|
|
114
86
|
return new CliError({
|
|
115
|
-
code: "
|
|
87
|
+
code: "PROJECT_SETUP_REQUIRED",
|
|
116
88
|
domain: "project",
|
|
117
|
-
summary: "
|
|
118
|
-
why:
|
|
119
|
-
fix: "
|
|
89
|
+
summary: "Choose a Project before running this command",
|
|
90
|
+
why: `This directory is not linked to a Prisma Project, and ${options.commandName ? `prisma-cli ${options.commandName}` : "this command"} will not choose one from package or directory names.`,
|
|
91
|
+
fix: "Link the directory to an existing Project, or pass --project <id-or-name> for this command.",
|
|
92
|
+
meta: { ...suggestion },
|
|
120
93
|
exitCode: 1,
|
|
121
|
-
nextSteps: ["prisma-cli project list"]
|
|
94
|
+
nextSteps: ["prisma-cli project list", ...suggestion.recoveryCommands],
|
|
95
|
+
nextActions: buildProjectSetupNextActions({
|
|
96
|
+
commandName: options.commandName,
|
|
97
|
+
suggestedProjectName: suggestion.suggestedProjectName
|
|
98
|
+
})
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function buildProjectSetupNextActions(options = {}) {
|
|
102
|
+
const recoveryCommands = buildProjectRecoveryCommands(options.commandName);
|
|
103
|
+
const linkCommand = recoveryCommands[0] ?? "prisma-cli project link <id-or-name>";
|
|
104
|
+
const retryCommand = recoveryCommands[1];
|
|
105
|
+
const actions = [{
|
|
106
|
+
kind: "user-choice",
|
|
107
|
+
journey: "project-setup",
|
|
108
|
+
label: "Ask the user whether to link an existing Project or create a new one",
|
|
109
|
+
commands: ["prisma-cli project list", ...recoveryCommands],
|
|
110
|
+
reason: options.reason ?? "This directory is not linked to a Prisma Project. Package and directory names are suggestions only, not a safe Project selection."
|
|
111
|
+
}, {
|
|
112
|
+
kind: "run-command",
|
|
113
|
+
journey: "project-setup",
|
|
114
|
+
label: "Link the chosen Project",
|
|
115
|
+
command: linkCommand,
|
|
116
|
+
reason: "Linking writes the durable local Project binding for this directory."
|
|
117
|
+
}];
|
|
118
|
+
const createCommand = options.createCommand ?? (options.suggestedProjectName ? `prisma-cli project create ${formatCommandArgument(options.suggestedProjectName)}` : void 0);
|
|
119
|
+
if (createCommand) actions.push({
|
|
120
|
+
kind: "run-command",
|
|
121
|
+
journey: "project-setup",
|
|
122
|
+
label: "Create and link a new Project",
|
|
123
|
+
command: createCommand,
|
|
124
|
+
reason: "Use this when the user wants a new Prisma Project instead of an existing one."
|
|
122
125
|
});
|
|
126
|
+
if (options.commandName) actions.push({
|
|
127
|
+
kind: "run-command",
|
|
128
|
+
journey: "recover",
|
|
129
|
+
label: "Retry with an explicit Project",
|
|
130
|
+
command: retryCommand ?? `prisma-cli ${options.commandName} --project <id-or-name>`
|
|
131
|
+
});
|
|
132
|
+
return actions;
|
|
123
133
|
}
|
|
124
134
|
async function readPackageName(cwd) {
|
|
125
135
|
try {
|
|
@@ -157,33 +167,46 @@ function resolveExplicitProject(projectRef, projects, workspace) {
|
|
|
157
167
|
if (matches.length > 1) throw projectAmbiguousError(projectRef, matches);
|
|
158
168
|
throw projectNotFoundError(projectRef, workspace);
|
|
159
169
|
}
|
|
160
|
-
function
|
|
161
|
-
|
|
162
|
-
message: "Select a project",
|
|
163
|
-
choices: sortProjects(matches).map((project) => ({
|
|
164
|
-
label: `${project.name} (${project.id})`,
|
|
165
|
-
value: project
|
|
166
|
-
}))
|
|
167
|
-
}).then((selected) => rememberIfRequested(options, selected, "prompt", {
|
|
168
|
-
targetName: projectRef,
|
|
169
|
-
targetNameSource
|
|
170
|
-
}));
|
|
171
|
-
throw projectAmbiguousError(projectRef, matches);
|
|
172
|
-
}
|
|
173
|
-
function projectMatchesPackageName(project, packageName) {
|
|
174
|
-
return project.id === packageName || project.name === packageName || project.slug === packageName;
|
|
170
|
+
function projectMatchesSuggestedName(project, suggestedName) {
|
|
171
|
+
return project.id === suggestedName || project.name === suggestedName || project.slug === suggestedName;
|
|
175
172
|
}
|
|
176
173
|
async function resolveDurablePlatformMapping() {
|
|
177
174
|
return null;
|
|
178
175
|
}
|
|
179
|
-
async function
|
|
180
|
-
if (options.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
workspaceId: options.workspace.id
|
|
176
|
+
async function resolveBoundProjectTarget(options, projects, settings) {
|
|
177
|
+
if (options.explicitProject) return resolvedTarget(options.workspace, resolveExplicitProject(options.explicitProject, projects, options.workspace), "explicit", {
|
|
178
|
+
targetName: options.explicitProject,
|
|
179
|
+
targetNameSource: "explicit"
|
|
184
180
|
});
|
|
181
|
+
if (settings.allowEnvProjectId && options.envProjectId) {
|
|
182
|
+
const project = projects.find((candidate) => candidate.id === options.envProjectId);
|
|
183
|
+
if (!project) throw projectNotFoundError(options.envProjectId, options.workspace);
|
|
184
|
+
return resolvedTarget(options.workspace, project, "env", {
|
|
185
|
+
targetName: options.envProjectId,
|
|
186
|
+
targetNameSource: "env"
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
const localPin = await readLocalResolutionPin(options.context.runtime.cwd);
|
|
190
|
+
if (localPin.kind === "invalid") throw localStateStaleError();
|
|
191
|
+
if (localPin.kind === "present") {
|
|
192
|
+
if (localPin.pin.workspaceId !== options.workspace.id) throw localStateStaleError();
|
|
193
|
+
const project = projects.find((candidate) => candidate.id === localPin.pin.projectId);
|
|
194
|
+
if (!project) throw localStateStaleError();
|
|
195
|
+
return resolvedTarget(options.workspace, project, "local-pin", {
|
|
196
|
+
targetName: project.name,
|
|
197
|
+
targetNameSource: "local-pin"
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const platformMapping = await resolveDurablePlatformMapping();
|
|
201
|
+
if (platformMapping && platformMapping.workspace.id === options.workspace.id) return resolvedTarget(options.workspace, platformMapping, "platform-mapping", {
|
|
202
|
+
targetName: platformMapping.name,
|
|
203
|
+
targetNameSource: "platform-mapping"
|
|
204
|
+
});
|
|
205
|
+
return null;
|
|
206
|
+
}
|
|
207
|
+
function resolvedTarget(workspace, project, projectSource, resolutionDetails) {
|
|
185
208
|
return {
|
|
186
|
-
workspace
|
|
209
|
+
workspace,
|
|
187
210
|
project: toProjectSummary(project),
|
|
188
211
|
resolution: {
|
|
189
212
|
projectSource,
|
|
@@ -191,6 +214,11 @@ async function rememberIfRequested(options, project, projectSource, resolutionDe
|
|
|
191
214
|
}
|
|
192
215
|
};
|
|
193
216
|
}
|
|
217
|
+
function buildProjectRecoveryCommands(commandName) {
|
|
218
|
+
const commands = ["prisma-cli project link <id-or-name>"];
|
|
219
|
+
if (commandName) commands.push(`prisma-cli ${commandName} --project <id-or-name>`);
|
|
220
|
+
return commands;
|
|
221
|
+
}
|
|
194
222
|
function toProjectSummary(project) {
|
|
195
223
|
return {
|
|
196
224
|
id: project.id,
|
|
@@ -198,4 +226,4 @@ function toProjectSummary(project) {
|
|
|
198
226
|
};
|
|
199
227
|
}
|
|
200
228
|
//#endregion
|
|
201
|
-
export { inferTargetName, projectNotFoundError, resolveProjectTarget, sortProjects };
|
|
229
|
+
export { buildProjectSetupNextActions, inferTargetName, inspectProjectBinding, projectAmbiguousError, projectNotFoundError, resolveDurablePlatformMapping, resolveProjectTarget, sortProjects };
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { CliError, usageError } from "../../shell/errors.js";
|
|
2
|
+
import "../../shell/command-arguments.js";
|
|
3
|
+
import { LOCAL_RESOLUTION_PIN_RELATIVE_PATH, ensureLocalResolutionPinGitignore, writeLocalResolutionPin } from "./local-pin.js";
|
|
4
|
+
import { projectAmbiguousError, projectNotFoundError } from "./resolution.js";
|
|
5
|
+
//#region src/lib/project/setup.ts
|
|
6
|
+
function isValidProjectSetupName(projectName) {
|
|
7
|
+
return projectName.trim().length > 0;
|
|
8
|
+
}
|
|
9
|
+
function validateProjectSetupNameText(value, fallback) {
|
|
10
|
+
if ((value?.trim() || fallback).trim().length > 0) return;
|
|
11
|
+
return "Enter a Project name.";
|
|
12
|
+
}
|
|
13
|
+
function resolveProjectForSetup(projectRef, projects, workspace) {
|
|
14
|
+
const matches = projects.filter((project) => project.id === projectRef || project.name === projectRef);
|
|
15
|
+
if (matches.length === 1) return matches[0];
|
|
16
|
+
if (matches.length > 1) throw projectAmbiguousError(projectRef, matches);
|
|
17
|
+
throw projectNotFoundError(projectRef, workspace);
|
|
18
|
+
}
|
|
19
|
+
async function bindProjectToDirectory(context, workspace, project, action) {
|
|
20
|
+
await writeLocalResolutionPin(context.runtime.cwd, {
|
|
21
|
+
workspaceId: workspace.id,
|
|
22
|
+
projectId: project.id
|
|
23
|
+
});
|
|
24
|
+
await ensureLocalResolutionPinGitignore(context.runtime.cwd);
|
|
25
|
+
return {
|
|
26
|
+
workspace,
|
|
27
|
+
project,
|
|
28
|
+
directory: formatSetupDirectory(context.runtime.cwd),
|
|
29
|
+
localPin: {
|
|
30
|
+
path: LOCAL_RESOLUTION_PIN_RELATIVE_PATH,
|
|
31
|
+
written: true
|
|
32
|
+
},
|
|
33
|
+
action
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function toProjectSummary(project) {
|
|
37
|
+
return {
|
|
38
|
+
id: project.id,
|
|
39
|
+
name: project.name
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
function projectSetupNameRequiredError(command) {
|
|
43
|
+
return usageError("Project create requires a name", "The project name must be a non-empty value.", "Pass a Project name explicitly.", [`prisma-cli ${command} my-app`], "project");
|
|
44
|
+
}
|
|
45
|
+
function projectCreateFailedError(error, projectName, workspace, options) {
|
|
46
|
+
const status = extractHttpStatus(error);
|
|
47
|
+
if (status === 401 || status === 403) return new CliError({
|
|
48
|
+
code: "PROJECT_CREATE_FAILED",
|
|
49
|
+
domain: "project",
|
|
50
|
+
summary: `Could not create Project "${projectName}"`,
|
|
51
|
+
why: `The platform rejected the Project create in workspace "${workspace.name}" (HTTP ${status}).`,
|
|
52
|
+
fix: options.permissionFix,
|
|
53
|
+
debug: formatDebugDetails(error),
|
|
54
|
+
exitCode: 1,
|
|
55
|
+
nextSteps: options.nextSteps
|
|
56
|
+
});
|
|
57
|
+
return new CliError({
|
|
58
|
+
code: "PROJECT_CREATE_FAILED",
|
|
59
|
+
domain: "project",
|
|
60
|
+
summary: `Could not create Project "${projectName}"`,
|
|
61
|
+
why: error instanceof Error ? error.message : String(error),
|
|
62
|
+
fix: options.fallbackFix,
|
|
63
|
+
debug: formatDebugDetails(error),
|
|
64
|
+
exitCode: 1,
|
|
65
|
+
nextSteps: options.nextSteps
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
function formatSetupDirectory(cwd) {
|
|
69
|
+
const basename = cwd.split(/[\\/]/).filter(Boolean).pop();
|
|
70
|
+
return basename ? `./${basename}` : ".";
|
|
71
|
+
}
|
|
72
|
+
function extractHttpStatus(error) {
|
|
73
|
+
if (!error || typeof error !== "object") return null;
|
|
74
|
+
const candidate = error;
|
|
75
|
+
if (typeof candidate.statusCode === "number") return candidate.statusCode;
|
|
76
|
+
if (typeof candidate.status === "number") return candidate.status;
|
|
77
|
+
if (typeof candidate.message === "string") {
|
|
78
|
+
const match = /\(HTTP (\d{3})\)/.exec(candidate.message);
|
|
79
|
+
if (match) return Number.parseInt(match[1], 10);
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
function formatDebugDetails(error) {
|
|
84
|
+
if (error instanceof Error) return error.stack ?? error.message;
|
|
85
|
+
return typeof error === "string" ? error : null;
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
export { bindProjectToDirectory, isValidProjectSetupName, projectCreateFailedError, projectSetupNameRequiredError, resolveProjectForSetup, toProjectSummary, validateProjectSetupNameText };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { renderList, renderShow, serializeList } from "../output/patterns.js";
|
|
2
2
|
//#region src/presenters/app-env.ts
|
|
3
3
|
function scopeLabel(scope) {
|
|
4
|
-
return scope.role;
|
|
4
|
+
if (scope.kind === "role") return scope.role ?? "unknown";
|
|
5
|
+
return `branch:${scope.branchName ?? scope.branchId ?? "unknown"}`;
|
|
5
6
|
}
|
|
6
7
|
function renderEnvAdd(context, descriptor, result) {
|
|
7
8
|
return renderShow({
|
|
@@ -79,7 +80,7 @@ function renderEnvList(context, descriptor, result) {
|
|
|
79
80
|
},
|
|
80
81
|
items: result.variables.map((variable) => ({
|
|
81
82
|
noun: "variable",
|
|
82
|
-
label: variable.key
|
|
83
|
+
label: `${variable.key} (${variable.source})`,
|
|
83
84
|
id: variable.id,
|
|
84
85
|
status: variable.isManagedBySystem ? "default" : null
|
|
85
86
|
})),
|
|
@@ -94,7 +95,7 @@ function serializeEnvList(result) {
|
|
|
94
95
|
context: { scope: scopeLabel(result.scope) },
|
|
95
96
|
items: result.variables.map((variable) => ({
|
|
96
97
|
noun: "variable",
|
|
97
|
-
label: variable.key
|
|
98
|
+
label: `${variable.key} (${variable.source})`,
|
|
98
99
|
id: variable.id,
|
|
99
100
|
status: variable.isManagedBySystem ? "default" : null
|
|
100
101
|
}))
|