Skip to content

Commit 10f6c43

Browse files
feat(cli): offer the Prisma Compute skill install during init (#118)
## Summary `init` now offers to install the `prisma-compute` skill the same way `app deploy` does, so interactive setups get agent skills without a separate `agent install` step. - Extracted deploy's `maybePromptForAgentSetup` into a shared `controllers/agent-setup.ts` (pure move, single implementation; deploy behavior unchanged). - `init` runs the prompt after the link step, in both the fresh path and the `--format ts` conversion path (acting on the config directory, matching the other conversion side-effect steps). - The prompt and its dismissal state are shared between init and deploy: whichever runs interactively first asks, declining records the dismissal, and neither asks twice. - Never prompts in `--json`, `--quiet`, CI, non-interactive, or `--yes` runs; a failed install downgrades to a warning with the retry command and the config write stands. - Spec: documented the init agent skill step and cross-referenced the shared prompt from the deploy section. ## Testing - New `tests/init-agent-setup.test.ts` covering accept (install invoked with `--skill prisma-compute` from the config dir), decline (dismissal recorded, init succeeds), already-installed (no prompt), install failure (warning + retry hint, exit 0), and non-interactive (no prompt). - Full CLI suite: 629 tests / 46 files pass; `tsc --noEmit` clean.
1 parent a589fdd commit 10f6c43

8 files changed

Lines changed: 296 additions & 132 deletions

File tree

docs/product/command-spec.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ Purpose:
393393
Behavior:
394394

395395
- init is the config formalizer: `app deploy` works with zero config, and init writes down what deploy would infer so the setup is committed, reviewable, and stable for teammates and CI
396-
- writing the config requires no auth and no network; linking is the only remote step
396+
- writing the config requires no auth and no network; the only steps that may go remote are the accepted types install, link, and agent skill install, all optional and all after the config is written
397397
- fails with `INIT_CONFIG_EXISTS` when a compute config already exists in the invocation directory or any ancestor up to the repository or workspace root; the error names the existing file, and init never overwrites or merges — editing a committed config is the user's editor's job
398398
- `--format <ts|json>` selects the config serialization; `ts` (alias `typescript`) is the default and writes `prisma.compute.ts`, `json` writes `prisma.compute.json` via the shared SDK serializer, a dependency-free static document (a `$schema` reference will be emitted once the schema is hosted; the loader already accepts and strips one); both formats pin exactly the same resolved values, and the CLI's global `--json` output flag is unrelated to the config format
399399
- with `--format json`, the types install step never runs: the JSON format exists to be dependency-free, so `--install` is a usage error; without `--install`, the result reports the step as skipped with no install hint
@@ -418,6 +418,7 @@ Behavior:
418418
- 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
419419
- `--no-link` suppresses the question; `--link` requires the step; `--project <id-or-name>` links to that project without prompting
420420
- link failures and cancellations after the config is written downgrade to warnings and `nextSteps`; the config write stands and init exits 0
421+
- agent skill step, after the link step: interactive runs prompt once to install the Prisma Compute skill for the project when `prisma-compute` is missing, the same shared prompt `app deploy` uses; accepting installs `prisma-compute` from `prisma/skills` from the config directory, equivalent to the detected package-runner command such as `pnpm dlx @prisma/cli@latest agent install --skill prisma-compute`; declining records the prompt as dismissed in local CLI state, so neither init nor deploy asks again; a failed install downgrades to a warning with the retry command, records no dismissal (a later interactive init or deploy offers again), and the config write stands; does not prompt in `--json`, `--quiet`, CI, non-interactive, or `--yes` runs
421422
- `nextSteps` includes the deploy command, plus the project link command when the directory is still unlinked
422423
- user-facing command hints in init output (next steps, link hints, error recovery commands) use the package runner detected from the project, such as `pnpm dlx @prisma/cli@latest project link` or `npx -y @prisma/cli@latest app deploy`, matching the `agent` group's convention
423424
- in `--json`, `result` includes `configPath`, `format` (`typescript` or `json`), `converted` (true only for the `--format ts` conversion path), the written `app` values (null when a conversion transported a config that does not pin a single fully-resolved app), per-value `settings` sources, and `link` state; `--json` never prompts
@@ -1411,7 +1412,7 @@ Behavior:
14111412
- maps user-facing framework names to deploy build strategies
14121413
- does not accept `--build-command` or `--output-directory`; custom build settings live in the `build` block of `prisma.compute.ts`
14131414
- deploys arbitrary framework output with `framework: "custom"` when the config provides a built output directory and entrypoint; `build.command` is optional for prebuilt artifacts
1414-
- in interactive human deploys, prompts once to install the Prisma Compute skill for the project when `prisma-compute` is missing; accepting installs `prisma-compute` from `prisma/skills` from the project directory, equivalent to the detected package-runner command such as `pnpm dlx @prisma/cli@latest agent install --skill prisma-compute`; declining records the prompt as dismissed in local CLI state
1415+
- in interactive human deploys, prompts once to install the Prisma Compute skill for the project when `prisma-compute` is missing; accepting installs `prisma-compute` from `prisma/skills` from the project directory, equivalent to the detected package-runner command such as `pnpm dlx @prisma/cli@latest agent install --skill prisma-compute`; declining records the prompt as dismissed in local CLI state, while a failed install records no dismissal, so a later interactive run offers again; the prompt and its dismissal state are shared with `init`, so whichever command runs interactively first asks and neither asks twice
14151416
- does not prompt for agent setup in `--json`, `--quiet`, CI, `--no-interactive`, or `--yes`
14161417
- uses `src/index.ts` as the Hono deploy entrypoint when the app has no `package.json#main` or `package.json#module` and that file exists
14171418
- supports vanilla Bun apps with `--framework bun` using `package.json#main` or `package.json#module`, or with `--entry <path>`
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { resolvePrismaCliPackageCommand } from "../lib/agent/cli-command";
2+
import {
3+
PRISMA_AGENT_INSTALL_ARGS,
4+
PRISMA_COMPUTE_AGENT_SKILL,
5+
} from "../lib/agent/constants";
6+
import {
7+
readPrismaAgentSetupStatus,
8+
shouldOfferPrismaAgentSetup,
9+
} from "../lib/agent/setup-status";
10+
import { confirmPrompt } from "../shell/prompt";
11+
import { type CommandContext, canPrompt } from "../shell/runtime";
12+
import { renderSummaryLine } from "../shell/ui";
13+
import { runAgentInstall } from "./agent";
14+
15+
/**
16+
* One-time interactive offer to install the Prisma Compute skill for the
17+
* project. Shared by the commands that establish a project setup (init,
18+
* deploy); the answer is remembered, so whichever command runs first asks.
19+
* Returns warnings; a failed install must not fail the calling command.
20+
*/
21+
export async function maybePromptForAgentSetup(
22+
context: CommandContext,
23+
projectDir: string,
24+
): Promise<string[]> {
25+
// canPrompt covers json/CI/non-TTY/--no-interactive; quiet and yes runs
26+
// must also stay prompt-free even on a TTY.
27+
if (!canPrompt(context) || context.flags.yes || context.flags.quiet) {
28+
return [];
29+
}
30+
31+
const status = await readPrismaAgentSetupStatus({
32+
cwd: projectDir,
33+
stateStore: context.stateStore,
34+
signal: context.runtime.signal,
35+
requiredSkill: PRISMA_COMPUTE_AGENT_SKILL,
36+
});
37+
if (!shouldOfferPrismaAgentSetup(status)) {
38+
return [];
39+
}
40+
41+
const shouldInstall = await confirmPrompt({
42+
input: context.runtime.stdin,
43+
output: context.runtime.stderr,
44+
signal: context.runtime.signal,
45+
message: "Install the Prisma Compute skill for this project?",
46+
initialValue: true,
47+
});
48+
49+
if (!shouldInstall) {
50+
await context.stateStore.setAgentSetupPromptDismissedAt(
51+
new Date().toISOString(),
52+
);
53+
return [];
54+
}
55+
56+
try {
57+
await runAgentInstall(
58+
context,
59+
{ skill: [PRISMA_COMPUTE_AGENT_SKILL] },
60+
"install",
61+
{ cwd: projectDir },
62+
);
63+
if (!context.flags.quiet && !context.flags.json) {
64+
context.output.stderr.write(
65+
`${renderSummaryLine(context.ui, "success", "Installed the Prisma Compute skill for this project.")}\n\n`,
66+
);
67+
}
68+
return [];
69+
} catch (error) {
70+
const message =
71+
error instanceof Error ? error.message : "Prisma skill install failed.";
72+
if (!context.flags.quiet && !context.flags.json) {
73+
context.output.stderr.write(
74+
`${renderSummaryLine(context.ui, "warning", `Skipped Prisma skill install: ${message}`)}\n\n`,
75+
);
76+
}
77+
const retryCommand = await resolvePrismaCliPackageCommand({
78+
cwd: projectDir,
79+
signal: context.runtime.signal,
80+
args: [
81+
...PRISMA_AGENT_INSTALL_ARGS,
82+
"--skill",
83+
PRISMA_COMPUTE_AGENT_SKILL,
84+
],
85+
});
86+
return [
87+
`The Prisma Compute skill was not installed. Run ${retryCommand} to try again.`,
88+
];
89+
}
90+
}

packages/cli/src/controllers/app.ts

Lines changed: 2 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -18,15 +18,6 @@ import type { ManagementApiClient } from "@prisma/management-api-sdk";
1818
import { matchError, Result } from "better-result";
1919
import open from "open";
2020
import { FileTokenStorage } from "../adapters/token-storage";
21-
import { resolvePrismaCliPackageCommand } from "../lib/agent/cli-command";
22-
import {
23-
PRISMA_AGENT_INSTALL_ARGS,
24-
PRISMA_COMPUTE_AGENT_SKILL,
25-
} from "../lib/agent/constants";
26-
import {
27-
readPrismaAgentSetupStatus,
28-
shouldOfferPrismaAgentSetup,
29-
} from "../lib/agent/setup-status";
3021
import { DEFAULT_REGION } from "../lib/app/app-interaction";
3122
import {
3223
type AppRecord,
@@ -139,7 +130,7 @@ import {
139130
import { type CommandSuccess, writeJsonEvent } from "../shell/output";
140131
import { confirmPrompt, selectPrompt, textPrompt } from "../shell/prompt";
141132
import { type CommandContext, canPrompt } from "../shell/runtime";
142-
import { renderCommandHeader, renderSummaryLine } from "../shell/ui";
133+
import { renderCommandHeader } from "../shell/ui";
143134
import type {
144135
AppBuildResult,
145136
AppDeployAllResult,
@@ -166,7 +157,7 @@ import type {
166157
import type { AuthWorkspace } from "../types/auth";
167158
import type { BranchKind } from "../types/branch";
168159
import type { ProjectResolution, ProjectSummary } from "../types/project";
169-
import { runAgentInstall } from "./agent";
160+
import { maybePromptForAgentSetup } from "./agent-setup";
170161
import { requireAuthenticatedAuthState } from "./auth";
171162
import { listRealWorkspaceProjects } from "./project";
172163
import { createSelectPromptPort } from "./select-prompt-port";
@@ -4321,75 +4312,6 @@ function maybeRenderProjectLinked(
43214312
);
43224313
}
43234314

4324-
async function maybePromptForAgentSetup(
4325-
context: CommandContext,
4326-
projectDir: string,
4327-
): Promise<string[]> {
4328-
if (!canPrompt(context) || context.flags.yes) {
4329-
return [];
4330-
}
4331-
4332-
const status = await readPrismaAgentSetupStatus({
4333-
cwd: projectDir,
4334-
stateStore: context.stateStore,
4335-
signal: context.runtime.signal,
4336-
requiredSkill: PRISMA_COMPUTE_AGENT_SKILL,
4337-
});
4338-
if (!shouldOfferPrismaAgentSetup(status)) {
4339-
return [];
4340-
}
4341-
4342-
const shouldInstall = await confirmPrompt({
4343-
input: context.runtime.stdin,
4344-
output: context.runtime.stderr,
4345-
signal: context.runtime.signal,
4346-
message: "Install the Prisma Compute skill for this project?",
4347-
initialValue: true,
4348-
});
4349-
4350-
if (!shouldInstall) {
4351-
await context.stateStore.setAgentSetupPromptDismissedAt(
4352-
new Date().toISOString(),
4353-
);
4354-
return [];
4355-
}
4356-
4357-
try {
4358-
await runAgentInstall(
4359-
context,
4360-
{ skill: [PRISMA_COMPUTE_AGENT_SKILL] },
4361-
"install",
4362-
{ cwd: projectDir },
4363-
);
4364-
if (!context.flags.quiet && !context.flags.json) {
4365-
context.output.stderr.write(
4366-
`${renderSummaryLine(context.ui, "success", "Installed the Prisma Compute skill for this project.")}\n\n`,
4367-
);
4368-
}
4369-
return [];
4370-
} catch (error) {
4371-
const message =
4372-
error instanceof Error ? error.message : "Prisma skill install failed.";
4373-
if (!context.flags.quiet && !context.flags.json) {
4374-
context.output.stderr.write(
4375-
`${renderSummaryLine(context.ui, "warning", `Skipped Prisma skill install: ${message}`)}\n\n`,
4376-
);
4377-
}
4378-
const retryCommand = await resolvePrismaCliPackageCommand({
4379-
cwd: projectDir,
4380-
signal: context.runtime.signal,
4381-
args: [
4382-
...PRISMA_AGENT_INSTALL_ARGS,
4383-
"--skill",
4384-
PRISMA_COMPUTE_AGENT_SKILL,
4385-
],
4386-
});
4387-
return [
4388-
`The Prisma Compute skill was not installed. Run ${retryCommand} to try again.`,
4389-
];
4390-
}
4391-
}
4392-
43934315
async function maybeCustomizeDeploySettings(
43944316
context: CommandContext,
43954317
options: {

packages/cli/src/controllers/init.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ import type {
4949
InitSettingRow,
5050
InitTypesState,
5151
} from "../types/init";
52+
import { maybePromptForAgentSetup } from "./agent-setup";
5253
import { detectDeployFramework } from "./app";
5354
import { runProjectLink } from "./project";
5455

@@ -216,6 +217,7 @@ export async function runInit(
216217
onWarning: (message) => warnings.push(message),
217218
formatCommand,
218219
});
220+
warnings.push(...(await maybePromptForAgentSetup(context, cwd)));
219221

220222
const unlinked = link.status !== "linked" && link.status !== "already-linked";
221223
const typesMissing =
@@ -600,6 +602,9 @@ async function runInitConversion(
600602
onWarning: (message) => warnings.push(message),
601603
formatCommand,
602604
});
605+
warnings.push(
606+
...(await maybePromptForAgentSetup(stepContext, loaded.configDir)),
607+
);
603608

604609
const unlinked = link.status !== "already-linked" && link.status !== "linked";
605610
const typesMissing =

packages/cli/tests/agent.test.ts

Lines changed: 3 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import path from "node:path";
44
import { afterEach, describe, expect, it, vi } from "vitest";
55

66
import { createTempCwd, executeCli } from "./helpers";
7+
import { writeSkillsLockWithSkill } from "./helpers/skills-lock";
78

89
function expectSkillsCommandPrefix(
910
command: string[],
@@ -348,21 +349,7 @@ describe("agent commands", () => {
348349

349350
it("checks required Prisma skills from the skills lock", async () => {
350351
const cwd = await createTempCwd();
351-
await writeFile(
352-
path.join(cwd, "skills-lock.json"),
353-
JSON.stringify({
354-
version: 1,
355-
skills: {
356-
"prisma-client-api": {
357-
source: "prisma/skills",
358-
sourceType: "github",
359-
skillPath: "prisma-client-api/SKILL.md",
360-
computedHash: "test",
361-
},
362-
},
363-
}),
364-
"utf8",
365-
);
352+
await writeSkillsLockWithSkill(cwd, "prisma-client-api");
366353

367354
const { readPrismaAgentSetupStatus } = await import(
368355
"../src/lib/agent/setup-status"
@@ -380,21 +367,7 @@ describe("agent commands", () => {
380367
}),
381368
).resolves.toMatchObject({ skillsInstalled: false });
382369

383-
await writeFile(
384-
path.join(cwd, "skills-lock.json"),
385-
JSON.stringify({
386-
version: 1,
387-
skills: {
388-
"prisma-compute": {
389-
source: "prisma/skills",
390-
sourceType: "github",
391-
skillPath: "prisma-compute/SKILL.md",
392-
computedHash: "test",
393-
},
394-
},
395-
}),
396-
"utf8",
397-
);
370+
await writeSkillsLockWithSkill(cwd, "prisma-compute");
398371

399372
await expect(
400373
readPrismaAgentSetupStatus({

packages/cli/tests/app-controller.test.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
createProjectClient,
1111
createResolveBranch,
1212
} from "./helpers/mock-factories";
13+
import { writeSkillsLockWithSkill } from "./helpers/skills-lock";
1314

1415
beforeEach(() => {
1516
process.env.PRISMA_CLI_TEST_REMEMBER_PROJECT_ID = "proj_123";
@@ -179,24 +180,6 @@ async function writeLocalPin(
179180
);
180181
}
181182

182-
async function writeAgentSetupInstalled(cwd: string): Promise<void> {
183-
await writeFile(
184-
path.join(cwd, "skills-lock.json"),
185-
JSON.stringify({
186-
version: 1,
187-
skills: {
188-
"prisma-compute": {
189-
source: "prisma/skills",
190-
sourceType: "github",
191-
skillPath: "prisma-compute/SKILL.md",
192-
computedHash: "test",
193-
},
194-
},
195-
}),
196-
"utf8",
197-
);
198-
}
199-
200183
async function setupAgentPromptDeployTest(options: {
201184
confirmPrompt: ReturnType<typeof vi.fn>;
202185
deployApp?: ReturnType<typeof vi.fn>;
@@ -3221,7 +3204,7 @@ describe("app controller", () => {
32213204
next: "15.0.0",
32223205
},
32233206
});
3224-
await writeAgentSetupInstalled(cwd);
3207+
await writeSkillsLockWithSkill(cwd);
32253208
const stateDir = path.join(cwd, ".state");
32263209
const { context, stderr } = await createTestCommandContext({
32273210
cwd,
@@ -3528,7 +3511,7 @@ describe("app controller", () => {
35283511
await writePackageJson(cwd, {
35293512
name: "suggested-name",
35303513
});
3531-
await writeAgentSetupInstalled(cwd);
3514+
await writeSkillsLockWithSkill(cwd);
35323515
const stateDir = path.join(cwd, ".state");
35333516
const { context, stderr } = await createTestCommandContext({
35343517
cwd,
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { writeFile } from "node:fs/promises";
2+
import path from "node:path";
3+
4+
/**
5+
* Writes a skills-lock.json recording the given skill as installed from
6+
* prisma/skills, the shape the agent setup status check reads.
7+
*/
8+
export async function writeSkillsLockWithSkill(
9+
cwd: string,
10+
skillName = "prisma-compute",
11+
): Promise<void> {
12+
await writeFile(
13+
path.join(cwd, "skills-lock.json"),
14+
JSON.stringify({
15+
version: 1,
16+
skills: {
17+
[skillName]: {
18+
source: "prisma/skills",
19+
sourceType: "github",
20+
skillPath: `${skillName}/SKILL.md`,
21+
computedHash: "test",
22+
},
23+
},
24+
}),
25+
"utf8",
26+
);
27+
}

0 commit comments

Comments
 (0)