@prisma-next/cli 0.10.0-dev.2 → 0.10.0-dev.4

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.
@@ -11,6 +11,159 @@ import { execFile } from "node:child_process";
11
11
  import { promisify } from "node:util";
12
12
  import { detect, getUserAgent } from "package-manager-detector/detect";
13
13
  import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
14
+ //#region src/commands/init/detect-package-manager.ts
15
+ const KNOWN = new Set([
16
+ "pnpm",
17
+ "npm",
18
+ "yarn",
19
+ "bun",
20
+ "deno"
21
+ ]);
22
+ /**
23
+ * Resolves the package manager `init` should drive for `add` / `install`
24
+ * commands. Tries, in order:
25
+ *
26
+ * 1. **`detect()`** — walks up from `cwd` looking for a lockfile, the
27
+ * `packageManager` field, the `devEngines.packageManager` field, or
28
+ * install metadata. This is the right answer whenever the user is
29
+ * anywhere inside an existing project, including a deep workspace
30
+ * subdirectory.
31
+ *
32
+ * 2. **`getUserAgent()`** — parses `npm_config_user_agent`, the env var
33
+ * every PM sets when it spawns a script. This catches the
34
+ * bare-directory case where there's no project to walk up to but the
35
+ * user invoked us via `pnpm dlx prisma-next init` / `bunx
36
+ * prisma-next init` / `yarn dlx …`. Same signal used by every
37
+ * `create-*` tool in the ecosystem (`create-vite`, `create-next-app`,
38
+ * `create-astro`, `@antfu/ni`, …).
39
+ *
40
+ * 3. **`npm`** — final fallback. Always present alongside Node.
41
+ */
42
+ async function detectPackageManager(cwd) {
43
+ const detected = await detect({ cwd });
44
+ if (detected && KNOWN.has(detected.name)) return detected.name;
45
+ const userAgent = getUserAgent();
46
+ if (userAgent !== null && KNOWN.has(userAgent)) return userAgent;
47
+ return "npm";
48
+ }
49
+ function hasProjectManifest(cwd) {
50
+ return existsSync(join(cwd, "package.json")) || existsSync(join(cwd, "deno.json")) || existsSync(join(cwd, "deno.jsonc"));
51
+ }
52
+ function formatRunCommand(pm, bin, args) {
53
+ if (pm === "npm") return `npx ${bin} ${args}`;
54
+ if (pm === "deno") return `deno run npm:${bin} ${args}`;
55
+ return `${pm} ${bin} ${args}`;
56
+ }
57
+ function formatAddArgs(pm, packages) {
58
+ if (pm === "deno") return ["add", ...packages.map((p) => `npm:${p}`)];
59
+ return ["add", ...packages];
60
+ }
61
+ function formatAddDevArgs(pm, packages) {
62
+ if (pm === "deno") return [
63
+ "add",
64
+ "--dev",
65
+ ...packages.map((p) => `npm:${p}`)
66
+ ];
67
+ return [
68
+ "add",
69
+ "-D",
70
+ ...packages
71
+ ];
72
+ }
73
+ //#endregion
74
+ //#region src/commands/init/detect-pnpm-catalog.ts
75
+ /**
76
+ * Walks up from `baseDir` looking for `pnpm-workspace.yaml`, then scans
77
+ * its top-level `catalog:` block for entries that match any of `packages`.
78
+ *
79
+ * Implements FR7.3 / Spec Decision 8 (honour-and-warn): when `init` runs
80
+ * inside a pnpm workspace whose catalog overrides one of the packages it
81
+ * installs, surface a structured warning so the user knows the catalog
82
+ * version (not the published `latest`) is what ended up in their
83
+ * `node_modules`. pnpm itself does this silently; the warning closes the
84
+ * "looks fine, must be wrong version six months later" gap.
85
+ *
86
+ * Notes / scope:
87
+ *
88
+ * - We only inspect the unnamed top-level `catalog:` block. pnpm also
89
+ * supports `catalogs:` (plural — *named* catalogs referenced via
90
+ * `catalog:foo` specifiers); those don't apply to a vanilla
91
+ * `pnpm add prisma-next` invocation, so we skip them.
92
+ * - We don't validate YAML syntax exhaustively. The file format pnpm
93
+ * ships is line-oriented and well-known; a minimal regex is more
94
+ * robust than depending on a YAML parser for one warning.
95
+ * - We don't compare against the registry's `latest` — pnpm uses the
96
+ * catalog version regardless, so the warning fires whenever a match
97
+ * exists. The user-facing copy explains how to opt out.
98
+ */
99
+ function detectPnpmCatalogOverrides(baseDir, packages) {
100
+ const workspaceFile = findNearestPnpmWorkspaceFile(baseDir);
101
+ if (workspaceFile === null) return null;
102
+ const catalog = extractCatalogBlock(readFileSync(workspaceFile, "utf-8"));
103
+ if (catalog === null) return {
104
+ workspaceFile,
105
+ entries: []
106
+ };
107
+ const wanted = new Set(packages);
108
+ const entries = [];
109
+ for (const [name, version] of catalog) if (wanted.has(name)) entries.push({
110
+ name,
111
+ version
112
+ });
113
+ return {
114
+ workspaceFile,
115
+ entries
116
+ };
117
+ }
118
+ function findNearestPnpmWorkspaceFile(baseDir) {
119
+ let dir = baseDir;
120
+ let prev = "";
121
+ while (dir !== prev) {
122
+ const candidate = join(dir, "pnpm-workspace.yaml");
123
+ if (existsSync(candidate)) return candidate;
124
+ prev = dir;
125
+ dir = dirname(dir);
126
+ }
127
+ return null;
128
+ }
129
+ /**
130
+ * Returns the entries inside the top-level `catalog:` block as `[name, version]`
131
+ * pairs in document order, or `null` when no `catalog:` block exists.
132
+ *
133
+ * The parser is intentionally minimal: it reads line-by-line, locates the
134
+ * top-level `catalog:` line (no leading whitespace), then collects every
135
+ * subsequent indented line of the form `<key>: <value>` until the next
136
+ * top-level key (or end of file). Quotes around `<key>` and `<value>`
137
+ * are stripped; comments (`#…`) are ignored.
138
+ */
139
+ function extractCatalogBlock(contents) {
140
+ const lines = contents.split(/\r?\n/);
141
+ const startIdx = lines.findIndex((line) => /^catalog\s*:\s*$/.test(line));
142
+ if (startIdx === -1) return null;
143
+ const entries = [];
144
+ for (let i = startIdx + 1; i < lines.length; i++) {
145
+ const raw = lines[i] ?? "";
146
+ if (raw.trim() === "" || /^\s*#/.test(raw)) continue;
147
+ if (!/^\s/.test(raw)) break;
148
+ const match = raw.match(/^\s+(?:'([^']+)'|"([^"]+)"|([^:\s'"]+))\s*:\s*(.*?)\s*(?:#.*)?$/);
149
+ if (!match) continue;
150
+ const name = match[1] ?? match[2] ?? match[3];
151
+ if (name === void 0) continue;
152
+ const version = stripQuotes((match[4] ?? "").trim());
153
+ if (version === "") continue;
154
+ entries.push([name, version]);
155
+ }
156
+ return entries;
157
+ }
158
+ function stripQuotes(value) {
159
+ if (value.length >= 2) {
160
+ const first = value[0];
161
+ const last = value[value.length - 1];
162
+ if (first === "\"" && last === "\"" || first === "'" && last === "'") return value.slice(1, -1);
163
+ }
164
+ return value;
165
+ }
166
+ //#endregion
14
167
  //#region src/commands/init/errors.ts
15
168
  /**
16
169
  * Re-init in non-interactive mode without `--force`. Distinct from the
@@ -190,383 +343,53 @@ function errorInitProbeFailed(options) {
190
343
  fix: "Confirm `DATABASE_URL` points at a reachable server, or drop `--strict-probe` to treat probe failures as warnings.",
191
344
  docsUrl: "https://prisma-next.dev/docs/cli/init",
192
345
  meta: {
193
- filesWritten: options.filesWritten,
194
- cause: options.cause
195
- }
196
- });
197
- }
198
- /**
199
- * `prisma-next contract emit` failed after a successful install. Surface
200
- * the underlying error so the user can fix it and re-run; files and
201
- * dependencies remain on disk untouched. Maps to exit code
202
- * `5 = EMIT_FAILED`.
203
- */
204
- function errorInitEmitFailed(options) {
205
- return new CliStructuredError("5008", "Failed to emit contract", {
206
- domain: "CLI",
207
- why: `\`prisma-next contract emit\` failed: ${options.cause}`,
208
- fix: `Inspect your contract file, fix the underlying issue, then re-run \`${options.emitCommand}\`. Pass \`-v\` for the full error envelope.`,
209
- docsUrl: "https://prisma-next.dev/docs/cli/contract-emit",
210
- meta: {
211
- filesWritten: options.filesWritten,
212
- cause: options.cause
213
- }
214
- });
215
- }
216
- /**
217
- * The project-level agent-skill install (`npx skills add
218
- * prisma/prisma-next#v<version>`) failed after a successful dependency
219
- * install + emit. The project's scaffold remains on disk; the user
220
- * can either fix the underlying issue (network, registry, PATH) and
221
- * run the install command manually, or re-run `init --no-skill` to
222
- * proceed without the skill.
223
- *
224
- * Non-rolling-back, matching the existing install/emit failure
225
- * semantics. Maps to exit code `6 = SKILL_INSTALL_FAILED`.
226
- */
227
- function errorInitSkillInstallFailed(options) {
228
- return new CliStructuredError("5013", "Failed to install Prisma Next skills", {
229
- domain: "CLI",
230
- why: `\`${options.skillInstallCommand}\` exited with an error: ${options.cause}`,
231
- fix: `Either:
232
- - Re-run \`prisma-next init --no-skill${options.filesWritten.length > 0 ? " --force" : ""}\` to skip the skill install for this run, or\n - Fix the underlying issue (network, npm registry, \`npx skills\` on PATH) and install manually:\n ${options.skillInstallCommand}`,
233
- docsUrl: "https://prisma-next.dev/docs/cli/init#agent-skill",
234
- meta: {
235
- filesWritten: options.filesWritten,
236
- skillInstallCommand: options.skillInstallCommand,
237
- cause: options.cause
238
- }
239
- });
240
- }
241
- //#endregion
242
- //#region src/commands/init/agent-skill-install.ts
243
- const exec = promisify(execFile);
244
- /**
245
- * Default base for the GitHub-URL form `<owner>/<repo>` consumed by
246
- * upstream `skills add`. Each `SkillSource` joins this base with its
247
- * own subpath (and optional `#ref` for version-pinned clusters).
248
- */
249
- const DEFAULT_AGENT_SKILL_BASE = "prisma/prisma-next";
250
- const DEFAULT_AGENT_SKILL_SOURCES = [
251
- {
252
- subpath: "skills",
253
- ref: "cli",
254
- description: "usage skills (version-locked to installed Prisma Next)"
255
- },
256
- {
257
- subpath: "skills/upgrade",
258
- ref: null,
259
- description: "upgrade skill (always tracks `main`)"
260
- },
261
- {
262
- subpath: "skills/extension-author",
263
- ref: null,
264
- description: "extension-author skill (always tracks `main`)"
265
- }
266
- ];
267
- /**
268
- * Test-only escape hatch for pinning the install base to a local
269
- * checkout. Production runs leave this unset, so installs always use
270
- * `DEFAULT_AGENT_SKILL_BASE`.
271
- *
272
- * When set to an absolute filesystem path (typical for tests), the
273
- * `#ref` fragment is dropped — local-path mode in upstream's CLI does
274
- * not accept refs, and the local clone has whatever content the test
275
- * checked into it anyway. When set to anything else (e.g. a fork name
276
- * `myuser/prisma-next`), the ref policy is preserved.
277
- */
278
- function resolveAgentSkillBase() {
279
- const override = process.env["PRISMA_NEXT_SKILLS_BASE"]?.trim();
280
- return override && override.length > 0 ? override : DEFAULT_AGENT_SKILL_BASE;
281
- }
282
- function isLocalPath(base) {
283
- return base.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(base);
284
- }
285
- /**
286
- * Build the `<base>/<subpath>[#ref]` URL the `skills` CLI will
287
- * resolve. Exported for unit tests so the per-source format can be
288
- * asserted without going through the full install loop.
289
- */
290
- function formatSkillSourceUrl(source) {
291
- const base = resolveAgentSkillBase();
292
- const url = `${base}/${source.subpath}`;
293
- if (source.ref === null) return url;
294
- if (isLocalPath(base)) return url;
295
- if (source.ref === "cli") return `${url}#v${version}`;
296
- return url;
297
- }
298
- /**
299
- * The skill-install command for one source, formatted for the
300
- * project's detected package manager. `npx`/`pnpm dlx`/`bunx` are
301
- * interchangeable to the user; we pick the variant that matches the
302
- * rest of the install step so a single project consistently uses one
303
- * runner.
304
- *
305
- * `--all` auto-selects every skill in the cluster and every detected
306
- * agent runtime, skipping the multi-select prompts the `skills` CLI
307
- * shows by default. A non-interactive scaffold step cannot present
308
- * prompts.
309
- *
310
- * Exported for unit tests so the per-PM dispatch can be asserted
311
- * without a live subprocess.
312
- */
313
- function formatSkillInstallCommand(pm, source) {
314
- return formatPackageManagerCommand(pm, [
315
- "skills@latest",
316
- "add",
317
- formatSkillSourceUrl(source),
318
- "--all"
319
- ]);
320
- }
321
- /**
322
- * `skills add --all` should cover Claude Code, but upstream currently skips
323
- * project-local Claude symlinks when `.claude/` does not already exist. Run
324
- * the explicit Claude Code install as well so fresh projects get
325
- * `.claude/skills` without asking users to create that folder first.
326
- */
327
- function formatClaudeSkillInstallCommand(pm, source) {
328
- return formatPackageManagerCommand(pm, [
329
- "skills@latest",
330
- "add",
331
- formatSkillSourceUrl(source),
332
- "--agent",
333
- "claude-code",
334
- "--skill",
335
- "'*'",
336
- "-y"
337
- ]);
338
- }
339
- function formatPackageManagerCommand(pm, args) {
340
- switch (pm) {
341
- case "pnpm": return `pnpm dlx ${args.join(" ")}`;
342
- case "yarn": return `yarn dlx ${args.join(" ")}`;
343
- case "bun": return `bunx ${args.join(" ")}`;
344
- case "deno": return `deno run -A npm:${args.join(" ")}`;
345
- case "npm": return `npx ${args.join(" ")}`;
346
- }
347
- }
348
- /**
349
- * Parse the project-pm-formatted command into an exec call. The
350
- * format-then-parse split keeps the user-facing command string the same
351
- * as the surface the structured error advertises, so a user who copies
352
- * the error's `fix` line gets the same invocation that init just
353
- * attempted. Single quotes are preserved in the display form so `*` is
354
- * safe to copy into a shell, then stripped before `execFile`.
355
- */
356
- function commandToExec(command) {
357
- const tokens = (command.match(/'[^']*'|\S+/g) ?? []).map((token) => token.startsWith("'") && token.endsWith("'") ? token.slice(1, -1) : token);
358
- return {
359
- file: tokens[0] ?? "npx",
360
- args: tokens.slice(1)
361
- };
362
- }
363
- /**
364
- * Runs the project-level skill install for every source in
365
- * `DEFAULT_AGENT_SKILL_SOURCES`, in order. Returns
366
- * `{ ok: true, commands }` on success; throws a structured
367
- * `errorInitSkillInstallFailed` on the first failure (subsequent
368
- * sources are not attempted — the user opted into Prisma Next by
369
- * running `init` and a partial install would leave the project in an
370
- * ambiguous state). The throw is intentionally fatal — project-level
371
- * skill install is unconditional (modulo `--no-skill`).
372
- */
373
- async function runProjectLevelSkillInstall(ctx) {
374
- const commands = [];
375
- const installCommands = DEFAULT_AGENT_SKILL_SOURCES.flatMap((source) => [formatSkillInstallCommand(ctx.pm, source), formatClaudeSkillInstallCommand(ctx.pm, source)]);
376
- for (const command of installCommands) {
377
- const { file, args } = commandToExec(command);
378
- try {
379
- await exec(file, args, { cwd: ctx.baseDir });
380
- commands.push(command);
381
- } catch (err) {
382
- throw errorInitSkillInstallFailed({
383
- skillInstallCommand: command,
384
- filesWritten: ctx.filesWritten,
385
- cause: redactSecrets$1(readChildStderr$1(err)) || (err instanceof Error ? err.message : String(err))
386
- });
387
- }
388
- }
389
- return {
390
- ok: true,
391
- commands
392
- };
393
- }
394
- function readChildStderr$1(err) {
395
- if (err instanceof Error && "stderr" in err) return String(err.stderr ?? "");
396
- return "";
397
- }
398
- /**
399
- * Strips credentials from a `scheme://user:pass@host/...` URL anywhere
400
- * in `stderr`. Package-manager stderr regularly contains credentialed
401
- * registry URLs (private npm registries, GitHub Packages tokens), and
402
- * those bubble into the structured `errorInitSkillInstallFailed`
403
- * envelope, which ends up in logs and CI output. Redact at the
404
- * boundary so we never re-emit a secret.
405
- *
406
- * Exported for unit tests.
407
- */
408
- function redactSecrets$1(stderr) {
409
- if (!stderr) return stderr;
410
- return stderr.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^/@\s]+)@/g, "$1***@");
411
- }
412
- /**
413
- * Hand-rolled skill stub path that init must not leave behind. Removed
414
- * on every init run so a project's `.agents/skills/prisma-next/` does
415
- * not shadow the installed Prisma Next skill cluster.
416
- */
417
- const LEGACY_SKILL_FILE = ".agents/skills/prisma-next/SKILL.md";
418
- //#endregion
419
- //#region src/commands/init/detect-package-manager.ts
420
- const KNOWN = new Set([
421
- "pnpm",
422
- "npm",
423
- "yarn",
424
- "bun",
425
- "deno"
426
- ]);
427
- /**
428
- * Resolves the package manager `init` should drive for `add` / `install`
429
- * commands. Tries, in order:
430
- *
431
- * 1. **`detect()`** — walks up from `cwd` looking for a lockfile, the
432
- * `packageManager` field, the `devEngines.packageManager` field, or
433
- * install metadata. This is the right answer whenever the user is
434
- * anywhere inside an existing project, including a deep workspace
435
- * subdirectory.
436
- *
437
- * 2. **`getUserAgent()`** — parses `npm_config_user_agent`, the env var
438
- * every PM sets when it spawns a script. This catches the
439
- * bare-directory case where there's no project to walk up to but the
440
- * user invoked us via `pnpm dlx prisma-next init` / `bunx
441
- * prisma-next init` / `yarn dlx …`. Same signal used by every
442
- * `create-*` tool in the ecosystem (`create-vite`, `create-next-app`,
443
- * `create-astro`, `@antfu/ni`, …).
444
- *
445
- * 3. **`npm`** — final fallback. Always present alongside Node.
446
- */
447
- async function detectPackageManager(cwd) {
448
- const detected = await detect({ cwd });
449
- if (detected && KNOWN.has(detected.name)) return detected.name;
450
- const userAgent = getUserAgent();
451
- if (userAgent !== null && KNOWN.has(userAgent)) return userAgent;
452
- return "npm";
453
- }
454
- function hasProjectManifest(cwd) {
455
- return existsSync(join(cwd, "package.json")) || existsSync(join(cwd, "deno.json")) || existsSync(join(cwd, "deno.jsonc"));
456
- }
457
- function formatRunCommand(pm, bin, args) {
458
- if (pm === "npm") return `npx ${bin} ${args}`;
459
- if (pm === "deno") return `deno run npm:${bin} ${args}`;
460
- return `${pm} ${bin} ${args}`;
461
- }
462
- function formatAddArgs(pm, packages) {
463
- if (pm === "deno") return ["add", ...packages.map((p) => `npm:${p}`)];
464
- return ["add", ...packages];
465
- }
466
- function formatAddDevArgs(pm, packages) {
467
- if (pm === "deno") return [
468
- "add",
469
- "--dev",
470
- ...packages.map((p) => `npm:${p}`)
471
- ];
472
- return [
473
- "add",
474
- "-D",
475
- ...packages
476
- ];
477
- }
478
- //#endregion
479
- //#region src/commands/init/detect-pnpm-catalog.ts
480
- /**
481
- * Walks up from `baseDir` looking for `pnpm-workspace.yaml`, then scans
482
- * its top-level `catalog:` block for entries that match any of `packages`.
483
- *
484
- * Implements FR7.3 / Spec Decision 8 (honour-and-warn): when `init` runs
485
- * inside a pnpm workspace whose catalog overrides one of the packages it
486
- * installs, surface a structured warning so the user knows the catalog
487
- * version (not the published `latest`) is what ended up in their
488
- * `node_modules`. pnpm itself does this silently; the warning closes the
489
- * "looks fine, must be wrong version six months later" gap.
490
- *
491
- * Notes / scope:
492
- *
493
- * - We only inspect the unnamed top-level `catalog:` block. pnpm also
494
- * supports `catalogs:` (plural — *named* catalogs referenced via
495
- * `catalog:foo` specifiers); those don't apply to a vanilla
496
- * `pnpm add prisma-next` invocation, so we skip them.
497
- * - We don't validate YAML syntax exhaustively. The file format pnpm
498
- * ships is line-oriented and well-known; a minimal regex is more
499
- * robust than depending on a YAML parser for one warning.
500
- * - We don't compare against the registry's `latest` — pnpm uses the
501
- * catalog version regardless, so the warning fires whenever a match
502
- * exists. The user-facing copy explains how to opt out.
503
- */
504
- function detectPnpmCatalogOverrides(baseDir, packages) {
505
- const workspaceFile = findNearestPnpmWorkspaceFile(baseDir);
506
- if (workspaceFile === null) return null;
507
- const catalog = extractCatalogBlock(readFileSync(workspaceFile, "utf-8"));
508
- if (catalog === null) return {
509
- workspaceFile,
510
- entries: []
511
- };
512
- const wanted = new Set(packages);
513
- const entries = [];
514
- for (const [name, version] of catalog) if (wanted.has(name)) entries.push({
515
- name,
516
- version
346
+ filesWritten: options.filesWritten,
347
+ cause: options.cause
348
+ }
517
349
  });
518
- return {
519
- workspaceFile,
520
- entries
521
- };
522
350
  }
523
- function findNearestPnpmWorkspaceFile(baseDir) {
524
- let dir = baseDir;
525
- let prev = "";
526
- while (dir !== prev) {
527
- const candidate = join(dir, "pnpm-workspace.yaml");
528
- if (existsSync(candidate)) return candidate;
529
- prev = dir;
530
- dir = dirname(dir);
531
- }
532
- return null;
351
+ /**
352
+ * `prisma-next contract emit` failed after a successful install. Surface
353
+ * the underlying error so the user can fix it and re-run; files and
354
+ * dependencies remain on disk untouched. Maps to exit code
355
+ * `5 = EMIT_FAILED`.
356
+ */
357
+ function errorInitEmitFailed(options) {
358
+ return new CliStructuredError("5008", "Failed to emit contract", {
359
+ domain: "CLI",
360
+ why: `\`prisma-next contract emit\` failed: ${options.cause}`,
361
+ fix: `Inspect your contract file, fix the underlying issue, then re-run \`${options.emitCommand}\`. Pass \`-v\` for the full error envelope.`,
362
+ docsUrl: "https://prisma-next.dev/docs/cli/contract-emit",
363
+ meta: {
364
+ filesWritten: options.filesWritten,
365
+ cause: options.cause
366
+ }
367
+ });
533
368
  }
534
369
  /**
535
- * Returns the entries inside the top-level `catalog:` block as `[name, version]`
536
- * pairs in document order, or `null` when no `catalog:` block exists.
370
+ * The project-level skills install (`npx skills add
371
+ * prisma/prisma-next#v<version>`) failed after a successful dependency
372
+ * install + emit. The project's scaffold remains on disk; the user
373
+ * can either fix the underlying issue (network, registry, PATH) and
374
+ * run the install command manually, or re-run `init --no-skill` to
375
+ * proceed without the skill.
537
376
  *
538
- * The parser is intentionally minimal: it reads line-by-line, locates the
539
- * top-level `catalog:` line (no leading whitespace), then collects every
540
- * subsequent indented line of the form `<key>: <value>` until the next
541
- * top-level key (or end of file). Quotes around `<key>` and `<value>`
542
- * are stripped; comments (`#…`) are ignored.
377
+ * Non-rolling-back, matching the existing install/emit failure
378
+ * semantics. Maps to exit code `6 = SKILL_INSTALL_FAILED`.
543
379
  */
544
- function extractCatalogBlock(contents) {
545
- const lines = contents.split(/\r?\n/);
546
- const startIdx = lines.findIndex((line) => /^catalog\s*:\s*$/.test(line));
547
- if (startIdx === -1) return null;
548
- const entries = [];
549
- for (let i = startIdx + 1; i < lines.length; i++) {
550
- const raw = lines[i] ?? "";
551
- if (raw.trim() === "" || /^\s*#/.test(raw)) continue;
552
- if (!/^\s/.test(raw)) break;
553
- const match = raw.match(/^\s+(?:'([^']+)'|"([^"]+)"|([^:\s'"]+))\s*:\s*(.*?)\s*(?:#.*)?$/);
554
- if (!match) continue;
555
- const name = match[1] ?? match[2] ?? match[3];
556
- if (name === void 0) continue;
557
- const version = stripQuotes((match[4] ?? "").trim());
558
- if (version === "") continue;
559
- entries.push([name, version]);
560
- }
561
- return entries;
562
- }
563
- function stripQuotes(value) {
564
- if (value.length >= 2) {
565
- const first = value[0];
566
- const last = value[value.length - 1];
567
- if (first === "\"" && last === "\"" || first === "'" && last === "'") return value.slice(1, -1);
568
- }
569
- return value;
380
+ function errorInitSkillInstallFailed(options) {
381
+ return new CliStructuredError("5013", "Failed to install Prisma Next skills", {
382
+ domain: "CLI",
383
+ why: `\`${options.skillInstallCommand}\` exited with an error: ${options.cause}`,
384
+ fix: `Either:
385
+ - Re-run \`prisma-next init --no-skill${options.filesWritten.length > 0 ? " --force" : ""}\` to skip the skill install for this run, or\n - Fix the underlying issue (network, npm registry, \`npx skills\` on PATH) and install manually:\n ${options.skillInstallCommand}`,
386
+ docsUrl: "https://prisma-next.dev/docs/cli/init#skills",
387
+ meta: {
388
+ filesWritten: options.filesWritten,
389
+ skillInstallCommand: options.skillInstallCommand,
390
+ cause: options.cause
391
+ }
392
+ });
570
393
  }
571
394
  //#endregion
572
395
  //#region src/commands/init/hygiene-gitattributes.ts
@@ -1519,6 +1342,183 @@ function removeDependency(existing, depName) {
1519
1342
  return `${JSON.stringify(parsed, null, 2)}${trailingNewline}`;
1520
1343
  }
1521
1344
  //#endregion
1345
+ //#region src/commands/init/skill-install.ts
1346
+ const exec = promisify(execFile);
1347
+ /**
1348
+ * Default base for the GitHub-URL form `<owner>/<repo>` consumed by
1349
+ * upstream `skills add`. Each `SkillSource` joins this base with its
1350
+ * own subpath (and optional `#ref` for version-pinned clusters).
1351
+ */
1352
+ const DEFAULT_SKILL_BASE = "prisma/prisma-next";
1353
+ const DEFAULT_SKILL_SOURCES = [
1354
+ {
1355
+ subpath: "skills",
1356
+ ref: "cli",
1357
+ description: "usage skills (version-locked to installed Prisma Next)"
1358
+ },
1359
+ {
1360
+ subpath: "skills/upgrade",
1361
+ ref: null,
1362
+ description: "upgrade skill (always tracks `main`)"
1363
+ },
1364
+ {
1365
+ subpath: "skills/extension-author",
1366
+ ref: null,
1367
+ description: "extension-author skill (always tracks `main`)"
1368
+ }
1369
+ ];
1370
+ /**
1371
+ * Test-only escape hatch for pinning the install base to a local
1372
+ * checkout. Production runs leave this unset, so installs always use
1373
+ * `DEFAULT_SKILL_BASE`.
1374
+ *
1375
+ * When set to an absolute filesystem path (typical for tests), the
1376
+ * `#ref` fragment is dropped — local-path mode in upstream's CLI does
1377
+ * not accept refs, and the local clone has whatever content the test
1378
+ * checked into it anyway. When set to anything else (e.g. a fork name
1379
+ * `myuser/prisma-next`), the ref policy is preserved.
1380
+ */
1381
+ function resolveAgentSkillBase() {
1382
+ const override = process.env["PRISMA_NEXT_SKILLS_BASE"]?.trim();
1383
+ return override && override.length > 0 ? override : DEFAULT_SKILL_BASE;
1384
+ }
1385
+ function isLocalPath(base) {
1386
+ return base.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(base);
1387
+ }
1388
+ /**
1389
+ * Build the `<base>/<subpath>[#ref]` URL the `skills` CLI will
1390
+ * resolve. Exported for unit tests so the per-source format can be
1391
+ * asserted without going through the full install loop.
1392
+ */
1393
+ function formatSkillSourceUrl(source) {
1394
+ const base = resolveAgentSkillBase();
1395
+ const url = `${base}/${source.subpath}`;
1396
+ if (source.ref === null) return url;
1397
+ if (isLocalPath(base)) return url;
1398
+ if (source.ref === "cli") return `${url}#v${version}`;
1399
+ return url;
1400
+ }
1401
+ /**
1402
+ * The skill-install command for one source, formatted for the
1403
+ * project's detected package manager. `npx`/`pnpm dlx`/`bunx` are
1404
+ * interchangeable to the user; we pick the variant that matches the
1405
+ * rest of the install step so a single project consistently uses one
1406
+ * runner.
1407
+ *
1408
+ * `--all` auto-selects every skill in the cluster and every detected
1409
+ * agent runtime, skipping the multi-select prompts the `skills` CLI
1410
+ * shows by default. A non-interactive scaffold step cannot present
1411
+ * prompts.
1412
+ *
1413
+ * Exported for unit tests so the per-PM dispatch can be asserted
1414
+ * without a live subprocess.
1415
+ */
1416
+ function formatSkillInstallCommand(pm, source) {
1417
+ return formatPackageManagerCommand(pm, [
1418
+ "skills@latest",
1419
+ "add",
1420
+ formatSkillSourceUrl(source),
1421
+ "--all"
1422
+ ]);
1423
+ }
1424
+ /**
1425
+ * `skills add --all` should cover Claude Code, but upstream currently skips
1426
+ * project-local Claude symlinks when `.claude/` does not already exist. Run
1427
+ * the explicit Claude Code install as well so fresh projects get
1428
+ * `.claude/skills` without asking users to create that folder first.
1429
+ */
1430
+ function formatClaudeSkillInstallCommand(pm, source) {
1431
+ return formatPackageManagerCommand(pm, [
1432
+ "skills@latest",
1433
+ "add",
1434
+ formatSkillSourceUrl(source),
1435
+ "--agent",
1436
+ "claude-code",
1437
+ "--skill",
1438
+ "'*'",
1439
+ "-y"
1440
+ ]);
1441
+ }
1442
+ function formatPackageManagerCommand(pm, args) {
1443
+ switch (pm) {
1444
+ case "pnpm": return `pnpm dlx ${args.join(" ")}`;
1445
+ case "yarn": return `yarn dlx ${args.join(" ")}`;
1446
+ case "bun": return `bunx ${args.join(" ")}`;
1447
+ case "deno": return `deno run -A npm:${args.join(" ")}`;
1448
+ case "npm": return `npx ${args.join(" ")}`;
1449
+ }
1450
+ }
1451
+ /**
1452
+ * Parse the project-pm-formatted command into an exec call. The
1453
+ * format-then-parse split keeps the user-facing command string the same
1454
+ * as the surface the structured error advertises, so a user who copies
1455
+ * the error's `fix` line gets the same invocation that init just
1456
+ * attempted. Single quotes are preserved in the display form so `*` is
1457
+ * safe to copy into a shell, then stripped before `execFile`.
1458
+ */
1459
+ function commandToExec(command) {
1460
+ const tokens = (command.match(/'[^']*'|\S+/g) ?? []).map((token) => token.startsWith("'") && token.endsWith("'") ? token.slice(1, -1) : token);
1461
+ return {
1462
+ file: tokens[0] ?? "npx",
1463
+ args: tokens.slice(1)
1464
+ };
1465
+ }
1466
+ /**
1467
+ * Runs the project-level skill install for every source in
1468
+ * `DEFAULT_SKILL_SOURCES`, in order. Returns
1469
+ * `{ ok: true, commands }` on success; throws a structured
1470
+ * `errorInitSkillInstallFailed` on the first failure (subsequent
1471
+ * sources are not attempted — the user opted into Prisma Next by
1472
+ * running `init` and a partial install would leave the project in an
1473
+ * ambiguous state). The throw is intentionally fatal — project-level
1474
+ * skill install is unconditional (modulo `--no-skill`).
1475
+ */
1476
+ async function runProjectLevelSkillInstall(ctx) {
1477
+ const commands = [];
1478
+ const installCommands = DEFAULT_SKILL_SOURCES.flatMap((source) => [formatSkillInstallCommand(ctx.pm, source), formatClaudeSkillInstallCommand(ctx.pm, source)]);
1479
+ for (const command of installCommands) {
1480
+ const { file, args } = commandToExec(command);
1481
+ try {
1482
+ await exec(file, args, { cwd: ctx.baseDir });
1483
+ commands.push(command);
1484
+ } catch (err) {
1485
+ throw errorInitSkillInstallFailed({
1486
+ skillInstallCommand: command,
1487
+ filesWritten: ctx.filesWritten,
1488
+ cause: redactSecrets$1(readChildStderr$1(err)) || (err instanceof Error ? err.message : String(err))
1489
+ });
1490
+ }
1491
+ }
1492
+ return {
1493
+ ok: true,
1494
+ commands
1495
+ };
1496
+ }
1497
+ function readChildStderr$1(err) {
1498
+ if (err instanceof Error && "stderr" in err) return String(err.stderr ?? "");
1499
+ return "";
1500
+ }
1501
+ /**
1502
+ * Strips credentials from a `scheme://user:pass@host/...` URL anywhere
1503
+ * in `stderr`. Package-manager stderr regularly contains credentialed
1504
+ * registry URLs (private npm registries, GitHub Packages tokens), and
1505
+ * those bubble into the structured `errorInitSkillInstallFailed`
1506
+ * envelope, which ends up in logs and CI output. Redact at the
1507
+ * boundary so we never re-emit a secret.
1508
+ *
1509
+ * Exported for unit tests.
1510
+ */
1511
+ function redactSecrets$1(stderr) {
1512
+ if (!stderr) return stderr;
1513
+ return stderr.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^/@\s]+)@/g, "$1***@");
1514
+ }
1515
+ /**
1516
+ * Hand-rolled skill stub path that init must not leave behind. Removed
1517
+ * on every init run so a project's `.agents/skills/prisma-next/` does
1518
+ * not shadow the installed Prisma Next skill cluster.
1519
+ */
1520
+ const LEGACY_SKILL_FILE = ".agents/skills/prisma-next/SKILL.md";
1521
+ //#endregion
1522
1522
  //#region src/commands/init/templates/env.ts
1523
1523
  /**
1524
1524
  * The minimum supported server version for each target (FR8.1). The
@@ -1790,7 +1790,7 @@ function mergeTypesArray(existing) {
1790
1790
  * structured CLI errors raised at every phase (input resolution, install,
1791
1791
  * emit) and renders them via the same UI surface as success output
1792
1792
  * (`--json` to stdout, human to stderr). Exit codes follow the documented
1793
- * stable set in `./exit-codes.ts` (FR1.6) and the
1793
+ * stable set in `./exit-codes.ts` and the
1794
1794
  * [Style Guide § Exit Codes](../../../../../../../docs/CLI%20Style%20Guide.md#exit-codes).
1795
1795
  *
1796
1796
  * Layered for testability: the action handler in `./index.ts` is
@@ -1991,10 +1991,10 @@ async function runInit(baseDir, runOptions) {
1991
1991
  filesWritten
1992
1992
  }));
1993
1993
  }
1994
- const manualProjectSkillSummary = DEFAULT_AGENT_SKILL_SOURCES.flatMap((source) => [formatSkillInstallCommand(install.effectivePm, source), formatClaudeSkillInstallCommand(install.effectivePm, source)]).map((c) => `\`${c}\``).join(" && ");
1994
+ const manualProjectSkillSummary = DEFAULT_SKILL_SOURCES.flatMap((source) => [formatSkillInstallCommand(install.effectivePm, source), formatClaudeSkillInstallCommand(install.effectivePm, source)]).map((c) => `\`${c}\``).join(" && ");
1995
1995
  let skillRegistered = false;
1996
- if (!inputs.installProjectSkill) warnings.push(`Skipped Prisma Next agent-skill install (--no-skill). To install the skills later, run: ${manualProjectSkillSummary}`);
1997
- else if (install.skipped) warnings.push(`Skipped Prisma Next agent-skill install because --no-install was passed. After you run install manually, install the skills with: ${manualProjectSkillSummary}`);
1996
+ if (!inputs.installProjectSkill) warnings.push(`Skipped Prisma Next skills install (--no-skill). To install the skills later, run: ${manualProjectSkillSummary}`);
1997
+ else if (install.skipped) warnings.push(`Skipped Prisma Next skills install because --no-install was passed. After you run install manually, install the skills with: ${manualProjectSkillSummary}`);
1998
1998
  else {
1999
1999
  const spinner = ui.spinner();
2000
2000
  spinner.start("Registering Prisma Next skills with the agent runtime...");
@@ -2360,4 +2360,4 @@ function sanitisePackageName(raw) {
2360
2360
  //#endregion
2361
2361
  export { runInit };
2362
2362
 
2363
- //# sourceMappingURL=init-DBRWZlFU.mjs.map
2363
+ //# sourceMappingURL=init-BHbEOgNr.mjs.map