Skip to content

Commit c131650

Browse files
feat(cli): init offers to install @prisma/compute-sdk for config types
The generated config's typed import resolves at deploy time without a local install, but editors need @prisma/compute-sdk locally for types. After writing the config, init now detects an existing dependency, otherwise offers the detected package manager's add command (pnpm add -D / bun add -d / yarn add -D / npm install -D). --install forces it, --no-install skips, non-interactive skips with the add command in nextSteps, and a failed install downgrades to a visible warning with the exact command; the config write always stands. Also surfaces types/link failures in human output directly, since the runner does not render success warnings in human mode.
1 parent 7008a46 commit c131650

6 files changed

Lines changed: 345 additions & 7 deletions

File tree

docs/product/command-spec.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ prisma-cli --version --json
384384

385385
`prisma-cli version` is the richer environment report; `prisma-cli --version` is the terse one-liner. Both report the same `cli.version`. Use the flag for quick checks, the subcommand for support tickets and bug reports.
386386

387-
## `prisma-cli init --framework <nextjs|nuxt|astro|hono|nestjs|tanstack-start|bun|custom> --entry <path> --http-port <port> --region <region> --name <app-name> --link --no-link --project <id-or-name>`
387+
## `prisma-cli init --framework <nextjs|nuxt|astro|hono|nestjs|tanstack-start|bun|custom> --entry <path> --http-port <port> --region <region> --name <app-name> --link --no-link --project <id-or-name> --install --no-install`
388388

389389
Purpose:
390390

@@ -403,6 +403,12 @@ Behavior:
403403
- init never scaffolds application code, never creates schema or database resources, and never deploys
404404
- with `--framework custom`, the config includes a commented `build` stub, since custom artifacts require `build.outputDirectory` and `build.entrypoint` before deploy can use them
405405
- when detection fails and no `--framework` is passed: interactive mode prompts for the framework from the supported list; non-interactive and `--json` mode fail with `INIT_DETECTION_FAILED`, with `nextActions` enumerating the `--framework` choices
406+
- types step, after the config is written: the generated config's typed import (`@prisma/compute-sdk/config`) is resolved by the CLI at deploy time without a local install, so a local `@prisma/compute-sdk` devDependency exists purely for editor types
407+
- when the package is already a dependency or devDependency, the step is a no-op
408+
- interactive mode asks `Install @prisma/compute-sdk for config types?` (default yes) and runs the detected package manager's add command (`pnpm add -D`, `bun add -d`, `yarn add -D`, `npm install -D`)
409+
- `--install` runs the install without prompting; `--no-install` skips the step; non-interactive and `--json` mode skip by default
410+
- a skipped, declined, or failed install downgrades to a hint or warning with the exact add command in `nextSteps`; the config write stands and init exits 0
411+
- a directory without a `package.json` skips the step with the hint
406412
- link step, after the config is written:
407413
- interactive mode asks `Link this directory to a Prisma Project now? (Y/n)` when the directory has no project binding; accepting enters the same picker `project link` uses
408414
- `--no-link` suppresses the question; `--link` requires the step; `--project <id-or-name>` links to that project without prompting

packages/cli/src/commands/init/index.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ export function createInitCommand(runtime: CliRuntime): Command {
3939
.addOption(new Option("--no-link", "Skip the Project link step"))
4040
.addOption(
4141
new Option("--project <id-or-name>", "Project to link this directory to"),
42-
);
42+
)
43+
.addOption(
44+
new Option("--install", "Install @prisma/compute-sdk for config types"),
45+
)
46+
.addOption(new Option("--no-install", "Skip the types install step"));
4347
addGlobalFlags(command);
4448

4549
command.action(async (options) => {
@@ -51,6 +55,7 @@ export function createInitCommand(runtime: CliRuntime): Command {
5155
name?: string;
5256
link?: boolean;
5357
project?: string;
58+
install?: boolean;
5459
};
5560

5661
await runCommand<InitResult>(
@@ -66,6 +71,7 @@ export function createInitCommand(runtime: CliRuntime): Command {
6671
name: flags.name,
6772
link: flags.link,
6873
project: flags.project,
74+
install: flags.install,
6975
}),
7076
{
7177
renderHuman: (context, descriptor, result) =>

packages/cli/src/controllers/init.ts

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { writeFile } from "node:fs/promises";
22
import path from "node:path";
3-
43
import {
54
COMPUTE_CONFIG_FILENAME,
65
COMPUTE_REGIONS,
@@ -15,11 +14,16 @@ import {
1514
frameworkFromAlias,
1615
serializeComputeConfig,
1716
} from "@prisma/compute-sdk/config";
17+
import { execa } from "execa";
1818

1919
import {
2020
type PrismaCliPackageCommandFormatter,
2121
resolvePrismaCliPackageCommandFormatterSync,
2222
} from "../lib/agent/cli-command";
23+
import {
24+
type AgentPackageManager,
25+
detectPackageManagerSync,
26+
} from "../lib/agent/package-manager";
2327
import {
2428
readBunPackageEntrypoint,
2529
readBunPackageJson,
@@ -34,7 +38,12 @@ import {
3438
textPrompt,
3539
} from "../shell/prompt";
3640
import { type CommandContext, canPrompt } from "../shell/runtime";
37-
import type { InitLinkState, InitResult, InitSettingRow } from "../types/init";
41+
import type {
42+
InitLinkState,
43+
InitResult,
44+
InitSettingRow,
45+
InitTypesState,
46+
} from "../types/init";
3847
import { detectDeployFramework } from "./app";
3948
import { runProjectLink } from "./project";
4049

@@ -46,6 +55,7 @@ export interface InitFlags {
4655
name?: string;
4756
link?: boolean;
4857
project?: string;
58+
install?: boolean;
4959
}
5060

5161
interface ResolvedInitFramework {
@@ -129,12 +139,17 @@ export async function runInit(
129139
}
130140

131141
const warnings: string[] = [];
142+
const types = await resolveInitTypes(context, flags, {
143+
onWarning: (message) => warnings.push(message),
144+
});
132145
const link = await resolveInitLink(context, flags, {
133146
onWarning: (message) => warnings.push(message),
134147
formatCommand,
135148
});
136149

137150
const unlinked = link.status !== "linked" && link.status !== "already-linked";
151+
const typesMissing =
152+
types.status !== "installed" && types.status !== "already-installed";
138153
return {
139154
command: "init",
140155
result: {
@@ -148,16 +163,161 @@ export async function runInit(
148163
...(region ? { region } : {}),
149164
},
150165
settings,
166+
types,
151167
link,
152168
},
153169
warnings,
154170
nextSteps: [
171+
...(typesMissing && types.installCommand ? [types.installCommand] : []),
155172
formatCommand(["app", "deploy"]),
156173
...(unlinked ? [formatCommand(["project", "link"])] : []),
157174
],
158175
};
159176
}
160177

178+
/** Dev dependency that provides editor types for the generated config. */
179+
const COMPUTE_SDK_PACKAGE = "@prisma/compute-sdk";
180+
181+
function packageAddCommand(packageManager: AgentPackageManager): string[] {
182+
switch (packageManager) {
183+
case "pnpm":
184+
return ["pnpm", "add", "-D", COMPUTE_SDK_PACKAGE];
185+
case "bun":
186+
return ["bun", "add", "-d", COMPUTE_SDK_PACKAGE];
187+
case "yarn":
188+
return ["yarn", "add", "-D", COMPUTE_SDK_PACKAGE];
189+
case "npm":
190+
return ["npm", "install", "-D", COMPUTE_SDK_PACKAGE];
191+
}
192+
}
193+
194+
/**
195+
* Offers to install `@prisma/compute-sdk` as a devDependency so the generated
196+
* config's typed import resolves in the editor. Deploy resolves the import
197+
* without a local install, so every outcome short of success is a hint, never
198+
* a failure.
199+
*/
200+
async function resolveInitTypes(
201+
context: CommandContext,
202+
flags: InitFlags,
203+
hooks: { onWarning: (message: string) => void },
204+
): Promise<InitTypesState> {
205+
const cwd = context.runtime.cwd;
206+
const packageJson = await readBunPackageJson(cwd, context.runtime.signal);
207+
if (hasComputeSdkDependency(packageJson)) {
208+
return {
209+
status: "already-installed",
210+
package: COMPUTE_SDK_PACKAGE,
211+
installCommand: null,
212+
};
213+
}
214+
215+
const packageManager = detectPackageManagerSync(cwd) ?? "npm";
216+
const installCommand = packageAddCommand(packageManager);
217+
const installCommandText = installCommand.join(" ");
218+
const state = (status: InitTypesState["status"]): InitTypesState => ({
219+
status,
220+
package: COMPUTE_SDK_PACKAGE,
221+
installCommand: installCommandText,
222+
});
223+
224+
// A directory without a package.json has nowhere to record the dependency.
225+
if (!packageJson) {
226+
return state("skipped");
227+
}
228+
229+
if (flags.install === false) {
230+
return state("skipped");
231+
}
232+
233+
let shouldInstall = flags.install === true;
234+
if (!shouldInstall) {
235+
if (!canPrompt(context) || context.flags.yes) {
236+
return state("skipped");
237+
}
238+
try {
239+
shouldInstall = await confirmPrompt({
240+
input: context.runtime.stdin,
241+
output: context.output.stderr,
242+
signal: context.runtime.signal,
243+
message: `Install ${COMPUTE_SDK_PACKAGE} for config types? (${installCommandText})`,
244+
initialValue: true,
245+
});
246+
} catch (error) {
247+
if (isPromptCancelError(error)) {
248+
return state("declined");
249+
}
250+
throw error;
251+
}
252+
if (!shouldInstall) {
253+
return state("declined");
254+
}
255+
}
256+
257+
const command = resolveInitInstallCommandOverride(context) ?? installCommand;
258+
if (!context.flags.quiet && !context.flags.json) {
259+
context.output.stderr.write(`Installing ${COMPUTE_SDK_PACKAGE}...\n`);
260+
}
261+
try {
262+
const [executable, ...args] = command;
263+
await execa(executable as string, args, {
264+
cwd,
265+
env: context.runtime.env,
266+
cancelSignal: context.runtime.signal,
267+
stdin: "ignore",
268+
});
269+
return state("installed");
270+
} catch (error) {
271+
if (context.runtime.signal.aborted) {
272+
throw error;
273+
}
274+
// execa's first message line is the short "Command failed" summary; the
275+
// full package-manager output stays out of the warning.
276+
const detail =
277+
error instanceof Error ? error.message.split("\n")[0] : String(error);
278+
hooks.onWarning(
279+
`Installing ${COMPUTE_SDK_PACKAGE} failed: ${detail}. Install it later with ${installCommandText}.`,
280+
);
281+
return state("failed");
282+
}
283+
}
284+
285+
function hasComputeSdkDependency(
286+
packageJson: Awaited<ReturnType<typeof readBunPackageJson>>,
287+
): boolean {
288+
for (const group of [
289+
packageJson?.dependencies,
290+
packageJson?.devDependencies,
291+
]) {
292+
if (
293+
group &&
294+
typeof group === "object" &&
295+
COMPUTE_SDK_PACKAGE in (group as Record<string, unknown>)
296+
) {
297+
return true;
298+
}
299+
}
300+
return false;
301+
}
302+
303+
/** Test hook: JSON array command that replaces the real package-manager install. */
304+
function resolveInitInstallCommandOverride(
305+
context: CommandContext,
306+
): string[] | null {
307+
const raw = context.runtime.env.PRISMA_CLI_INIT_INSTALL_COMMAND;
308+
if (!raw) {
309+
return null;
310+
}
311+
try {
312+
const parsed = JSON.parse(raw);
313+
return Array.isArray(parsed) && parsed.every((p) => typeof p === "string")
314+
? parsed
315+
: null;
316+
} catch {
317+
return null;
318+
}
319+
}
320+
161321
const CUSTOM_BUILD_STUB = `
162322
// framework "custom" deploys a prebuilt artifact. Add its build settings:
163323
// build: {

packages/cli/src/presenters/init.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,31 @@ export function renderInit(
1717
renderSummaryLine(ui, "success", `Wrote ${result.configPath}`),
1818
];
1919

20+
if (result.types.status === "installed") {
21+
lines.push(
22+
renderSummaryLine(
23+
ui,
24+
"success",
25+
`Installed ${result.types.package} (config types)`,
26+
),
27+
);
28+
} else if (result.types.status === "failed" && result.types.installCommand) {
29+
lines.push(
30+
renderSummaryLine(
31+
ui,
32+
"warning",
33+
`Could not install ${result.types.package}; install later with ${result.types.installCommand}`,
34+
),
35+
);
36+
} else if (
37+
result.types.status !== "already-installed" &&
38+
result.types.installCommand
39+
) {
40+
lines.push(
41+
` ${ui.dim(`For editor types: ${result.types.installCommand}`)}`,
42+
);
43+
}
44+
2045
switch (result.link.status) {
2146
case "linked":
2247
lines.push(
@@ -36,7 +61,14 @@ export function renderInit(
3661
);
3762
break;
3863
case "failed":
39-
// The failure detail is in warnings; nothing extra here.
64+
// Human mode does not render success.warnings, so surface it here.
65+
lines.push(
66+
renderSummaryLine(
67+
ui,
68+
"warning",
69+
`Project link failed; link later with ${formatCommand(["project", "link"])}`,
70+
),
71+
);
4072
break;
4173
}
4274

packages/cli/src/types/init.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,20 @@ export interface InitLinkState {
1919
} | null;
2020
}
2121

22+
export type InitTypesStatus =
23+
| "installed"
24+
| "already-installed"
25+
| "skipped"
26+
| "declined"
27+
| "failed";
28+
29+
export interface InitTypesState {
30+
status: InitTypesStatus;
31+
package: string;
32+
/** Human-runnable install command for hints when not installed. */
33+
installCommand: string | null;
34+
}
35+
2236
export interface InitResult {
2337
configPath: string;
2438
directory: string;
@@ -30,5 +44,6 @@ export interface InitResult {
3044
region?: string;
3145
};
3246
settings: InitSettingRow[];
47+
types: InitTypesState;
3348
link: InitLinkState;
3449
}

0 commit comments

Comments
 (0)