@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.
Files changed (44) hide show
  1. package/README.md +39 -0
  2. package/dist/adapters/config.js +74 -0
  3. package/dist/adapters/local-state.js +98 -0
  4. package/dist/adapters/mock-api.js +57 -0
  5. package/dist/adapters/token-storage.js +43 -0
  6. package/dist/cli.js +9 -0
  7. package/dist/cli2.js +59 -0
  8. package/dist/commands/app/index.js +178 -0
  9. package/dist/commands/auth/index.js +42 -0
  10. package/dist/commands/env/index.js +51 -0
  11. package/dist/commands/project/index.js +45 -0
  12. package/dist/controllers/app.js +658 -0
  13. package/dist/controllers/auth.js +107 -0
  14. package/dist/controllers/env.js +73 -0
  15. package/dist/controllers/project.js +214 -0
  16. package/dist/controllers/select-prompt-port.js +12 -0
  17. package/dist/lib/app/local-dev.js +178 -0
  18. package/dist/lib/app/prototype-build.js +109 -0
  19. package/dist/lib/app/prototype-interaction.js +38 -0
  20. package/dist/lib/app/prototype-progress.js +115 -0
  21. package/dist/lib/app/prototype-provider.js +163 -0
  22. package/dist/lib/auth/auth-ops.js +57 -0
  23. package/dist/lib/auth/client.js +22 -0
  24. package/dist/lib/auth/guard.js +34 -0
  25. package/dist/lib/auth/login.js +117 -0
  26. package/dist/output/patterns.js +93 -0
  27. package/dist/presenters/app.js +333 -0
  28. package/dist/presenters/auth.js +73 -0
  29. package/dist/presenters/env.js +111 -0
  30. package/dist/presenters/project.js +84 -0
  31. package/dist/shell/command-meta.js +294 -0
  32. package/dist/shell/command-runner.js +33 -0
  33. package/dist/shell/errors.js +64 -0
  34. package/dist/shell/global-flags.js +25 -0
  35. package/dist/shell/help.js +78 -0
  36. package/dist/shell/output.js +48 -0
  37. package/dist/shell/prompt.js +31 -0
  38. package/dist/shell/runtime.js +51 -0
  39. package/dist/shell/ui.js +59 -0
  40. package/dist/use-cases/auth.js +70 -0
  41. package/dist/use-cases/create-cli-gateways.js +93 -0
  42. package/dist/use-cases/env.js +104 -0
  43. package/dist/use-cases/project.js +75 -0
  44. package/package.json +30 -0
@@ -0,0 +1,107 @@
1
+ import { authRequiredError, usageError } from "../shell/errors.js";
2
+ import { canPrompt } from "../shell/runtime.js";
3
+ import { createSelectPromptPort } from "./select-prompt-port.js";
4
+ import { createAuthUseCases } from "../use-cases/auth.js";
5
+ import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways.js";
6
+ import { performLogin, performLogout, readAuthState } from "../lib/auth/auth-ops.js";
7
+ //#region src/controllers/auth.ts
8
+ function isRealMode(context) {
9
+ return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
10
+ }
11
+ async function runAuthLogin(context, options) {
12
+ let result;
13
+ if (isRealMode(context)) {
14
+ await performLogin(context.runtime.env);
15
+ result = await readAuthState(context.runtime.env);
16
+ } else result = await loginWithSelectionFlow(context, createAuthUseCases(createCliUseCaseGateways(context)), options);
17
+ return createAuthSuccess("auth.login", result, ["prisma auth whoami", "prisma project list"]);
18
+ }
19
+ async function runAuthLogout(context) {
20
+ let result;
21
+ if (isRealMode(context)) {
22
+ await performLogout(context.runtime.env);
23
+ result = await readAuthState(context.runtime.env);
24
+ } else result = await createAuthUseCases(createCliUseCaseGateways(context)).logout();
25
+ return createAuthSuccess("auth.logout", result, ["prisma auth login"]);
26
+ }
27
+ async function runAuthWhoAmI(context) {
28
+ let result;
29
+ if (isRealMode(context)) result = await readAuthState(context.runtime.env);
30
+ else result = await createAuthUseCases(createCliUseCaseGateways(context)).whoami();
31
+ return createAuthSuccess("auth.whoami", result, result.authenticated ? [] : ["prisma auth login"]);
32
+ }
33
+ async function requireAuthenticatedAuthState(context) {
34
+ if (isRealMode(context)) {
35
+ const current = await readAuthState(context.runtime.env);
36
+ if (current.authenticated) return current;
37
+ if (!canPrompt(context)) throw authRequiredError();
38
+ await performLogin(context.runtime.env);
39
+ return readAuthState(context.runtime.env);
40
+ }
41
+ const useCases = createAuthUseCases(createCliUseCaseGateways(context));
42
+ const current = await useCases.whoami();
43
+ if (current.authenticated) return current;
44
+ if (!canPrompt(context)) throw authRequiredError();
45
+ return loginWithSelectionFlow(context, useCases, {});
46
+ }
47
+ async function loginWithSelectionFlow(context, useCases, options) {
48
+ const selection = await resolveLoginSelection(useCases, canPrompt(context) ? createSelectPromptPort(context) : null, options);
49
+ return useCases.login(selection);
50
+ }
51
+ async function resolveLoginSelection(useCases, prompt, options) {
52
+ const provider = options.provider ? (await useCases.resolveProvider(options.provider)).id : (await selectProvider(useCases, prompt)).id;
53
+ const user = options.user ? await useCases.resolveUserForProvider(provider, options.user) : await selectUser(useCases, prompt, provider);
54
+ const workspace = options.workspace ? await useCases.resolveWorkspaceForUser(user.id, options.workspace) : await selectWorkspace(useCases, prompt, user.id);
55
+ return {
56
+ provider,
57
+ userId: user.id,
58
+ workspaceId: workspace.id
59
+ };
60
+ }
61
+ async function selectProvider(useCases, prompt) {
62
+ if (!prompt) throw nonInteractiveLoginError("Re-run prisma auth login in a TTY, or provide --provider and --user, and --workspace when required.");
63
+ const providers = await useCases.listProviders();
64
+ return prompt.select({
65
+ message: "Select a provider",
66
+ choices: providers.map((provider) => ({
67
+ label: provider.name,
68
+ value: provider
69
+ }))
70
+ });
71
+ }
72
+ async function selectUser(useCases, prompt, provider) {
73
+ const users = await useCases.listUsersForProvider(provider);
74
+ if (!prompt) throw nonInteractiveLoginError("Re-run prisma auth login in a TTY, or provide --provider and --user, and --workspace when required.");
75
+ return prompt.select({
76
+ message: "Select a user",
77
+ choices: users.map((user) => ({
78
+ label: `${user.name} <${user.email}>`,
79
+ value: user
80
+ }))
81
+ });
82
+ }
83
+ async function selectWorkspace(useCases, prompt, userId) {
84
+ const workspaces = await useCases.listWorkspacesForUser(userId);
85
+ if (workspaces.length === 1) return workspaces[0];
86
+ if (!prompt) throw usageError("Login requires explicit selectors in non-interactive mode", "The selected mock user belongs to more than one workspace and the shell cannot prompt in the current mode.", "Re-run prisma auth login in a TTY, or provide --workspace.", ["prisma auth login"], "auth");
87
+ return prompt.select({
88
+ message: "Select a workspace",
89
+ choices: workspaces.map((workspace) => ({
90
+ label: `${workspace.name} (${workspace.id})`,
91
+ value: workspace
92
+ }))
93
+ });
94
+ }
95
+ function nonInteractiveLoginError(fix) {
96
+ return usageError("Login requires explicit selectors in non-interactive mode", "The mock shell cannot prompt in the current mode.", fix, ["prisma auth login"], "auth");
97
+ }
98
+ function createAuthSuccess(command, result, nextSteps) {
99
+ return {
100
+ command,
101
+ result,
102
+ warnings: [],
103
+ nextSteps
104
+ };
105
+ }
106
+ //#endregion
107
+ export { requireAuthenticatedAuthState, runAuthLogin, runAuthLogout, runAuthWhoAmI };
@@ -0,0 +1,73 @@
1
+ import { featureUnavailableError, usageError } from "../shell/errors.js";
2
+ import { canPrompt } from "../shell/runtime.js";
3
+ import { createSelectPromptPort } from "./select-prompt-port.js";
4
+ import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways.js";
5
+ import { createEnvUseCases } from "../use-cases/env.js";
6
+ //#region src/controllers/env.ts
7
+ const PREVIEW_ENVIRONMENT_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
+ function isRealMode(context) {
9
+ return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
10
+ }
11
+ async function runEnvList(context) {
12
+ if (isRealMode(context)) throw envCommandsUnavailableError();
13
+ return {
14
+ command: "env.list",
15
+ result: await createEnvUseCases(createCliUseCaseGateways(context)).list(),
16
+ warnings: [],
17
+ nextSteps: []
18
+ };
19
+ }
20
+ async function runEnvShow(context) {
21
+ if (isRealMode(context)) throw envCommandsUnavailableError();
22
+ const result = await createEnvUseCases(createCliUseCaseGateways(context)).show();
23
+ return {
24
+ command: "env.show",
25
+ result,
26
+ warnings: [],
27
+ nextSteps: result.environment.kind === "preview" && !result.environment.remoteState ? ["prisma app deploy"] : []
28
+ };
29
+ }
30
+ async function runEnvUse(context, environmentName) {
31
+ if (isRealMode(context)) throw envCommandsUnavailableError();
32
+ const useCases = createEnvUseCases(createCliUseCaseGateways(context));
33
+ const resolvedEnvironmentName = await resolveEnvironmentNameForUse(context, useCases, environmentName);
34
+ validateEnvironmentName(resolvedEnvironmentName);
35
+ const result = await useCases.use(resolvedEnvironmentName);
36
+ return {
37
+ command: "env.use",
38
+ result,
39
+ warnings: result.environment.kind === "production" ? ["Production is now the default target."] : [],
40
+ nextSteps: result.environment.kind === "preview" && !result.environment.remoteState ? ["prisma env show", "prisma app deploy"] : ["prisma env show"]
41
+ };
42
+ }
43
+ async function resolveEnvironmentNameForUse(context, useCases, environmentName) {
44
+ if (environmentName) return environmentName;
45
+ if (!canPrompt(context)) throw environmentSelectionRequiredError();
46
+ const result = await useCases.list();
47
+ return createSelectPromptPort(context).select({
48
+ message: "Select an environment",
49
+ choices: result.environments.map((environment) => ({
50
+ label: renderEnvironmentChoiceLabel(environment),
51
+ value: environment.name
52
+ }))
53
+ });
54
+ }
55
+ function renderEnvironmentChoiceLabel(environment) {
56
+ const markers = [];
57
+ if (environment.active) markers.push("active");
58
+ if (environment.kind !== "local" && !environment.remoteState) markers.push("not created yet");
59
+ return markers.length > 0 ? `${environment.name} (${markers.join(", ")})` : environment.name;
60
+ }
61
+ function validateEnvironmentName(environmentName) {
62
+ if (environmentName === "local" || environmentName === "production") return;
63
+ if (PREVIEW_ENVIRONMENT_PATTERN.test(environmentName)) return;
64
+ throw usageError("Environment name must use the documented form", "Environment names must be local, production, or a lowercase preview slug such as preview or feat-auth.", "Use local, production, or a lowercase preview name with letters, numbers, and hyphens.", ["prisma env list"], "env");
65
+ }
66
+ function environmentSelectionRequiredError() {
67
+ return usageError("Environment use requires a target in non-interactive mode", "This command cannot prompt for environment selection in the current mode.", "Re-run prisma env use in a TTY, or pass an environment name explicitly.", ["prisma env list"], "env");
68
+ }
69
+ function envCommandsUnavailableError() {
70
+ return featureUnavailableError("Environment commands are not available in real provider mode", "The current Management API does not expose environment resources, so the CLI cannot resolve or change real environment context yet.", "Use the prototype app deploy flow for real-provider testing, or rerun with PRISMA_CLI_MOCK_FIXTURE_PATH to exercise the documented MVP environment model.", ["prisma app deploy --app <name>"], "env");
71
+ }
72
+ //#endregion
73
+ export { runEnvList, runEnvShow, runEnvUse };
@@ -0,0 +1,214 @@
1
+ import { UnsafeConfigWriteError, readLinkedProjectId, writeLinkedProjectId } from "../adapters/config.js";
2
+ import { CliError, authRequiredError, usageError } from "../shell/errors.js";
3
+ import { canPrompt } from "../shell/runtime.js";
4
+ import { requireComputeAuth } from "../lib/auth/guard.js";
5
+ import { createProjectUseCases, projectNotFoundError } from "../use-cases/project.js";
6
+ import { createSelectPromptPort } from "./select-prompt-port.js";
7
+ import { createAuthUseCases } from "../use-cases/auth.js";
8
+ import { createCliUseCaseGateways } from "../use-cases/create-cli-gateways.js";
9
+ import { readAuthState } from "../lib/auth/auth-ops.js";
10
+ import { requireAuthenticatedAuthState } from "./auth.js";
11
+ //#region src/controllers/project.ts
12
+ function isRealMode(context) {
13
+ return !context.runtime.fixturePath && !context.runtime.env.PRISMA_CLI_MOCK_FIXTURE_PATH;
14
+ }
15
+ async function runProjectList(context) {
16
+ if (isRealMode(context)) {
17
+ const authState = await requireAuthenticatedAuthState(context);
18
+ const client = await requireComputeAuth(context.runtime.env);
19
+ const workspace = authState.workspace;
20
+ if (!client || !workspace) throw authRequiredError();
21
+ const { data: projectsData } = await client.GET("/v1/projects", {});
22
+ return {
23
+ command: "project.list",
24
+ result: {
25
+ workspace,
26
+ linkedProjectId: await readLinkedProjectId(context.runtime.cwd),
27
+ projects: (projectsData?.data ?? []).filter((project) => project.workspace.id === workspace.id).map((project) => ({
28
+ id: project.id,
29
+ name: project.name
30
+ })).sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id))
31
+ },
32
+ warnings: [],
33
+ nextSteps: ["prisma project link"]
34
+ };
35
+ }
36
+ const authState = await requireAuthenticatedAuthState(context);
37
+ return {
38
+ command: "project.list",
39
+ result: await createProjectUseCases(createCliUseCaseGateways(context)).list(authState),
40
+ warnings: [],
41
+ nextSteps: ["prisma project link"]
42
+ };
43
+ }
44
+ async function runProjectShow(context) {
45
+ if (isRealMode(context)) {
46
+ const linkedProjectId = await readLinkedProjectId(context.runtime.cwd);
47
+ if (!linkedProjectId) return {
48
+ command: "project.show",
49
+ result: {
50
+ linkedProjectId: null,
51
+ workspace: null,
52
+ project: null
53
+ },
54
+ warnings: [],
55
+ nextSteps: ["prisma project link"]
56
+ };
57
+ const authState = await readAuthState(context.runtime.env);
58
+ if (!authState.authenticated || !authState.workspace) return {
59
+ command: "project.show",
60
+ result: {
61
+ linkedProjectId,
62
+ workspace: null,
63
+ project: null
64
+ },
65
+ warnings: [],
66
+ nextSteps: ["prisma auth login"]
67
+ };
68
+ const client = await requireComputeAuth(context.runtime.env);
69
+ if (!client) return {
70
+ command: "project.show",
71
+ result: {
72
+ linkedProjectId,
73
+ workspace: null,
74
+ project: null
75
+ },
76
+ warnings: [],
77
+ nextSteps: ["prisma auth login"]
78
+ };
79
+ try {
80
+ const { data } = await client.GET("/v1/projects/{id}", { params: { path: { id: linkedProjectId } } });
81
+ const project = data?.data;
82
+ if (!project || project.workspace.id !== authState.workspace.id) return {
83
+ command: "project.show",
84
+ result: {
85
+ linkedProjectId,
86
+ workspace: null,
87
+ project: null
88
+ },
89
+ warnings: [],
90
+ nextSteps: []
91
+ };
92
+ return {
93
+ command: "project.show",
94
+ result: {
95
+ linkedProjectId,
96
+ workspace: {
97
+ id: project.workspace.id,
98
+ name: project.workspace.name
99
+ },
100
+ project: {
101
+ id: project.id,
102
+ name: project.name
103
+ }
104
+ },
105
+ warnings: [],
106
+ nextSteps: []
107
+ };
108
+ } catch {
109
+ return {
110
+ command: "project.show",
111
+ result: {
112
+ linkedProjectId,
113
+ workspace: null,
114
+ project: null
115
+ },
116
+ warnings: [],
117
+ nextSteps: []
118
+ };
119
+ }
120
+ }
121
+ const gateways = createCliUseCaseGateways(context);
122
+ const authUseCases = createAuthUseCases(gateways);
123
+ const projectUseCases = createProjectUseCases(gateways);
124
+ const authState = await authUseCases.whoami();
125
+ const result = await projectUseCases.show(authState);
126
+ return {
127
+ command: "project.show",
128
+ result,
129
+ warnings: [],
130
+ nextSteps: result.linkedProjectId ? authState.authenticated ? [] : ["prisma auth login"] : ["prisma project link"]
131
+ };
132
+ }
133
+ async function runProjectLink(context, projectId) {
134
+ if (!projectId && !canPrompt(context)) throw projectSelectionRequiredError();
135
+ if (isRealMode(context)) {
136
+ const authState = await requireAuthenticatedAuthState(context);
137
+ const client = await requireComputeAuth(context.runtime.env);
138
+ const workspace = authState.workspace;
139
+ if (!client || !workspace) throw authRequiredError();
140
+ let selectedProject;
141
+ if (projectId) try {
142
+ const { data } = await client.GET("/v1/projects/{id}", { params: { path: { id: projectId } } });
143
+ if (!data?.data || data.data.workspace.id !== workspace.id) throw projectNotFoundError(`The project "${projectId}" does not exist in workspace "${workspace.name}".`, "Run prisma project list and choose a project id from the active workspace.");
144
+ selectedProject = data.data;
145
+ } catch (error) {
146
+ if (error instanceof CliError) throw error;
147
+ throw projectNotFoundError(`The project "${projectId}" does not exist in workspace "${workspace.name}".`, "Run prisma project list and choose a project id from the active workspace.");
148
+ }
149
+ else {
150
+ const { data: projectsData } = await client.GET("/v1/projects", {});
151
+ const projects = (projectsData?.data ?? []).filter((project) => project.workspace.id === workspace.id).map((project) => ({
152
+ id: project.id,
153
+ name: project.name,
154
+ workspace: project.workspace
155
+ })).sort((left, right) => left.name.localeCompare(right.name) || left.id.localeCompare(right.id));
156
+ if (projects.length === 0) throw projectNotFoundError(`No projects are available in workspace "${workspace.name}".`, "Use prisma app deploy to create project context, or switch workspaces and try again.", []);
157
+ selectedProject = await createSelectPromptPort(context).select({
158
+ message: "Select a project",
159
+ choices: projects.map((project) => ({
160
+ label: `${project.name} (${project.id})`,
161
+ value: project
162
+ }))
163
+ });
164
+ }
165
+ try {
166
+ await writeLinkedProjectId(context.runtime.cwd, selectedProject.id);
167
+ } catch (error) {
168
+ if (error instanceof UnsafeConfigWriteError) throw usageError("Project link requires a writable Prisma config", error.message, "Update prisma.config.ts to use a recognizable project field, or remove it and rerun prisma project link.", ["prisma project link proj_123"], "project");
169
+ throw error;
170
+ }
171
+ return {
172
+ command: "project.link",
173
+ result: {
174
+ linkedProjectId: selectedProject.id,
175
+ workspace: {
176
+ id: selectedProject.workspace.id,
177
+ name: selectedProject.workspace.name
178
+ },
179
+ project: {
180
+ id: selectedProject.id,
181
+ name: selectedProject.name
182
+ }
183
+ },
184
+ warnings: [],
185
+ nextSteps: ["prisma project show", "prisma app deploy"]
186
+ };
187
+ }
188
+ const projectUseCases = createProjectUseCases(createCliUseCaseGateways(context));
189
+ const authState = await requireAuthenticatedAuthState(context);
190
+ const resolvedProjectId = projectId ?? await resolveProjectIdForLink(context, authState, projectUseCases);
191
+ return {
192
+ command: "project.link",
193
+ result: await projectUseCases.link(authState, resolvedProjectId),
194
+ warnings: [],
195
+ nextSteps: ["prisma project show", "prisma app deploy"]
196
+ };
197
+ }
198
+ async function resolveProjectIdForLink(context, authState, projectUseCases) {
199
+ if (!authState.workspace) throw projectSelectionRequiredError();
200
+ const projects = await projectUseCases.listProjectsForWorkspace(authState.workspace.id);
201
+ if (projects.length === 0) throw projectNotFoundError(`No projects are available in workspace "${authState.workspace.name}".`, "Use prisma app deploy to create project context, or switch workspaces and try again.", []);
202
+ return (await createSelectPromptPort(context).select({
203
+ message: "Select a project",
204
+ choices: projects.map((project) => ({
205
+ label: `${project.name} (${project.id})`,
206
+ value: project
207
+ }))
208
+ })).id;
209
+ }
210
+ function projectSelectionRequiredError() {
211
+ return usageError("Project link requires a project target in non-interactive mode", "This command cannot prompt for project selection in the current mode.", "Re-run prisma project link in a TTY, or pass a project id explicitly.", ["prisma project list"], "project");
212
+ }
213
+ //#endregion
214
+ export { runProjectLink, runProjectList, runProjectShow };
@@ -0,0 +1,12 @@
1
+ import { selectPrompt } from "../shell/prompt.js";
2
+ //#region src/controllers/select-prompt-port.ts
3
+ function createSelectPromptPort(context) {
4
+ return { select: ({ message, choices }) => selectPrompt({
5
+ input: context.runtime.stdin,
6
+ output: context.runtime.stderr,
7
+ message,
8
+ choices
9
+ }) };
10
+ }
11
+ //#endregion
12
+ export { createSelectPromptPort };
@@ -0,0 +1,178 @@
1
+ import path from "node:path";
2
+ import { access, readFile } from "node:fs/promises";
3
+ import { spawn } from "node:child_process";
4
+ //#region src/lib/app/local-dev.ts
5
+ const NEXT_CONFIG_FILENAMES = [
6
+ "next.config.js",
7
+ "next.config.mjs",
8
+ "next.config.ts",
9
+ "next.config.mts"
10
+ ];
11
+ const DEFAULT_LOCAL_DEV_PORT = 3e3;
12
+ async function resolveLocalBuildType(appPath, buildType) {
13
+ if (buildType === "bun" || buildType === "nextjs") return buildType;
14
+ return detectLocalBuildType(appPath);
15
+ }
16
+ async function detectLocalBuildType(appPath) {
17
+ if (await isNextProject(appPath)) return "nextjs";
18
+ if (await isBunProject(appPath)) return "bun";
19
+ return null;
20
+ }
21
+ async function resolveBunEntrypoint(appPath, explicitEntrypoint) {
22
+ const packageJson = await readPackageJson(appPath);
23
+ const candidate = explicitEntrypoint ?? (typeof packageJson?.main === "string" ? packageJson.main : void 0);
24
+ if (!candidate) throw new Error("Entrypoint is required. Pass --entry or define package.json main.");
25
+ if (path.isAbsolute(candidate)) throw new Error("Entrypoint must be a relative path.");
26
+ const normalized = path.normalize(candidate);
27
+ if (normalized.startsWith("..") || path.isAbsolute(normalized) || normalized.includes(`${path.sep}..${path.sep}`)) throw new Error("Entrypoint must not escape the app directory.");
28
+ const entrypointPath = path.join(appPath, normalized);
29
+ try {
30
+ await access(entrypointPath);
31
+ } catch {
32
+ throw new Error(`Entrypoint file does not exist: ${entrypointPath}`);
33
+ }
34
+ return normalized.split(path.sep).join("/");
35
+ }
36
+ async function runLocalApp(options) {
37
+ const spawnImpl = options.spawnImpl ?? spawn;
38
+ if (options.buildType === "nextjs") {
39
+ const command = await runWithFallback([
40
+ {
41
+ command: path.join(options.appPath, "node_modules", ".bin", process.platform === "win32" ? "next.cmd" : "next"),
42
+ args: [
43
+ "dev",
44
+ "--port",
45
+ String(options.port)
46
+ ],
47
+ display: `next dev --port ${options.port}`
48
+ },
49
+ {
50
+ command: "npx",
51
+ args: [
52
+ "next",
53
+ "dev",
54
+ "--port",
55
+ String(options.port)
56
+ ],
57
+ display: `next dev --port ${options.port}`
58
+ },
59
+ {
60
+ command: "bunx",
61
+ args: [
62
+ "next",
63
+ "dev",
64
+ "--port",
65
+ String(options.port)
66
+ ],
67
+ display: `next dev --port ${options.port}`
68
+ }
69
+ ], {
70
+ cwd: options.appPath,
71
+ env: {
72
+ ...options.env,
73
+ PORT: String(options.port)
74
+ }
75
+ }, spawnImpl, "Could not find the Next.js CLI. Install it with `npm install next` or ensure npx/bunx is available.");
76
+ return {
77
+ framework: "nextjs",
78
+ entrypoint: null,
79
+ port: options.port,
80
+ command: command.display,
81
+ exitCode: command.exitCode,
82
+ signal: command.signal
83
+ };
84
+ }
85
+ const entrypoint = await resolveBunEntrypoint(options.appPath, options.entrypoint);
86
+ const command = await runWithFallback([{
87
+ command: "bun",
88
+ args: ["--watch", entrypoint],
89
+ display: `bun --watch ${entrypoint}`
90
+ }], {
91
+ cwd: options.appPath,
92
+ env: {
93
+ ...options.env,
94
+ PORT: String(options.port)
95
+ }
96
+ }, spawnImpl, "Bun is required to run this app locally. Install it from https://bun.sh.");
97
+ return {
98
+ framework: "bun",
99
+ entrypoint,
100
+ port: options.port,
101
+ command: command.display,
102
+ exitCode: command.exitCode,
103
+ signal: command.signal
104
+ };
105
+ }
106
+ async function isNextProject(appPath) {
107
+ for (const fileName of NEXT_CONFIG_FILENAMES) try {
108
+ await access(path.join(appPath, fileName));
109
+ return true;
110
+ } catch {}
111
+ return hasDependency(await readPackageJson(appPath), "next");
112
+ }
113
+ async function isBunProject(appPath) {
114
+ try {
115
+ await access(path.join(appPath, "bun.lock"));
116
+ return true;
117
+ } catch {}
118
+ try {
119
+ await access(path.join(appPath, "bun.lockb"));
120
+ return true;
121
+ } catch {}
122
+ const packageJson = await readPackageJson(appPath);
123
+ if (!packageJson) return false;
124
+ const hasMain = typeof packageJson.main === "string";
125
+ const hasBunDependency = hasDependency(packageJson, "@types/bun") || hasDependency(packageJson, "bun");
126
+ const usesBunScripts = (typeof packageJson.scripts === "object" && packageJson.scripts !== null ? Object.values(packageJson.scripts) : []).some((value) => typeof value === "string" && /\bbun\b/.test(value));
127
+ return hasMain && (hasBunDependency || usesBunScripts);
128
+ }
129
+ function hasDependency(packageJson, dependencyName) {
130
+ if (!packageJson) return false;
131
+ return [packageJson.dependencies, packageJson.devDependencies].some((group) => typeof group === "object" && group !== null && dependencyName in group);
132
+ }
133
+ async function readPackageJson(appPath) {
134
+ const packageJsonPath = path.join(appPath, "package.json");
135
+ let content;
136
+ try {
137
+ content = await readFile(packageJsonPath, "utf8");
138
+ } catch (error) {
139
+ if (error.code === "ENOENT") return null;
140
+ throw new Error(`Failed to read ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
141
+ }
142
+ try {
143
+ return JSON.parse(content);
144
+ } catch (error) {
145
+ throw new Error(`Failed to parse ${packageJsonPath}: ${error instanceof Error ? error.message : String(error)}`);
146
+ }
147
+ }
148
+ async function runWithFallback(candidates, options, spawnImpl, missingCommandMessage) {
149
+ for (const candidate of candidates) try {
150
+ const result = await spawnCommand(candidate, options, spawnImpl);
151
+ return {
152
+ display: candidate.display,
153
+ exitCode: result.exitCode,
154
+ signal: result.signal
155
+ };
156
+ } catch (error) {
157
+ if (error.code === "ENOENT") continue;
158
+ throw error;
159
+ }
160
+ throw new Error(missingCommandMessage);
161
+ }
162
+ function spawnCommand(candidate, options, spawnImpl) {
163
+ return new Promise((resolve, reject) => {
164
+ const child = spawnImpl(candidate.command, candidate.args, {
165
+ ...options,
166
+ stdio: "inherit"
167
+ });
168
+ child.once("error", reject);
169
+ child.once("exit", (code, signal) => {
170
+ resolve({
171
+ exitCode: code ?? 1,
172
+ signal
173
+ });
174
+ });
175
+ });
176
+ }
177
+ //#endregion
178
+ export { DEFAULT_LOCAL_DEV_PORT, resolveLocalBuildType, runLocalApp };