@looma/prisma-cli 0.1.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 +39 -0
- package/dist/adapters/config.js +74 -0
- package/dist/adapters/local-state.js +98 -0
- package/dist/adapters/mock-api.js +57 -0
- package/dist/adapters/token-storage.js +43 -0
- package/dist/cli.js +9 -0
- package/dist/cli2.js +59 -0
- package/dist/commands/app/index.js +178 -0
- package/dist/commands/auth/index.js +42 -0
- package/dist/commands/env/index.js +51 -0
- package/dist/commands/project/index.js +45 -0
- package/dist/controllers/app.js +658 -0
- package/dist/controllers/auth.js +107 -0
- package/dist/controllers/env.js +73 -0
- package/dist/controllers/project.js +214 -0
- package/dist/controllers/select-prompt-port.js +12 -0
- package/dist/lib/app/local-dev.js +178 -0
- package/dist/lib/app/prototype-build.js +109 -0
- package/dist/lib/app/prototype-interaction.js +38 -0
- package/dist/lib/app/prototype-progress.js +115 -0
- package/dist/lib/app/prototype-provider.js +163 -0
- package/dist/lib/auth/auth-ops.js +57 -0
- package/dist/lib/auth/client.js +22 -0
- package/dist/lib/auth/guard.js +34 -0
- package/dist/lib/auth/login.js +117 -0
- package/dist/output/patterns.js +93 -0
- package/dist/presenters/app.js +333 -0
- package/dist/presenters/auth.js +73 -0
- package/dist/presenters/env.js +111 -0
- package/dist/presenters/project.js +84 -0
- package/dist/shell/command-meta.js +294 -0
- package/dist/shell/command-runner.js +33 -0
- package/dist/shell/errors.js +64 -0
- package/dist/shell/global-flags.js +25 -0
- package/dist/shell/help.js +78 -0
- package/dist/shell/output.js +48 -0
- package/dist/shell/prompt.js +31 -0
- package/dist/shell/runtime.js +51 -0
- package/dist/shell/ui.js +59 -0
- package/dist/use-cases/auth.js +70 -0
- package/dist/use-cases/create-cli-gateways.js +93 -0
- package/dist/use-cases/env.js +104 -0
- package/dist/use-cases/project.js +75 -0
- package/package.json +30 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import { UnsafeConfigWriteError, assertLinkedProjectIdWritable, readLinkedProjectId, writeLinkedProjectId } from "../adapters/config.js";
|
|
2
|
+
import { CliError, authRequiredError, featureUnavailableError, usageError } from "../shell/errors.js";
|
|
3
|
+
import { canPrompt } from "../shell/runtime.js";
|
|
4
|
+
import { textPrompt } from "../shell/prompt.js";
|
|
5
|
+
import { requireComputeAuth } from "../lib/auth/guard.js";
|
|
6
|
+
import { DEFAULT_LOCAL_DEV_PORT, resolveLocalBuildType, runLocalApp } from "../lib/app/local-dev.js";
|
|
7
|
+
import { projectNotFoundError } from "../use-cases/project.js";
|
|
8
|
+
import { executePrototypeBuild } from "../lib/app/prototype-build.js";
|
|
9
|
+
import { PROTOTYPE_DEFAULT_REGION, createPrototypeDeployInteraction } from "../lib/app/prototype-interaction.js";
|
|
10
|
+
import { createPrototypeDeployProgress, createPrototypePromoteProgress } from "../lib/app/prototype-progress.js";
|
|
11
|
+
import { createPrototypeAppProvider } from "../lib/app/prototype-provider.js";
|
|
12
|
+
import { createSelectPromptPort } from "./select-prompt-port.js";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import open from "open";
|
|
15
|
+
//#region src/controllers/app.ts
|
|
16
|
+
function isRealMode(context) {
|
|
17
|
+
return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
|
|
18
|
+
}
|
|
19
|
+
async function runAppBuild(context, entrypoint, requestedBuildType) {
|
|
20
|
+
const buildType = normalizeBuildType(requestedBuildType);
|
|
21
|
+
assertSupportedEntrypoint(buildType, entrypoint, "build");
|
|
22
|
+
const resolvedBuildType = await requireLocalBuildType(context, buildType, "build");
|
|
23
|
+
try {
|
|
24
|
+
const { artifact, buildType: actualBuildType } = await executePrototypeBuild({
|
|
25
|
+
appPath: context.runtime.cwd,
|
|
26
|
+
entrypoint,
|
|
27
|
+
buildType: resolvedBuildType
|
|
28
|
+
});
|
|
29
|
+
return {
|
|
30
|
+
command: "app.build",
|
|
31
|
+
result: {
|
|
32
|
+
directory: artifact.directory,
|
|
33
|
+
entrypoint: artifact.entrypoint,
|
|
34
|
+
buildType: actualBuildType
|
|
35
|
+
},
|
|
36
|
+
warnings: [],
|
|
37
|
+
nextSteps: ["prisma app deploy"]
|
|
38
|
+
};
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw buildFailedError("Local app build failed", error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function runAppRun(context, entrypoint, requestedBuildType, requestedPort) {
|
|
44
|
+
if (context.flags.json) throw usageError("App run does not support --json", "This command streams the framework dev server directly and cannot return structured JSON.", "Rerun without --json to pass framework logs through directly.", ["prisma app run"], "app");
|
|
45
|
+
const buildType = normalizeBuildType(requestedBuildType);
|
|
46
|
+
assertSupportedEntrypoint(buildType, entrypoint, "run");
|
|
47
|
+
const port = parseLocalPort(requestedPort);
|
|
48
|
+
const resolvedBuildType = await requireLocalBuildType(context, buildType, "run");
|
|
49
|
+
let runResult;
|
|
50
|
+
try {
|
|
51
|
+
runResult = await runLocalApp({
|
|
52
|
+
appPath: context.runtime.cwd,
|
|
53
|
+
buildType: resolvedBuildType,
|
|
54
|
+
entrypoint,
|
|
55
|
+
port,
|
|
56
|
+
env: context.runtime.env
|
|
57
|
+
});
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw runFailedError("Local app run failed", error);
|
|
60
|
+
}
|
|
61
|
+
if (runResult.signal === "SIGINT" || runResult.signal === "SIGTERM") process.exitCode = runResult.signal === "SIGINT" ? 130 : 143;
|
|
62
|
+
else if (runResult.exitCode !== 0) throw runFailedError("Local app run failed", `The ${formatFrameworkName(runResult.framework)} process exited with code ${runResult.exitCode}.`, runResult.exitCode);
|
|
63
|
+
return {
|
|
64
|
+
command: "app.run",
|
|
65
|
+
result: {
|
|
66
|
+
framework: runResult.framework,
|
|
67
|
+
entrypoint: runResult.entrypoint,
|
|
68
|
+
port: runResult.port,
|
|
69
|
+
command: runResult.command
|
|
70
|
+
},
|
|
71
|
+
warnings: [],
|
|
72
|
+
nextSteps: []
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
async function runAppDeploy(context, appName) {
|
|
76
|
+
ensurePrototypeAppMode(context);
|
|
77
|
+
const provider = await requirePrototypeAppProvider(context);
|
|
78
|
+
const projectId = await resolveProjectIdForDeploy(context, provider);
|
|
79
|
+
const selectedApp = await resolveDeploySelection(context, projectId, await listApps(context, provider, projectId), appName);
|
|
80
|
+
const deployResult = await provider.deployApp({
|
|
81
|
+
cwd: context.runtime.cwd,
|
|
82
|
+
projectId,
|
|
83
|
+
appId: selectedApp.appId,
|
|
84
|
+
appName: selectedApp.appName,
|
|
85
|
+
region: selectedApp.region,
|
|
86
|
+
interaction: selectedApp.useInteractiveSelection ? createPrototypeDeployInteraction(context) : void 0,
|
|
87
|
+
progress: createPrototypeDeployProgress(context.output.stderr, !context.flags.json && !context.flags.quiet)
|
|
88
|
+
}).catch((error) => {
|
|
89
|
+
throw deployFailedError("App deploy failed", error, ["prisma app list-deploys"]);
|
|
90
|
+
});
|
|
91
|
+
await context.stateStore.setSelectedApp(projectId, {
|
|
92
|
+
id: deployResult.app.id,
|
|
93
|
+
name: deployResult.app.name
|
|
94
|
+
});
|
|
95
|
+
await context.stateStore.setKnownLiveDeployment(projectId, deployResult.app.id, deployResult.deployment.id);
|
|
96
|
+
return {
|
|
97
|
+
command: "app.deploy",
|
|
98
|
+
result: {
|
|
99
|
+
projectId: deployResult.projectId,
|
|
100
|
+
app: {
|
|
101
|
+
id: deployResult.app.id,
|
|
102
|
+
name: deployResult.app.name
|
|
103
|
+
},
|
|
104
|
+
deployment: deployResult.deployment
|
|
105
|
+
},
|
|
106
|
+
warnings: [],
|
|
107
|
+
nextSteps: ["prisma app list-deploys", `prisma app show-deploy ${deployResult.deployment.id}`]
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
async function runAppListDeploys(context, appName) {
|
|
111
|
+
ensurePrototypeAppMode(context);
|
|
112
|
+
const projectId = await requireLinkedProjectId(context);
|
|
113
|
+
const provider = await requirePrototypeAppProvider(context);
|
|
114
|
+
const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId), appName);
|
|
115
|
+
if (!selectedApp) return {
|
|
116
|
+
command: "app.list-deploys",
|
|
117
|
+
result: {
|
|
118
|
+
projectId,
|
|
119
|
+
app: null,
|
|
120
|
+
deployments: []
|
|
121
|
+
},
|
|
122
|
+
warnings: [],
|
|
123
|
+
nextSteps: ["prisma app deploy"]
|
|
124
|
+
};
|
|
125
|
+
const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
|
|
126
|
+
throw deployFailedError("Failed to list app deployments", error, ["prisma app deploy"]);
|
|
127
|
+
});
|
|
128
|
+
const currentLiveDeploymentId = await resolveCurrentLiveDeploymentId(context, projectId, deploymentsResult.app, deploymentsResult.deployments);
|
|
129
|
+
const deployments = applyLiveDeploymentHint(deploymentsResult.deployments, currentLiveDeploymentId).slice().sort((left, right) => right.createdAt.localeCompare(left.createdAt) || right.id.localeCompare(left.id));
|
|
130
|
+
await context.stateStore.setSelectedApp(projectId, {
|
|
131
|
+
id: deploymentsResult.app.id,
|
|
132
|
+
name: deploymentsResult.app.name
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
command: "app.list-deploys",
|
|
136
|
+
result: {
|
|
137
|
+
projectId,
|
|
138
|
+
app: {
|
|
139
|
+
id: deploymentsResult.app.id,
|
|
140
|
+
name: deploymentsResult.app.name
|
|
141
|
+
},
|
|
142
|
+
deployments
|
|
143
|
+
},
|
|
144
|
+
warnings: [],
|
|
145
|
+
nextSteps: deployments.length > 0 ? [`prisma app show-deploy ${deployments[0]?.id}`] : ["prisma app deploy"]
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
async function runAppShow(context, appName) {
|
|
149
|
+
ensurePrototypeAppMode(context);
|
|
150
|
+
const projectId = await requireLinkedProjectId(context);
|
|
151
|
+
const provider = await requirePrototypeAppProvider(context);
|
|
152
|
+
const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId), appName);
|
|
153
|
+
if (!selectedApp) return {
|
|
154
|
+
command: "app.show",
|
|
155
|
+
result: {
|
|
156
|
+
projectId,
|
|
157
|
+
app: null,
|
|
158
|
+
liveDeployment: null,
|
|
159
|
+
liveUrl: null,
|
|
160
|
+
recentDeployments: []
|
|
161
|
+
},
|
|
162
|
+
warnings: [],
|
|
163
|
+
nextSteps: ["prisma app deploy"]
|
|
164
|
+
};
|
|
165
|
+
const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
|
|
166
|
+
throw deployFailedError("Failed to inspect app", error, ["prisma app list-deploys"]);
|
|
167
|
+
});
|
|
168
|
+
const currentLiveDeploymentId = await resolveCurrentLiveDeploymentId(context, projectId, deploymentsResult.app, deploymentsResult.deployments);
|
|
169
|
+
const deployments = applyLiveDeploymentHint(deploymentsResult.deployments, currentLiveDeploymentId).slice().sort((left, right) => right.createdAt.localeCompare(left.createdAt) || right.id.localeCompare(left.id));
|
|
170
|
+
const liveDeployment = currentLiveDeploymentId ? deployments.find((deployment) => deployment.id === currentLiveDeploymentId) ?? null : null;
|
|
171
|
+
await context.stateStore.setSelectedApp(projectId, {
|
|
172
|
+
id: deploymentsResult.app.id,
|
|
173
|
+
name: deploymentsResult.app.name
|
|
174
|
+
});
|
|
175
|
+
return {
|
|
176
|
+
command: "app.show",
|
|
177
|
+
result: {
|
|
178
|
+
projectId,
|
|
179
|
+
app: {
|
|
180
|
+
id: deploymentsResult.app.id,
|
|
181
|
+
name: deploymentsResult.app.name
|
|
182
|
+
},
|
|
183
|
+
liveDeployment,
|
|
184
|
+
liveUrl: deploymentsResult.app.liveUrl,
|
|
185
|
+
recentDeployments: deployments.slice(0, 5)
|
|
186
|
+
},
|
|
187
|
+
warnings: [],
|
|
188
|
+
nextSteps: buildAppShowNextSteps(deploymentsResult.app.liveUrl, liveDeployment, deployments)
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
async function runAppShowDeploy(context, deploymentId) {
|
|
192
|
+
ensurePrototypeAppMode(context);
|
|
193
|
+
const deployment = await (await requirePrototypeAppProvider(context)).showDeployment(deploymentId).catch((error) => {
|
|
194
|
+
throw deployFailedError("Failed to show deployment", error, ["prisma app list-deploys"]);
|
|
195
|
+
});
|
|
196
|
+
if (!deployment) throw new CliError({
|
|
197
|
+
code: "DEPLOYMENT_NOT_FOUND",
|
|
198
|
+
domain: "app",
|
|
199
|
+
summary: `Deployment "${deploymentId}" not found`,
|
|
200
|
+
why: "The requested deployment does not exist or is no longer available.",
|
|
201
|
+
fix: "Run prisma app list-deploys to choose an available deployment id.",
|
|
202
|
+
exitCode: 1,
|
|
203
|
+
nextSteps: ["prisma app list-deploys"]
|
|
204
|
+
});
|
|
205
|
+
const linkedProjectId = deployment?.app ? await readLinkedProjectId(context.runtime.cwd) : null;
|
|
206
|
+
const knownLiveDeploymentId = deployment?.app && linkedProjectId ? await context.stateStore.readKnownLiveDeployment(linkedProjectId, deployment.app.id) : null;
|
|
207
|
+
const providerLiveDeploymentId = deployment.app?.liveDeploymentId ?? null;
|
|
208
|
+
return {
|
|
209
|
+
command: "app.show-deploy",
|
|
210
|
+
result: {
|
|
211
|
+
app: deployment.app ? {
|
|
212
|
+
id: deployment.app.id,
|
|
213
|
+
name: deployment.app.name
|
|
214
|
+
} : null,
|
|
215
|
+
deployment: {
|
|
216
|
+
...deployment.deployment,
|
|
217
|
+
live: providerLiveDeploymentId ? deployment.deployment.id === providerLiveDeploymentId : knownLiveDeploymentId ? deployment.deployment.id === knownLiveDeploymentId : deployment.deployment.live
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
warnings: [],
|
|
221
|
+
nextSteps: []
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
async function runAppOpen(context, appName) {
|
|
225
|
+
ensurePrototypeAppMode(context);
|
|
226
|
+
const projectId = await requireLinkedProjectId(context);
|
|
227
|
+
const provider = await requirePrototypeAppProvider(context);
|
|
228
|
+
const selectedApp = await resolveExistingAppSelection(context, projectId, await listApps(context, provider, projectId), appName);
|
|
229
|
+
if (!selectedApp) throw noDeploymentsError("No deployments available to open", "The linked project does not have any deployed app yet.");
|
|
230
|
+
const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
|
|
231
|
+
throw deployFailedError("Failed to resolve app URL", error, ["prisma app show"]);
|
|
232
|
+
});
|
|
233
|
+
const currentLiveDeploymentId = await resolveCurrentLiveDeploymentId(context, projectId, deploymentsResult.app, deploymentsResult.deployments);
|
|
234
|
+
const deployments = applyLiveDeploymentHint(deploymentsResult.deployments, currentLiveDeploymentId).slice().sort((left, right) => right.createdAt.localeCompare(left.createdAt) || right.id.localeCompare(left.id));
|
|
235
|
+
const liveDeployment = currentLiveDeploymentId ? deployments.find((deployment) => deployment.id === currentLiveDeploymentId) ?? null : null;
|
|
236
|
+
await context.stateStore.setSelectedApp(projectId, {
|
|
237
|
+
id: deploymentsResult.app.id,
|
|
238
|
+
name: deploymentsResult.app.name
|
|
239
|
+
});
|
|
240
|
+
if (!liveDeployment) throw noDeploymentsError("No deployments available to open", `The selected app "${deploymentsResult.app.name}" does not have any deployments yet.`);
|
|
241
|
+
if (!deploymentsResult.app.liveUrl) throw featureUnavailableError("Live URL is not available for the selected app", "Deployments exist, but the provider does not expose a stable live service URL for this app yet.", "Run prisma app show to inspect the current deployment state and try again after the app reports a live URL.", ["prisma app show"], "app");
|
|
242
|
+
const shouldOpen = canPrompt(context);
|
|
243
|
+
if (shouldOpen) await open(deploymentsResult.app.liveUrl);
|
|
244
|
+
return {
|
|
245
|
+
command: "app.open",
|
|
246
|
+
result: {
|
|
247
|
+
projectId,
|
|
248
|
+
app: {
|
|
249
|
+
id: deploymentsResult.app.id,
|
|
250
|
+
name: deploymentsResult.app.name
|
|
251
|
+
},
|
|
252
|
+
url: deploymentsResult.app.liveUrl,
|
|
253
|
+
opened: shouldOpen
|
|
254
|
+
},
|
|
255
|
+
warnings: [],
|
|
256
|
+
nextSteps: ["prisma app show", `prisma app show-deploy ${liveDeployment.id}`]
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
async function runAppLogs(context, _appName, _deploymentId) {
|
|
260
|
+
ensurePrototypeAppMode(context);
|
|
261
|
+
throw blockedPrototypeAppCommandError("App logs are not available in the current prototype", "The provider does not yet expose app log access through the Management API.");
|
|
262
|
+
}
|
|
263
|
+
async function runAppPromote(context, deploymentId, appName) {
|
|
264
|
+
ensurePrototypeAppMode(context);
|
|
265
|
+
const projectId = await requireLinkedProjectId(context);
|
|
266
|
+
const provider = await requirePrototypeAppProvider(context);
|
|
267
|
+
const selectedApp = await requireReleaseAppSelection(context, projectId, await listApps(context, provider, projectId), appName, "promote");
|
|
268
|
+
const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
|
|
269
|
+
throw deployFailedError("Failed to list app deployments", error, ["prisma app list-deploys"]);
|
|
270
|
+
});
|
|
271
|
+
const currentLiveDeploymentId = await resolveCurrentLiveDeploymentId(context, projectId, deploymentsResult.app, deploymentsResult.deployments);
|
|
272
|
+
const targetDeployment = requireDeploymentForApp(deploymentsResult.deployments, deploymentId, selectedApp.name);
|
|
273
|
+
const targetAlreadyLive = currentLiveDeploymentId === targetDeployment.id;
|
|
274
|
+
await context.stateStore.setSelectedApp(projectId, {
|
|
275
|
+
id: deploymentsResult.app.id,
|
|
276
|
+
name: deploymentsResult.app.name
|
|
277
|
+
});
|
|
278
|
+
if (!targetAlreadyLive) await provider.promoteDeployment({
|
|
279
|
+
appId: selectedApp.id,
|
|
280
|
+
deploymentId: targetDeployment.id,
|
|
281
|
+
progress: createPrototypePromoteProgress(context.output.stderr, !context.flags.json && !context.flags.quiet)
|
|
282
|
+
}).catch((error) => {
|
|
283
|
+
throw deployFailedError("Failed to promote deployment", error, ["prisma app list-deploys"]);
|
|
284
|
+
});
|
|
285
|
+
await context.stateStore.setKnownLiveDeployment(projectId, deploymentsResult.app.id, targetDeployment.id);
|
|
286
|
+
return {
|
|
287
|
+
command: "app.promote",
|
|
288
|
+
result: {
|
|
289
|
+
projectId,
|
|
290
|
+
app: {
|
|
291
|
+
id: deploymentsResult.app.id,
|
|
292
|
+
name: deploymentsResult.app.name
|
|
293
|
+
},
|
|
294
|
+
deployment: {
|
|
295
|
+
...targetDeployment,
|
|
296
|
+
status: "running",
|
|
297
|
+
live: true
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
warnings: targetAlreadyLive ? ["The selected deployment is already live for this app."] : [],
|
|
301
|
+
nextSteps: ["prisma app list-deploys", `prisma app show-deploy ${targetDeployment.id}`]
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
async function runAppRollback(context, appName, deploymentId) {
|
|
305
|
+
ensurePrototypeAppMode(context);
|
|
306
|
+
const projectId = await requireLinkedProjectId(context);
|
|
307
|
+
const provider = await requirePrototypeAppProvider(context);
|
|
308
|
+
const selectedApp = await requireReleaseAppSelection(context, projectId, await listApps(context, provider, projectId), appName, "rollback");
|
|
309
|
+
const deploymentsResult = await provider.listDeployments(selectedApp.id).catch((error) => {
|
|
310
|
+
throw deployFailedError("Failed to list app deployments", error, ["prisma app list-deploys"]);
|
|
311
|
+
});
|
|
312
|
+
const currentLiveDeploymentId = await resolveCurrentLiveDeploymentId(context, projectId, deploymentsResult.app, deploymentsResult.deployments);
|
|
313
|
+
const currentLiveDeployment = currentLiveDeploymentId ? deploymentsResult.deployments.find((deployment) => deployment.id === currentLiveDeploymentId) ?? null : null;
|
|
314
|
+
const targetDeployment = deploymentId ? requireDeploymentForApp(deploymentsResult.deployments, deploymentId, selectedApp.name) : resolveRollbackTarget(deploymentsResult.deployments, currentLiveDeploymentId);
|
|
315
|
+
const targetAlreadyLive = currentLiveDeploymentId === targetDeployment.id;
|
|
316
|
+
await context.stateStore.setSelectedApp(projectId, {
|
|
317
|
+
id: deploymentsResult.app.id,
|
|
318
|
+
name: deploymentsResult.app.name
|
|
319
|
+
});
|
|
320
|
+
if (!targetAlreadyLive) await provider.promoteDeployment({
|
|
321
|
+
appId: selectedApp.id,
|
|
322
|
+
deploymentId: targetDeployment.id,
|
|
323
|
+
progress: createPrototypePromoteProgress(context.output.stderr, !context.flags.json && !context.flags.quiet)
|
|
324
|
+
}).catch((error) => {
|
|
325
|
+
throw deployFailedError("Failed to roll back deployment", error, ["prisma app list-deploys"]);
|
|
326
|
+
});
|
|
327
|
+
await context.stateStore.setKnownLiveDeployment(projectId, deploymentsResult.app.id, targetDeployment.id);
|
|
328
|
+
return {
|
|
329
|
+
command: "app.rollback",
|
|
330
|
+
result: {
|
|
331
|
+
projectId,
|
|
332
|
+
app: {
|
|
333
|
+
id: deploymentsResult.app.id,
|
|
334
|
+
name: deploymentsResult.app.name
|
|
335
|
+
},
|
|
336
|
+
deployment: {
|
|
337
|
+
...targetDeployment,
|
|
338
|
+
status: "running",
|
|
339
|
+
live: true
|
|
340
|
+
},
|
|
341
|
+
previousLiveDeploymentId: currentLiveDeployment?.id ?? null
|
|
342
|
+
},
|
|
343
|
+
warnings: targetAlreadyLive ? ["The selected deployment is already live for this app."] : [],
|
|
344
|
+
nextSteps: ["prisma app list-deploys", `prisma app show-deploy ${targetDeployment.id}`]
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
async function runAppRemove(context, appName) {
|
|
348
|
+
ensurePrototypeAppMode(context);
|
|
349
|
+
const projectId = await requireLinkedProjectId(context);
|
|
350
|
+
const provider = await requirePrototypeAppProvider(context);
|
|
351
|
+
const selectedApp = await requireReleaseAppSelection(context, projectId, await listApps(context, provider, projectId), appName, "remove");
|
|
352
|
+
await confirmAppRemoval(context, selectedApp);
|
|
353
|
+
const removedApp = await provider.removeApp(selectedApp.id).catch((error) => {
|
|
354
|
+
throw removeFailedError("Failed to remove app", error, ["prisma app show", "prisma app list-deploys"]);
|
|
355
|
+
});
|
|
356
|
+
const warnings = await cleanupRemovedAppState(context, projectId, removedApp.id);
|
|
357
|
+
return {
|
|
358
|
+
command: "app.remove",
|
|
359
|
+
result: {
|
|
360
|
+
projectId,
|
|
361
|
+
app: {
|
|
362
|
+
id: removedApp.id,
|
|
363
|
+
name: removedApp.name
|
|
364
|
+
},
|
|
365
|
+
removed: true
|
|
366
|
+
},
|
|
367
|
+
warnings,
|
|
368
|
+
nextSteps: ["prisma app deploy", "prisma app list-deploys"]
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
async function resolveDeploySelection(context, projectId, apps, explicitAppName) {
|
|
372
|
+
if (explicitAppName) {
|
|
373
|
+
const matched = findAppByName(apps, explicitAppName);
|
|
374
|
+
if (matched) return {
|
|
375
|
+
appId: matched.id,
|
|
376
|
+
useInteractiveSelection: false
|
|
377
|
+
};
|
|
378
|
+
return {
|
|
379
|
+
appName: explicitAppName,
|
|
380
|
+
region: PROTOTYPE_DEFAULT_REGION,
|
|
381
|
+
useInteractiveSelection: false
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const savedSelection = await context.stateStore.readSelectedApp(projectId);
|
|
385
|
+
if (savedSelection) {
|
|
386
|
+
const matched = apps.find((app) => app.id === savedSelection.id) ?? findAppByName(apps, savedSelection.name);
|
|
387
|
+
if (matched) return {
|
|
388
|
+
appId: matched.id,
|
|
389
|
+
useInteractiveSelection: false
|
|
390
|
+
};
|
|
391
|
+
if (!canPrompt(context)) throw usageError("Saved app selection is no longer available", "The locally selected app could not be found in the linked project.", "Pass --app <name>, or rerun prisma app deploy in a TTY to choose or create an app again.", ["prisma app deploy"], "app");
|
|
392
|
+
}
|
|
393
|
+
if (!canPrompt(context)) throw usageError("App deploy requires an app selection in non-interactive mode", "This command cannot choose or create an app in the current mode.", "Pass --app <name>, or rerun prisma app deploy in a TTY to choose or create an app.", ["prisma app deploy --app hello-world"], "app");
|
|
394
|
+
return { useInteractiveSelection: true };
|
|
395
|
+
}
|
|
396
|
+
async function resolveExistingAppSelection(context, projectId, apps, explicitAppName) {
|
|
397
|
+
if (explicitAppName) {
|
|
398
|
+
const matched = findAppByName(apps, explicitAppName);
|
|
399
|
+
if (!matched) throw usageError("Selected app does not exist in the linked project", `The app "${explicitAppName}" could not be found in linked project "${projectId}".`, "Pass the name of an existing app, or rerun prisma app list-deploys in a TTY to choose one.", ["prisma app list-deploys"], "app");
|
|
400
|
+
return matched;
|
|
401
|
+
}
|
|
402
|
+
const savedSelection = await context.stateStore.readSelectedApp(projectId);
|
|
403
|
+
if (savedSelection) {
|
|
404
|
+
const matched = apps.find((app) => app.id === savedSelection.id) ?? findAppByName(apps, savedSelection.name);
|
|
405
|
+
if (matched) return matched;
|
|
406
|
+
if (!canPrompt(context)) throw usageError("Saved app selection is no longer available", "The locally selected app could not be found in the linked project.", "Pass --app <name>, or rerun prisma app list-deploys in a TTY to choose an available app.", ["prisma app list-deploys"], "app");
|
|
407
|
+
}
|
|
408
|
+
if (apps.length === 0) return null;
|
|
409
|
+
if (!canPrompt(context)) throw usageError("App selection required in non-interactive mode", "This command cannot choose an app in the current mode.", "Pass --app <name>, or rerun prisma app list-deploys in a TTY to choose an app.", ["prisma app list-deploys"], "app");
|
|
410
|
+
const selectedId = await createSelectPromptPort(context).select({
|
|
411
|
+
message: "Select an app",
|
|
412
|
+
choices: sortApps(apps).map((app) => ({
|
|
413
|
+
label: app.name,
|
|
414
|
+
value: app.id
|
|
415
|
+
}))
|
|
416
|
+
});
|
|
417
|
+
return apps.find((app) => app.id === selectedId) ?? null;
|
|
418
|
+
}
|
|
419
|
+
async function requireReleaseAppSelection(context, projectId, apps, explicitAppName, commandName) {
|
|
420
|
+
const selectedApp = await resolveExistingAppSelection(context, projectId, apps, explicitAppName);
|
|
421
|
+
if (selectedApp) return selectedApp;
|
|
422
|
+
throw usageError(`App ${commandName} requires an existing app`, "The linked project does not have an app that can be selected for this command.", `Deploy an app first, or rerun prisma app ${commandName} with --app <name> after an app exists.`, ["prisma app deploy", "prisma app list-deploys"], "app");
|
|
423
|
+
}
|
|
424
|
+
async function confirmAppRemoval(context, app) {
|
|
425
|
+
if (context.flags.yes) return;
|
|
426
|
+
if (!canPrompt(context)) throw new CliError({
|
|
427
|
+
code: "CONFIRMATION_REQUIRED",
|
|
428
|
+
domain: "app",
|
|
429
|
+
summary: "App remove requires confirmation in the current mode",
|
|
430
|
+
why: "This command is destructive and cannot prompt for confirmation in the current mode.",
|
|
431
|
+
fix: `Pass --yes to confirm removal of "${app.name}", or rerun prisma app remove in an interactive TTY.`,
|
|
432
|
+
exitCode: 1,
|
|
433
|
+
nextSteps: [`prisma app remove --app ${app.name} --yes`]
|
|
434
|
+
});
|
|
435
|
+
await textPrompt({
|
|
436
|
+
input: context.runtime.stdin,
|
|
437
|
+
output: context.output.stderr,
|
|
438
|
+
message: `Type ${app.name} to confirm app removal`,
|
|
439
|
+
placeholder: app.name,
|
|
440
|
+
validate: (value) => value === app.name ? void 0 : `Type "${app.name}" to confirm removal.`
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
async function cleanupRemovedAppState(context, projectId, appId) {
|
|
444
|
+
const warnings = [];
|
|
445
|
+
try {
|
|
446
|
+
await context.stateStore.clearSelectedApp(projectId, appId);
|
|
447
|
+
} catch (error) {
|
|
448
|
+
warnings.push(localStateCleanupWarning("selected app", error));
|
|
449
|
+
}
|
|
450
|
+
try {
|
|
451
|
+
await context.stateStore.clearKnownLiveDeployment(projectId, appId);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
warnings.push(localStateCleanupWarning("known live deployment", error));
|
|
454
|
+
}
|
|
455
|
+
return warnings;
|
|
456
|
+
}
|
|
457
|
+
function requireDeploymentForApp(deployments, deploymentId, appName) {
|
|
458
|
+
const deployment = deployments.find((candidate) => candidate.id === deploymentId);
|
|
459
|
+
if (deployment) return deployment;
|
|
460
|
+
throw new CliError({
|
|
461
|
+
code: "DEPLOYMENT_NOT_FOUND",
|
|
462
|
+
domain: "app",
|
|
463
|
+
summary: `Deployment "${deploymentId}" not found for app "${appName}"`,
|
|
464
|
+
why: "The requested deployment does not belong to the resolved app or is no longer available.",
|
|
465
|
+
fix: "Run prisma app list-deploys to choose an available deployment id for this app.",
|
|
466
|
+
exitCode: 1,
|
|
467
|
+
nextSteps: ["prisma app list-deploys"]
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
async function resolveCurrentLiveDeploymentId(context, projectId, app, deployments) {
|
|
471
|
+
if (app.liveDeploymentId && deployments.some((deployment) => deployment.id === app.liveDeploymentId)) return app.liveDeploymentId;
|
|
472
|
+
const providerLiveDeployment = deployments.find((deployment) => deployment.live === true);
|
|
473
|
+
if (providerLiveDeployment) return providerLiveDeployment.id;
|
|
474
|
+
const knownLiveDeploymentId = await context.stateStore.readKnownLiveDeployment(projectId, app.id);
|
|
475
|
+
if (knownLiveDeploymentId && deployments.some((deployment) => deployment.id === knownLiveDeploymentId)) return knownLiveDeploymentId;
|
|
476
|
+
return deployments[0]?.id ?? null;
|
|
477
|
+
}
|
|
478
|
+
function buildAppShowNextSteps(liveUrl, liveDeployment, deployments) {
|
|
479
|
+
const nextSteps = [];
|
|
480
|
+
if (liveUrl) nextSteps.push("prisma app open");
|
|
481
|
+
if (liveDeployment) nextSteps.push(`prisma app show-deploy ${liveDeployment.id}`);
|
|
482
|
+
else if (deployments[0]) nextSteps.push(`prisma app show-deploy ${deployments[0].id}`);
|
|
483
|
+
else nextSteps.push("prisma app deploy");
|
|
484
|
+
return nextSteps;
|
|
485
|
+
}
|
|
486
|
+
function applyLiveDeploymentHint(deployments, currentLiveDeploymentId) {
|
|
487
|
+
if (!currentLiveDeploymentId) return deployments.map((deployment) => ({
|
|
488
|
+
...deployment,
|
|
489
|
+
live: deployment.live ?? null
|
|
490
|
+
}));
|
|
491
|
+
return deployments.map((deployment) => ({
|
|
492
|
+
...deployment,
|
|
493
|
+
live: deployment.id === currentLiveDeploymentId
|
|
494
|
+
}));
|
|
495
|
+
}
|
|
496
|
+
function resolveRollbackTarget(deployments, currentLiveDeploymentId) {
|
|
497
|
+
const previousDeployment = deployments.find((deployment) => deployment.id !== currentLiveDeploymentId);
|
|
498
|
+
if (previousDeployment) return previousDeployment;
|
|
499
|
+
throw new CliError({
|
|
500
|
+
code: "NO_PREVIOUS_DEPLOYMENT",
|
|
501
|
+
domain: "app",
|
|
502
|
+
summary: "No previous deployment available for rollback",
|
|
503
|
+
why: "The selected app does not have an earlier deployment to switch back to.",
|
|
504
|
+
fix: "Deploy a second version first, or rerun prisma app rollback --to <deployment-id> for a specific earlier deployment.",
|
|
505
|
+
exitCode: 1,
|
|
506
|
+
nextSteps: ["prisma app deploy", "prisma app list-deploys"]
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
async function listApps(context, provider, projectId) {
|
|
510
|
+
return provider.listApps(projectId).then(sortApps).catch((error) => {
|
|
511
|
+
if (isMissingProjectError(error)) throw projectNotFoundError(`The linked project "${projectId}" does not exist in the authenticated workspace or is no longer accessible.`, "Run prisma project show to inspect the current link, then relink the repo or rerun prisma app deploy to bootstrap a new project.", [
|
|
512
|
+
"prisma project show",
|
|
513
|
+
"prisma project link",
|
|
514
|
+
"prisma app deploy"
|
|
515
|
+
]);
|
|
516
|
+
throw deployFailedError("Failed to list apps", error, ["prisma project show"]);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
async function requirePrototypeAppProvider(context) {
|
|
520
|
+
const client = await requireComputeAuth(context.runtime.env);
|
|
521
|
+
if (!client) throw authRequiredError(["prisma auth login"]);
|
|
522
|
+
return createPrototypeAppProvider(client);
|
|
523
|
+
}
|
|
524
|
+
async function requireLinkedProjectId(context) {
|
|
525
|
+
const projectId = await readLinkedProjectId(context.runtime.cwd);
|
|
526
|
+
if (!projectId) throw new CliError({
|
|
527
|
+
code: "PROJECT_NOT_LINKED",
|
|
528
|
+
domain: "project",
|
|
529
|
+
summary: "Project link required",
|
|
530
|
+
why: "This command needs a linked project for the current repo.",
|
|
531
|
+
fix: "Run prisma project link before deploying or inspecting app deployments.",
|
|
532
|
+
exitCode: 1,
|
|
533
|
+
nextSteps: ["prisma project link"]
|
|
534
|
+
});
|
|
535
|
+
return projectId;
|
|
536
|
+
}
|
|
537
|
+
async function resolveProjectIdForDeploy(context, provider) {
|
|
538
|
+
const linkedProjectId = await readLinkedProjectId(context.runtime.cwd);
|
|
539
|
+
if (linkedProjectId) return linkedProjectId;
|
|
540
|
+
await assertProjectLinkWritableForDeploy(context);
|
|
541
|
+
const projectName = path.basename(context.runtime.cwd);
|
|
542
|
+
const project = await provider.createProject({ name: projectName }).catch((error) => {
|
|
543
|
+
throw deployFailedError("Failed to create project for first deploy", error, ["prisma app deploy"]);
|
|
544
|
+
});
|
|
545
|
+
try {
|
|
546
|
+
await writeLinkedProjectId(context.runtime.cwd, project.id);
|
|
547
|
+
} catch (error) {
|
|
548
|
+
const cause = error instanceof Error ? error.message : String(error);
|
|
549
|
+
throw deployFailedError("Failed to link created project", `Project "${project.name}" (${project.id}) was created remotely but could not be linked locally: ${cause}`, ["prisma project show", "prisma app deploy"]);
|
|
550
|
+
}
|
|
551
|
+
return project.id;
|
|
552
|
+
}
|
|
553
|
+
async function assertProjectLinkWritableForDeploy(context) {
|
|
554
|
+
try {
|
|
555
|
+
await assertLinkedProjectIdWritable(context.runtime.cwd);
|
|
556
|
+
} catch (error) {
|
|
557
|
+
if (error instanceof UnsafeConfigWriteError) throw usageError("Project bootstrap requires a writable Prisma config", error.message, "Update prisma.config.ts to use a recognizable project field, or remove it and rerun prisma app deploy.", ["prisma app deploy --app hello-world"], "app");
|
|
558
|
+
throw error;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
function normalizeBuildType(requestedBuildType) {
|
|
562
|
+
if (!requestedBuildType) return "auto";
|
|
563
|
+
if (requestedBuildType === "auto" || requestedBuildType === "bun" || requestedBuildType === "nextjs") return requestedBuildType;
|
|
564
|
+
throw usageError(`Unsupported build type "${requestedBuildType}"`, "Only auto, bun, and nextjs are supported in the current prototype.", "Pass --build-type auto, --build-type bun, or --build-type nextjs.", ["prisma app build --build-type nextjs", "prisma app build --build-type bun --entry server.ts"], "app");
|
|
565
|
+
}
|
|
566
|
+
function assertSupportedEntrypoint(buildType, entrypoint, commandName) {
|
|
567
|
+
if (buildType === "nextjs" && entrypoint) throw usageError(`App ${commandName} does not accept --entry with --build-type nextjs`, "Next.js build and dev commands do not use an entrypoint flag in this prototype.", `Remove --entry, or rerun prisma app ${commandName} with --build-type bun when you want to target a Bun entrypoint directly.`, [buildType === "nextjs" ? `prisma app ${commandName} --build-type nextjs` : `prisma app ${commandName} --build-type bun --entry server.ts`], "app");
|
|
568
|
+
}
|
|
569
|
+
async function requireLocalBuildType(context, buildType, commandName) {
|
|
570
|
+
const resolvedBuildType = await resolveLocalBuildType(context.runtime.cwd, buildType);
|
|
571
|
+
if (resolvedBuildType) return resolvedBuildType;
|
|
572
|
+
throw usageError(`App ${commandName} requires an explicit framework when detection is ambiguous`, "This prototype only auto-detects clear Next.js or Bun project shapes.", "Pass --build-type nextjs for a Next.js app, or pass --build-type bun with --entry <path> for a Bun app.", [`prisma app ${commandName} --build-type nextjs`, `prisma app ${commandName} --build-type bun --entry server.ts`], "app");
|
|
573
|
+
}
|
|
574
|
+
function parseLocalPort(requestedPort) {
|
|
575
|
+
if (!requestedPort) return DEFAULT_LOCAL_DEV_PORT;
|
|
576
|
+
const port = Number.parseInt(requestedPort, 10);
|
|
577
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535) throw usageError(`Invalid port "${requestedPort}"`, "Port must be an integer between 1 and 65535.", "Pass --port <number> with a valid local port value.", ["prisma app run --port 3000"], "app");
|
|
578
|
+
return port;
|
|
579
|
+
}
|
|
580
|
+
function ensurePrototypeAppMode(context) {
|
|
581
|
+
if (isRealMode(context)) return;
|
|
582
|
+
throw featureUnavailableError("Prototype app commands are only available in real provider mode", "This prototype depends on the live provider integration and does not run against the fixture shell yet.", "Rerun without PRISMA_CLI_MOCK_FIXTURE_PATH to use the provider-backed prototype flow.", ["prisma auth login", "prisma project link"], "app");
|
|
583
|
+
}
|
|
584
|
+
function blockedPrototypeAppCommandError(summary, why) {
|
|
585
|
+
return featureUnavailableError(summary, why, "Use prisma app show, prisma app open, prisma app deploy, or prisma app list-deploys in the current prototype.", ["prisma app show", "prisma app list-deploys"], "app");
|
|
586
|
+
}
|
|
587
|
+
function deployFailedError(summary, error, nextSteps) {
|
|
588
|
+
return new CliError({
|
|
589
|
+
code: "DEPLOY_FAILED",
|
|
590
|
+
domain: "app",
|
|
591
|
+
summary,
|
|
592
|
+
why: error instanceof Error ? error.message : String(error),
|
|
593
|
+
fix: "Retry the command, or rerun with --trace for more detailed diagnostics.",
|
|
594
|
+
exitCode: 1,
|
|
595
|
+
nextSteps
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
function noDeploymentsError(summary, why) {
|
|
599
|
+
return new CliError({
|
|
600
|
+
code: "NO_DEPLOYMENTS",
|
|
601
|
+
domain: "app",
|
|
602
|
+
summary,
|
|
603
|
+
why,
|
|
604
|
+
fix: "Run prisma app deploy first, or use prisma app show to inspect the current app state.",
|
|
605
|
+
exitCode: 1,
|
|
606
|
+
nextSteps: ["prisma app deploy", "prisma app show"]
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
function buildFailedError(summary, error) {
|
|
610
|
+
return new CliError({
|
|
611
|
+
code: "BUILD_FAILED",
|
|
612
|
+
domain: "app",
|
|
613
|
+
summary,
|
|
614
|
+
why: error instanceof Error ? error.message : String(error),
|
|
615
|
+
fix: "Inspect the framework output, fix the build issue, and rerun prisma app build.",
|
|
616
|
+
exitCode: 1,
|
|
617
|
+
nextSteps: ["prisma app build", "prisma app deploy"]
|
|
618
|
+
});
|
|
619
|
+
}
|
|
620
|
+
function runFailedError(summary, error, exitCode = 1) {
|
|
621
|
+
return new CliError({
|
|
622
|
+
code: "RUN_FAILED",
|
|
623
|
+
domain: "app",
|
|
624
|
+
summary,
|
|
625
|
+
why: error instanceof Error ? error.message : String(error),
|
|
626
|
+
fix: "Inspect the framework output above, fix the issue, and rerun prisma app run.",
|
|
627
|
+
exitCode,
|
|
628
|
+
nextSteps: ["prisma app run"]
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
function formatFrameworkName(framework) {
|
|
632
|
+
return framework === "nextjs" ? "Next.js" : "Bun";
|
|
633
|
+
}
|
|
634
|
+
function removeFailedError(summary, error, nextSteps) {
|
|
635
|
+
return new CliError({
|
|
636
|
+
code: "REMOVE_FAILED",
|
|
637
|
+
domain: "app",
|
|
638
|
+
summary,
|
|
639
|
+
why: error instanceof Error ? error.message : String(error),
|
|
640
|
+
fix: "Retry the command, or rerun with --trace for more detailed diagnostics.",
|
|
641
|
+
exitCode: 1,
|
|
642
|
+
nextSteps
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
function localStateCleanupWarning(target, error) {
|
|
646
|
+
return `The app was removed remotely, but the local ${target} state could not be cleared: ${error instanceof Error ? error.message : String(error)}`;
|
|
647
|
+
}
|
|
648
|
+
function isMissingProjectError(error) {
|
|
649
|
+
return error instanceof Error && error.message === "Resource Not Found";
|
|
650
|
+
}
|
|
651
|
+
function findAppByName(apps, name) {
|
|
652
|
+
return apps.find((app) => app.name === name);
|
|
653
|
+
}
|
|
654
|
+
function sortApps(apps) {
|
|
655
|
+
return apps.slice().sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
|
|
656
|
+
}
|
|
657
|
+
//#endregion
|
|
658
|
+
export { runAppBuild, runAppDeploy, runAppListDeploys, runAppLogs, runAppOpen, runAppPromote, runAppRemove, runAppRollback, runAppRun, runAppShow, runAppShowDeploy };
|