@prisma/orm-toolchain 8.0.0-rc.1-dev.38 → 8.0.0-rc.1-dev.39
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin__prisma-next.mjs +8 -301
- package/dist/bin__prisma-next.mjs.map +1 -1
- package/dist/cli.mjs +1944 -256
- package/dist/cli.mjs.map +1 -1
- package/dist/cli__commands__migrate.mjs +1 -1
- package/dist/cli__commands__migration-check.d.mts +1 -1
- package/dist/cli__commands__migration-check.mjs +1 -1
- package/dist/cli__commands__migration-graph.d.mts +1 -1
- package/dist/cli__commands__migration-graph.mjs +1 -1
- package/dist/cli__commands__migration-list.d.mts +1 -1
- package/dist/cli__commands__migration-list.mjs +1 -1
- package/dist/cli__commands__migration-log.d.mts +1 -1
- package/dist/cli__commands__migration-log.mjs +1 -1
- package/dist/cli__commands__migration-show.d.mts +1 -1
- package/dist/cli__commands__migration-status.d.mts +1 -1
- package/dist/cli__commands__migration-status.mjs +2 -2
- package/dist/cli__control-api.d.mts +1 -1
- package/dist/cli__control-api.mjs +1 -1
- package/dist/cli__init-output.d.mts +1 -1
- package/dist/cli__init-output.mjs +1 -1
- package/dist/code-templates-CSLH8721-CWRA4Lru.mjs +295 -0
- package/dist/code-templates-CSLH8721-CWRA4Lru.mjs.map +1 -0
- package/dist/init-CcJxAIeS-CITTa8YU.mjs +800 -0
- package/dist/init-CcJxAIeS-CITTa8YU.mjs.map +1 -0
- package/dist/{migrate-CgSnzAUm-aGstaO5N.mjs → migrate-BVEyoBor-CN_EOCzx.mjs} +3 -3
- package/dist/{migrate-CgSnzAUm-aGstaO5N.mjs.map → migrate-BVEyoBor-CN_EOCzx.mjs.map} +1 -1
- package/dist/{migrate-failure-CnoUcDms-taa9pfd7.mjs → migrate-failure-CKMEnA_x-ByHkYjn5.mjs} +3 -3
- package/dist/{migrate-failure-CnoUcDms-taa9pfd7.mjs.map → migrate-failure-CKMEnA_x-ByHkYjn5.mjs.map} +1 -1
- package/dist/{migration-graph-D2dbLCkl-CCHL0aDA.mjs → migration-graph-D4sgTiXa-BLtiLIk7.mjs} +4 -4
- package/dist/{migration-graph-D2dbLCkl-CCHL0aDA.mjs.map → migration-graph-D4sgTiXa-BLtiLIk7.mjs.map} +1 -1
- package/dist/{migration-graph-command-render-BeuEyf2B-BNH9oUB_.mjs → migration-graph-command-render-h0VdTkSM-C3rPZEw1.mjs} +17 -3
- package/dist/migration-graph-command-render-h0VdTkSM-C3rPZEw1.mjs.map +1 -0
- package/dist/{migration-graph-sections-0lKA5l1b-CTHQquu7.mjs → migration-graph-sections-B8WSWSkJ-CyaRo0T6.mjs} +3 -3
- package/dist/{migration-graph-sections-0lKA5l1b-CTHQquu7.mjs.map → migration-graph-sections-B8WSWSkJ-CyaRo0T6.mjs.map} +1 -1
- package/dist/{migration-list-DCx0KOFp-B3MiGnpa.mjs → migration-list-BIL-re1m-CZY4m3d0.mjs} +3 -3
- package/dist/{migration-list-DCx0KOFp-B3MiGnpa.mjs.map → migration-list-BIL-re1m-CZY4m3d0.mjs.map} +1 -1
- package/dist/{migration-log-CJ6_9_uj-uWyc3vlC.mjs → migration-log-BdBKYAUW-DTPxKZtD.mjs} +4 -4
- package/dist/{migration-log-CJ6_9_uj-uWyc3vlC.mjs.map → migration-log-BdBKYAUW-DTPxKZtD.mjs.map} +1 -1
- package/dist/{migration-log-table-DRiOnvzf-zTQBThqe.mjs → migration-log-table-eES9lPjv-BCFm8qJr.mjs} +3 -3
- package/dist/{migration-log-table-DRiOnvzf-zTQBThqe.mjs.map → migration-log-table-eES9lPjv-BCFm8qJr.mjs.map} +1 -1
- package/dist/{migration-status-S88DjM8c-DLHnCKfW.mjs → migration-status-DzPVkkB3-BA7jjITd.mjs} +5 -4
- package/dist/{migration-status-S88DjM8c-DLHnCKfW.mjs.map → migration-status-DzPVkkB3-BA7jjITd.mjs.map} +1 -1
- package/dist/{output-DWNYqZGc-D2sRgLAm.mjs → output-DP3woAjV-BxCr9nHA.mjs} +21 -11
- package/dist/output-DP3woAjV-BxCr9nHA.mjs.map +1 -0
- package/dist/{init-Bco4QkKJ-DqUZVrBU.mjs → redact-secrets-kQ0KKV1u-MnVSKe-u.mjs} +740 -1516
- package/dist/redact-secrets-kQ0KKV1u-MnVSKe-u.mjs.map +1 -0
- package/dist/{schemas-Bm2e3Hk1-qLKF0B36.d.mts → schemas-BGgjiKV0-Rh8RlMCl.d.mts} +4 -2
- package/dist/schemas-BGgjiKV0-Rh8RlMCl.d.mts.map +1 -0
- package/dist/{schemas-BMkqaqvM-CLaqlat4.mjs → schemas-CYSD83Cu-Bff4PPoi.mjs} +3 -2
- package/dist/schemas-CYSD83Cu-Bff4PPoi.mjs.map +1 -0
- package/package.json +13 -13
- package/dist/init-Bco4QkKJ-DqUZVrBU.mjs.map +0 -1
- package/dist/migration-graph-command-render-BeuEyf2B-BNH9oUB_.mjs.map +0 -1
- package/dist/output-DWNYqZGc-D2sRgLAm.mjs.map +0 -1
- package/dist/schemas-BMkqaqvM-CLaqlat4.mjs.map +0 -1
- package/dist/schemas-Bm2e3Hk1-qLKF0B36.d.mts.map +0 -1
|
@@ -1,91 +1,17 @@
|
|
|
1
1
|
import { t as CliStructuredError$1 } from "./cli-errors-BG6bItjG-DwdKIeGz.mjs";
|
|
2
|
-
import { a as
|
|
3
|
-
import { i as renderInitOutro, n as buildNextSteps, r as formatInitJson, t as InitOutputSchema } from "./output-DWNYqZGc-D2sRgLAm.mjs";
|
|
4
|
-
import { a as schemaSample, c as targetLabel, i as scaffoldSpecifierResolverFor, l as targetPackageName, n as dbFile, o as starterSchema, r as defaultSchemaPath, s as targetEntrypoint, t as configFile, u as version } from "./bin__prisma-next.mjs";
|
|
2
|
+
import { a as schemaSample, l as targetPackageName, s as targetEntrypoint, u as version } from "./code-templates-CSLH8721-CWRA4Lru.mjs";
|
|
5
3
|
import { createRequire } from "node:module";
|
|
6
4
|
import { CliStructuredError } from "@prisma/orm-framework/errors/control";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
5
|
+
import { ifDefined } from "@prisma/orm-framework/utils/defined";
|
|
6
|
+
import { dirname, extname, join, normalize } from "pathe";
|
|
7
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
9
8
|
import { docsUrlFor } from "@prisma/orm-framework/utils/structured-error";
|
|
10
9
|
import { keepInternalSpecifiers } from "@prisma/orm-framework/components/emission";
|
|
11
10
|
import { blindCast } from "@prisma/orm-framework/utils/casts";
|
|
12
11
|
import { InternalError } from "@prisma/orm-framework/utils/internal-error";
|
|
13
|
-
import * as clack from "@clack/prompts";
|
|
14
|
-
import { execFile } from "node:child_process";
|
|
15
|
-
import { promisify } from "node:util";
|
|
16
12
|
import { detect, getUserAgent } from "package-manager-detector/detect";
|
|
17
13
|
import { applyEdits, modify, parse, printParseErrorCode } from "jsonc-parser";
|
|
18
|
-
//#region ../../../1-framework/3-tooling/cli/dist/
|
|
19
|
-
const KNOWN = /* @__PURE__ */ new Set([
|
|
20
|
-
"pnpm",
|
|
21
|
-
"npm",
|
|
22
|
-
"yarn",
|
|
23
|
-
"bun",
|
|
24
|
-
"deno"
|
|
25
|
-
]);
|
|
26
|
-
function isPackageManager(name) {
|
|
27
|
-
return KNOWN.has(name);
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Resolves the package manager `init` should drive for `add` / `install`
|
|
31
|
-
* commands. Tries, in order:
|
|
32
|
-
*
|
|
33
|
-
* 1. **`detect()`** — walks up from `cwd` looking for a lockfile, the
|
|
34
|
-
* `packageManager` field, the `devEngines.packageManager` field, or
|
|
35
|
-
* install metadata. This is the right answer whenever the user is
|
|
36
|
-
* anywhere inside an existing project, including a deep workspace
|
|
37
|
-
* subdirectory.
|
|
38
|
-
*
|
|
39
|
-
* 2. **`getUserAgent()`** — parses `npm_config_user_agent`, the env var
|
|
40
|
-
* every PM sets when it spawns a script. This catches the
|
|
41
|
-
* bare-directory case where there's no project to walk up to but the
|
|
42
|
-
* user invoked us via `pnpm dlx prisma-next init` / `bunx
|
|
43
|
-
* prisma-next init` / `yarn dlx …`. Same signal used by every
|
|
44
|
-
* `create-*` tool in the ecosystem (`create-vite`, `create-next-app`,
|
|
45
|
-
* `create-astro`, `@antfu/ni`, …).
|
|
46
|
-
*
|
|
47
|
-
* 3. **`npm`** — final fallback. Always present alongside Node.
|
|
48
|
-
*/
|
|
49
|
-
async function detectPackageManager(cwd) {
|
|
50
|
-
const detected = await detect({ cwd });
|
|
51
|
-
if (detected && isPackageManager(detected.name)) return detected.name;
|
|
52
|
-
const userAgent = getUserAgent();
|
|
53
|
-
if (userAgent !== null && isPackageManager(userAgent)) return userAgent;
|
|
54
|
-
return "npm";
|
|
55
|
-
}
|
|
56
|
-
function hasProjectManifest(cwd) {
|
|
57
|
-
return existsSync(join(cwd, "package.json")) || existsSync(join(cwd, "deno.json")) || existsSync(join(cwd, "deno.jsonc"));
|
|
58
|
-
}
|
|
59
|
-
function formatRunCommand(pm, bin, args) {
|
|
60
|
-
if (pm === "npm") return `npx ${bin} ${args}`;
|
|
61
|
-
if (pm === "deno") return `deno run npm:${bin} ${args}`;
|
|
62
|
-
return `${pm} ${bin} ${args}`;
|
|
63
|
-
}
|
|
64
|
-
function formatRunScriptCommand(pm, scriptName) {
|
|
65
|
-
switch (pm) {
|
|
66
|
-
case "deno": return `deno task ${scriptName}`;
|
|
67
|
-
case "bun": return `bun run ${scriptName}`;
|
|
68
|
-
case "pnpm": return `pnpm run ${scriptName}`;
|
|
69
|
-
case "yarn": return `yarn run ${scriptName}`;
|
|
70
|
-
default: return `npm run ${scriptName}`;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
function formatAddArgs(pm, packages) {
|
|
74
|
-
if (pm === "deno") return ["add", ...packages.map((p) => `npm:${p}`)];
|
|
75
|
-
return ["add", ...packages];
|
|
76
|
-
}
|
|
77
|
-
function formatAddDevArgs(pm, packages) {
|
|
78
|
-
if (pm === "deno") return [
|
|
79
|
-
"add",
|
|
80
|
-
"--dev",
|
|
81
|
-
...packages.map((p) => `npm:${p}`)
|
|
82
|
-
];
|
|
83
|
-
return [
|
|
84
|
-
"add",
|
|
85
|
-
"-D",
|
|
86
|
-
...packages
|
|
87
|
-
];
|
|
88
|
-
}
|
|
14
|
+
//#region ../../../1-framework/3-tooling/cli/dist/redact-secrets-kQ0KKV1u.mjs
|
|
89
15
|
/**
|
|
90
16
|
* Walks up from `baseDir` looking for `pnpm-workspace.yaml`, then scans
|
|
91
17
|
* its top-level `catalog:` block for entries that match any of `packages`.
|
|
@@ -177,6 +103,25 @@ function stripQuotes(value) {
|
|
|
177
103
|
}
|
|
178
104
|
return value;
|
|
179
105
|
}
|
|
106
|
+
function formatCatalogWarning(workspaceFile, entries) {
|
|
107
|
+
return [
|
|
108
|
+
"pnpm workspace catalog overrides detected — pnpm will install these versions instead of `latest`:",
|
|
109
|
+
entries.map((entry) => ` • ${entry.name}: ${entry.version}`).join("\n"),
|
|
110
|
+
`Catalog source: ${workspaceFile}`,
|
|
111
|
+
"To use the published `latest` instead, remove or update the catalog entry, then re-run `pnpm install`."
|
|
112
|
+
].join("\n");
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Honour-and-warn: when the surrounding pnpm workspace pins one of the
|
|
116
|
+
* packages `init` installs through its catalog, say so — the catalog version,
|
|
117
|
+
* not the published `latest`, is what ends up in the project. Empty when there
|
|
118
|
+
* is no workspace above the project or its catalog names none of them.
|
|
119
|
+
*/
|
|
120
|
+
function buildCatalogWarnings(baseDir, packages) {
|
|
121
|
+
const result = detectPnpmCatalogOverrides(baseDir, packages);
|
|
122
|
+
if (result === null || result.entries.length === 0) return [];
|
|
123
|
+
return [formatCatalogWarning(result.workspaceFile, result.entries)];
|
|
124
|
+
}
|
|
180
125
|
/**
|
|
181
126
|
* Re-init in non-interactive mode without `--force`. Distinct from the
|
|
182
127
|
* decline-the-prompt path (which is `errorInitUserAborted`) because here
|
|
@@ -388,6 +333,26 @@ function errorInitEmitFailed(options) {
|
|
|
388
333
|
});
|
|
389
334
|
}
|
|
390
335
|
/**
|
|
336
|
+
* A scaffold file could not be written after earlier writes had already
|
|
337
|
+
* landed. The directory is half-scaffolded, so this carries the list of what
|
|
338
|
+
* did get written, the way every other post-write failure in `init` does.
|
|
339
|
+
*
|
|
340
|
+
* Maps to exit code `2 = PRECONDITION`: what stopped the write is something
|
|
341
|
+
* about the directory the user can fix.
|
|
342
|
+
*/
|
|
343
|
+
function errorInitWriteFailed(options) {
|
|
344
|
+
return new CliStructuredError$1("CLI.INIT_WRITE_FAILED", `Failed to write ${options.path}`, {
|
|
345
|
+
why: `\`${options.path}\` could not be written: ${options.cause}`,
|
|
346
|
+
fix: "Fix what stopped the write — a directory sitting where the file goes, permissions, a full disk — then run `prisma-next init` again. It will ask you to confirm replacing the files this run already wrote (listed in `meta.filesWritten`).",
|
|
347
|
+
docsUrl: docsUrlFor("CLI.INIT_WRITE_FAILED"),
|
|
348
|
+
meta: {
|
|
349
|
+
path: options.path,
|
|
350
|
+
cause: options.cause,
|
|
351
|
+
filesWritten: options.filesWritten
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
391
356
|
* The project-level skills install (`npx skills add
|
|
392
357
|
* prisma/prisma#v<version>`) failed after a successful dependency
|
|
393
358
|
* install + emit. The project's scaffold remains on disk; the user
|
|
@@ -412,365 +377,416 @@ function errorInitSkillInstallFailed(options) {
|
|
|
412
377
|
});
|
|
413
378
|
}
|
|
414
379
|
/**
|
|
415
|
-
*
|
|
416
|
-
*
|
|
417
|
-
*
|
|
418
|
-
*
|
|
419
|
-
* - **Today**: `contract.json`, `contract.d.ts` are emitted on every
|
|
420
|
-
* `prisma-next contract emit`. Marking them `linguist-generated`
|
|
421
|
-
* keeps GitHub's diff stats honest and collapses the file in code
|
|
422
|
-
* review by default.
|
|
423
|
-
* - **Forward-looking**: `ops.json`, `migration.json` are not yet emitted
|
|
424
|
-
* by `init` flows but will be produced by adjacent commands (lower /
|
|
425
|
-
* migration tooling). Adding them now matches Decision 5
|
|
426
|
-
* (forward-looking subset) so the file does not need to be amended
|
|
427
|
-
* every time a new artifact lands.
|
|
380
|
+
* Connects (when configured) to the user's database and returns a
|
|
381
|
+
* structured outcome describing whether the server meets the declared
|
|
382
|
+
* minimum (FR8.1). Pure with respect to its inputs: no I/O happens
|
|
383
|
+
* unless `databaseUrl` is set.
|
|
428
384
|
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
* gets `db/contract.json linguist-generated` — not the workspace-glob
|
|
432
|
-
* form `<glob>/contract.json` (which would over-match any unrelated
|
|
433
|
-
* `contract.json` the user has elsewhere) and not the absolute
|
|
434
|
-
* `DEFAULT_CONTRACT_SOURCE_DIR/contract.json` (which would silently
|
|
435
|
-
* break for a non-default schema path).
|
|
385
|
+
* The outcome is shaped so that `--strict-probe` can branch on the
|
|
386
|
+
* `kind`/`meetsMinimum` pair without re-stringifying the message:
|
|
436
387
|
*
|
|
437
|
-
*
|
|
438
|
-
*
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
* `
|
|
443
|
-
|
|
444
|
-
const ARTIFACT_FILENAMES$1 = [
|
|
445
|
-
"contract.json",
|
|
446
|
-
"contract.d.ts",
|
|
447
|
-
"ops.json",
|
|
448
|
-
"migration.json"
|
|
449
|
-
];
|
|
450
|
-
const ATTRIBUTE = "linguist-generated";
|
|
451
|
-
/**
|
|
452
|
-
* Full `.gitattributes` lines for the migration contract snapshot store,
|
|
453
|
-
* already anchored to the migrations root — unlike `ARTIFACT_FILENAMES`,
|
|
454
|
-
* these are not combined with the schema-relative prefix.
|
|
455
|
-
*/
|
|
456
|
-
const STORE_GITATTRIBUTES_LINES = [`migrations/snapshots/**/contract.json ${ATTRIBUTE}`, `migrations/snapshots/**/contract.d.ts ${ATTRIBUTE}`];
|
|
457
|
-
/**
|
|
458
|
-
* Computes the `.gitattributes` lines this scaffold expects to own. Each
|
|
459
|
-
* line has the shape `<path> linguist-generated`. The `target` parameter
|
|
460
|
-
* is currently unused but accepted for symmetry with the other hygiene
|
|
461
|
-
* helpers and to leave room for target-specific entries (e.g. a future
|
|
462
|
-
* family-specific artifact) without a signature break.
|
|
388
|
+
* - `ok` — informational; `init` continues.
|
|
389
|
+
* - `below-minimum` — warning; `init` continues regardless of
|
|
390
|
+
* `--strict-probe` (the spec scopes strict-probe to "probe
|
|
391
|
+
* *failures*", and a successful probe that finds an old server is
|
|
392
|
+
* not a failure).
|
|
393
|
+
* - `no-database-url` / `connection-failed` / `driver-missing` —
|
|
394
|
+
* warning by default, fatal under `--strict-probe`.
|
|
463
395
|
*/
|
|
464
|
-
function
|
|
465
|
-
const
|
|
466
|
-
|
|
467
|
-
|
|
396
|
+
async function probeServerVersion(ctx, overrides = {}) {
|
|
397
|
+
const { databaseUrl, minVersion, target } = ctx;
|
|
398
|
+
if (databaseUrl === void 0 || databaseUrl.trim().length === 0) return {
|
|
399
|
+
kind: "no-database-url",
|
|
400
|
+
minVersion,
|
|
401
|
+
meetsMinimum: null,
|
|
402
|
+
message: "Skipped --probe-db: DATABASE_URL is not set in the current shell environment. (init does not read .env for the probe; export the variable or drop --probe-db.)"
|
|
403
|
+
};
|
|
404
|
+
let driverResult;
|
|
405
|
+
try {
|
|
406
|
+
if (target === "postgres") driverResult = overrides.probePostgres !== void 0 ? await overrides.probePostgres(databaseUrl) : await defaultProbePostgres(databaseUrl, ctx.baseDir, overrides);
|
|
407
|
+
else driverResult = overrides.probeMongo !== void 0 ? await overrides.probeMongo(databaseUrl) : await defaultProbeMongo(databaseUrl, ctx.baseDir, overrides);
|
|
408
|
+
} catch (err) {
|
|
409
|
+
if (err instanceof DriverMissingError) return {
|
|
410
|
+
kind: "driver-missing",
|
|
411
|
+
minVersion,
|
|
412
|
+
meetsMinimum: null,
|
|
413
|
+
cause: err.message,
|
|
414
|
+
message: `Skipped --probe-db: ${err.message}. (Run with install enabled, or install the driver yourself, then re-run \`prisma-next init --probe-db\`.)`
|
|
415
|
+
};
|
|
416
|
+
const cause = redactDatabaseUrlSecrets(causeMessage(err));
|
|
417
|
+
return {
|
|
418
|
+
kind: "connection-failed",
|
|
419
|
+
minVersion,
|
|
420
|
+
meetsMinimum: null,
|
|
421
|
+
cause,
|
|
422
|
+
message: `--probe-db could not connect: ${cause}.`
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
if (compareVersionPrefix(driverResult.serverVersion, minVersion) < 0) return {
|
|
426
|
+
kind: "below-minimum",
|
|
427
|
+
serverVersion: driverResult.serverVersion,
|
|
428
|
+
minVersion,
|
|
429
|
+
meetsMinimum: false,
|
|
430
|
+
message: `--probe-db: server reports version ${driverResult.serverVersion}, below the declared minimum (${minVersion}). Some queries may fail until the server is upgraded.`
|
|
431
|
+
};
|
|
432
|
+
return {
|
|
433
|
+
kind: "ok",
|
|
434
|
+
serverVersion: driverResult.serverVersion,
|
|
435
|
+
minVersion,
|
|
436
|
+
meetsMinimum: true,
|
|
437
|
+
message: `--probe-db: server reports version ${driverResult.serverVersion} (>= ${minVersion}).`
|
|
438
|
+
};
|
|
468
439
|
}
|
|
469
440
|
/**
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
441
|
+
* Compares two semver-prefix strings ("14", "14.2", "6.0", …) by
|
|
442
|
+
* numeric components left-to-right. Returns a negative number when `a`
|
|
443
|
+
* is older than `b`, zero when both versions agree on every numeric
|
|
444
|
+
* component (treating missing trailing components as `0`), and a
|
|
445
|
+
* positive number when `a` is newer.
|
|
473
446
|
*
|
|
474
|
-
*
|
|
475
|
-
* `
|
|
476
|
-
*
|
|
477
|
-
*
|
|
478
|
-
* appending the narrow one — because the narrow lines are what the
|
|
479
|
-
* acceptance criteria pin (FR3.4 AC).
|
|
447
|
+
* The loop runs over the **longer** of the two prefixes so that
|
|
448
|
+
* `'14'` compares less than `'14.1'` — without that, the shorter
|
|
449
|
+
* prefix would be silently accepted whenever the configured minimum
|
|
450
|
+
* has a non-zero minor or patch.
|
|
480
451
|
*
|
|
481
|
-
*
|
|
482
|
-
* every required entry).
|
|
452
|
+
* Exported for unit tests.
|
|
483
453
|
*/
|
|
484
|
-
function
|
|
485
|
-
|
|
486
|
-
const
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
454
|
+
function compareVersionPrefix(a, b) {
|
|
455
|
+
const aParts = parseNumericParts(a);
|
|
456
|
+
const bParts = parseNumericParts(b);
|
|
457
|
+
const len = Math.max(aParts.length, bParts.length);
|
|
458
|
+
for (let i = 0; i < len; i += 1) {
|
|
459
|
+
const aPart = aParts[i] ?? 0;
|
|
460
|
+
const bPart = bParts[i] ?? 0;
|
|
461
|
+
if (aPart !== bPart) return aPart - bPart;
|
|
462
|
+
}
|
|
463
|
+
return 0;
|
|
464
|
+
}
|
|
465
|
+
function parseNumericParts(version) {
|
|
466
|
+
const match = version.match(/^[^\d]*(\d+(?:\.\d+){0,3})/);
|
|
467
|
+
if (match === null) return [];
|
|
468
|
+
return (match[1] ?? "").split(".").map((part) => Number.parseInt(part, 10));
|
|
469
|
+
}
|
|
470
|
+
var DriverMissingError = class extends Error {};
|
|
471
|
+
function causeMessage(err) {
|
|
472
|
+
if (err instanceof Error) return err.message;
|
|
473
|
+
return String(err);
|
|
490
474
|
}
|
|
491
475
|
/**
|
|
492
|
-
*
|
|
493
|
-
*
|
|
476
|
+
* Strips `user:password@` userinfo from any URL-shaped substring before
|
|
477
|
+
* we surface the cause to the user. Mirrors `redactSecrets` in
|
|
478
|
+
* `init.ts` — the probe path has its own redactor because the inputs
|
|
479
|
+
* here include the raw connection string by construction (driver
|
|
480
|
+
* errors echo the URL back).
|
|
494
481
|
*
|
|
495
|
-
*
|
|
496
|
-
* because the scaffolded `tsconfig.json` writes there; `.env` last so
|
|
497
|
-
* the secret-bearing file is the one most-recently visible in any diff
|
|
498
|
-
* (a paranoid-correct ordering — humans skim from the top).
|
|
482
|
+
* Exported for unit tests.
|
|
499
483
|
*/
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
484
|
+
function redactDatabaseUrlSecrets(text) {
|
|
485
|
+
if (!text) return text;
|
|
486
|
+
return text.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^/@\s]+)@/g, "$1***@");
|
|
487
|
+
}
|
|
488
|
+
async function defaultProbePostgres(databaseUrl, baseDir, overrides) {
|
|
489
|
+
const client = new (requirePeer("pg", baseDir, overrides)).Client({ connectionString: databaseUrl });
|
|
490
|
+
await client.connect();
|
|
491
|
+
try {
|
|
492
|
+
const result = await client.query("SELECT version() as version");
|
|
493
|
+
return { serverVersion: parsePostgresVersion(String(result?.rows?.[0]?.version ?? "")) };
|
|
494
|
+
} finally {
|
|
495
|
+
await client.end().catch(() => void 0);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
505
498
|
/**
|
|
506
|
-
*
|
|
507
|
-
* content given the existing content (or `undefined` if the file does
|
|
508
|
-
* not yet exist). Adds only entries that are not already present and
|
|
509
|
-
* never duplicates a line. Existing comments and blank lines are
|
|
510
|
-
* preserved verbatim — `.gitignore` is parsed by `git` without a tree,
|
|
511
|
-
* so any line modification risks changing semantics.
|
|
499
|
+
* Extracts the numeric prefix from a Postgres `version()` row, e.g.
|
|
512
500
|
*
|
|
513
|
-
*
|
|
514
|
-
*
|
|
515
|
-
*
|
|
516
|
-
*
|
|
501
|
+
* `PostgreSQL 14.10 on x86_64-pc-linux-gnu, ...` → `"14.10"`
|
|
502
|
+
* `PostgreSQL 16beta1 on …` → `"16"` (we
|
|
503
|
+
* conservatively drop the suffix; minimum-version comparisons
|
|
504
|
+
* treat 16beta1 as 16, which is what every reasonable user
|
|
505
|
+
* expects).
|
|
517
506
|
*
|
|
518
|
-
*
|
|
519
|
-
* every required entry). The caller can use this to decide whether to
|
|
520
|
-
* include `.gitignore` in `filesWritten`.
|
|
507
|
+
* Exported for unit tests.
|
|
521
508
|
*/
|
|
522
|
-
function
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
509
|
+
function parsePostgresVersion(versionString) {
|
|
510
|
+
const match = versionString.match(/PostgreSQL\s+(\d+(?:\.\d+)?)/i);
|
|
511
|
+
if (match === null || match[1] === void 0) throw new CliStructuredError("CLI.INIT_PROBE_FAILED", `Could not parse PostgreSQL version from \`${versionString}\``);
|
|
512
|
+
return match[1];
|
|
513
|
+
}
|
|
514
|
+
async function defaultProbeMongo(databaseUrl, baseDir, overrides) {
|
|
515
|
+
const client = new (requirePeer("mongodb", baseDir, overrides)).MongoClient(databaseUrl);
|
|
516
|
+
await client.connect();
|
|
517
|
+
try {
|
|
518
|
+
const buildInfo = await client.db().admin().command({ buildInfo: 1 });
|
|
519
|
+
const versionString = String(buildInfo.version ?? "");
|
|
520
|
+
if (versionString.length === 0) throw new CliStructuredError("CLI.INIT_PROBE_FAILED", "buildInfo did not include a `version` field");
|
|
521
|
+
return { serverVersion: versionString };
|
|
522
|
+
} finally {
|
|
523
|
+
await client.close().catch(() => void 0);
|
|
524
|
+
}
|
|
528
525
|
}
|
|
529
|
-
const REQUIRED_SCRIPTS = [{
|
|
530
|
-
name: "contract:emit",
|
|
531
|
-
command: "prisma-next contract emit"
|
|
532
|
-
}];
|
|
533
526
|
/**
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
537
|
-
*
|
|
538
|
-
*
|
|
539
|
-
*
|
|
540
|
-
* -
|
|
541
|
-
*
|
|
542
|
-
* The user's override is sacred — `init` should never silently
|
|
543
|
-
* overwrite a custom build pipeline.
|
|
544
|
-
*
|
|
545
|
-
* Preserves the existing key order (so a user who has alphabetised
|
|
546
|
-
* their scripts does not see them reshuffled) and appends new entries
|
|
547
|
-
* at the end.
|
|
548
|
-
*
|
|
549
|
-
* The `package.json` is parsed and re-stringified through `JSON` —
|
|
550
|
-
* comments are not preserved (package.json does not support them per
|
|
551
|
-
* spec). Trailing newline matches the original input's trailing
|
|
552
|
-
* newline behaviour.
|
|
527
|
+
* Loads a peer driver (`pg` / `mongodb`) from the user's project
|
|
528
|
+
* `node_modules`. We deliberately resolve from `baseDir` rather than
|
|
529
|
+
* from the CLI bundle — the CLI does not depend on `pg` or `mongodb`
|
|
530
|
+
* directly, but the user's `init`-generated `package.json` does (via
|
|
531
|
+
* the target facade). Failure to resolve is folded into a typed
|
|
532
|
+
* `DriverMissingError` so `probeServerVersion` can map it to a
|
|
533
|
+
* `driver-missing` outcome rather than letting a `MODULE_NOT_FOUND`
|
|
534
|
+
* leak as a generic connection failure.
|
|
553
535
|
*/
|
|
554
|
-
function
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
const existingValue = scripts[name];
|
|
561
|
-
if (existingValue === void 0) {
|
|
562
|
-
scripts[name] = command;
|
|
563
|
-
mutated = true;
|
|
564
|
-
continue;
|
|
565
|
-
}
|
|
566
|
-
if (existingValue !== command) warnings.push(`package.json already has a "${name}" script with a different command — keeping yours.\n existing: ${existingValue}\n expected: ${command}\nIf you want the default, remove your "${name}" script and re-run \`init\`.`);
|
|
536
|
+
function requirePeer(moduleId, baseDir, overrides) {
|
|
537
|
+
try {
|
|
538
|
+
if (overrides.requireFromBaseDir !== void 0) return overrides.requireFromBaseDir(baseDir, moduleId);
|
|
539
|
+
return createRequire(join(baseDir, "package.json"))(moduleId);
|
|
540
|
+
} catch (err) {
|
|
541
|
+
throw new DriverMissingError(`\`${moduleId}\` is not installed in this project (resolved from ${baseDir}; cause: ${causeMessage(err)})`);
|
|
567
542
|
}
|
|
568
|
-
if (!mutated) return {
|
|
569
|
-
content: null,
|
|
570
|
-
warnings
|
|
571
|
-
};
|
|
572
|
-
parsed["scripts"] = scripts;
|
|
573
|
-
const trailingNewline = existing.endsWith("\n") ? "\n" : "";
|
|
574
|
-
return {
|
|
575
|
-
content: `${JSON.stringify(parsed, null, 2)}${trailingNewline}`,
|
|
576
|
-
warnings
|
|
577
|
-
};
|
|
578
543
|
}
|
|
579
544
|
/**
|
|
580
|
-
*
|
|
581
|
-
*
|
|
582
|
-
*
|
|
583
|
-
*
|
|
584
|
-
* Without this field Node either:
|
|
585
|
-
*
|
|
586
|
-
* - emits `MODULE_TYPELESS_PACKAGE_JSON` and reparses the file as ESM
|
|
587
|
-
* with a perf penalty (Node 22+ with `--experimental-strip-types`), or
|
|
588
|
-
* - hard-fails with `ERR_*` because the CJS loader cannot parse the
|
|
589
|
-
* import-attribute syntax (older Node, or any tool that doesn't
|
|
590
|
-
* reparse).
|
|
591
|
-
*
|
|
592
|
-
* Behaviour:
|
|
593
|
-
*
|
|
594
|
-
* - **Field missing** → set to `"module"`. New entry is inserted right
|
|
595
|
-
* after `"name"` (when present) so the diff lands in a conventional
|
|
596
|
-
* spot for human review; falls through to the natural append position
|
|
597
|
-
* otherwise.
|
|
598
|
-
* - **Field already `"module"`** → no-op (idempotent).
|
|
599
|
-
* - **Field set to anything else** (e.g. `"commonjs"`) → leave it alone
|
|
600
|
-
* and surface a structured warning. The user explicitly opted out of
|
|
601
|
-
* ESM and we don't silently overwrite that.
|
|
545
|
+
* Default base for the GitHub-URL form `<owner>/<repo>` consumed by
|
|
546
|
+
* upstream `skills add`. Each `SkillSource` joins this base with its
|
|
547
|
+
* own subpath (and optional `#ref` for version-pinned clusters).
|
|
602
548
|
*/
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
}
|
|
549
|
+
const DEFAULT_SKILL_BASE = "prisma/prisma";
|
|
550
|
+
const DEFAULT_SKILL_SOURCES = [
|
|
551
|
+
{
|
|
552
|
+
subpath: "skills",
|
|
553
|
+
skill: "prisma-8",
|
|
554
|
+
ref: "cli",
|
|
555
|
+
description: "usage skill (version-locked to installed Prisma Next)"
|
|
556
|
+
},
|
|
557
|
+
{
|
|
558
|
+
subpath: "skills",
|
|
559
|
+
skill: "prisma-next-upgrade",
|
|
560
|
+
ref: null,
|
|
561
|
+
description: "upgrade skill (always tracks `main`)"
|
|
562
|
+
},
|
|
563
|
+
{
|
|
564
|
+
subpath: "skills",
|
|
565
|
+
skill: "prisma-8-extension-upgrade",
|
|
566
|
+
ref: null,
|
|
567
|
+
description: "extension-author upgrade skill (always tracks `main`)"
|
|
623
568
|
}
|
|
624
|
-
|
|
625
|
-
const trailingNewline = existing.endsWith("\n") ? "\n" : "";
|
|
626
|
-
return {
|
|
627
|
-
content: `${JSON.stringify(next, null, 2)}${trailingNewline}`,
|
|
628
|
-
warning: null
|
|
629
|
-
};
|
|
630
|
-
}
|
|
631
|
-
const TARGET_ALIASES = /* @__PURE__ */ new Map([
|
|
632
|
-
["postgres", "postgres"],
|
|
633
|
-
["postgresql", "postgres"],
|
|
634
|
-
["mongo", "mongo"],
|
|
635
|
-
["mongodb", "mongo"]
|
|
636
|
-
]);
|
|
637
|
-
const AUTHORING_VALUES = /* @__PURE__ */ new Map([
|
|
638
|
-
["psl", "psl"],
|
|
639
|
-
["typescript", "typescript"],
|
|
640
|
-
["ts", "typescript"]
|
|
641
|
-
]);
|
|
569
|
+
];
|
|
642
570
|
/**
|
|
643
|
-
*
|
|
644
|
-
*
|
|
645
|
-
*
|
|
646
|
-
* (FR1.4). Throws `CliStructuredError` on any unrecoverable input issue.
|
|
571
|
+
* Test-only escape hatch for pinning the install base to a local
|
|
572
|
+
* checkout. Production runs leave this unset, so installs always use
|
|
573
|
+
* `DEFAULT_SKILL_BASE`.
|
|
647
574
|
*
|
|
648
|
-
*
|
|
649
|
-
*
|
|
650
|
-
*
|
|
651
|
-
*
|
|
575
|
+
* When set to an absolute filesystem path (typical for tests), the
|
|
576
|
+
* `#ref` fragment is dropped — local-path mode in upstream's CLI does
|
|
577
|
+
* not accept refs, and the local clone has whatever content the test
|
|
578
|
+
* checked into it anyway. When set to anything else (e.g. a fork name
|
|
579
|
+
* `myuser/prisma-next`), the ref policy is preserved.
|
|
652
580
|
*/
|
|
653
|
-
|
|
654
|
-
const
|
|
655
|
-
|
|
656
|
-
const autoAcceptPrompts = Boolean(flags.yes);
|
|
657
|
-
if (options.strictProbe && !options.probeDb) throw errorInitStrictProbeWithoutProbe();
|
|
658
|
-
const reinit = await resolveReinit({
|
|
659
|
-
baseDir,
|
|
660
|
-
force,
|
|
661
|
-
canPrompt,
|
|
662
|
-
autoAcceptPrompts
|
|
663
|
-
});
|
|
664
|
-
const target = resolveTarget(options.target);
|
|
665
|
-
const authoring = resolveAuthoring(options.authoring);
|
|
666
|
-
const missing = [];
|
|
667
|
-
if (target === void 0) missing.push("target");
|
|
668
|
-
if (authoring === void 0) missing.push("authoring");
|
|
669
|
-
if (!canPrompt && missing.length > 0) throw errorInitMissingFlags({
|
|
670
|
-
missing,
|
|
671
|
-
why: process.stdin.isTTY ? "Non-interactive mode is active (`--no-interactive` or stdout is piped)." : "stdin is not a TTY, so `init` cannot prompt interactively."
|
|
672
|
-
});
|
|
673
|
-
const finalTarget = target ?? await promptTarget();
|
|
674
|
-
const finalAuthoring = authoring ?? await promptAuthoring();
|
|
675
|
-
const finalSchemaPath = options.schemaPath !== void 0 ? validateSchemaPath(options.schemaPath, finalAuthoring) : canPrompt ? await promptSchemaPath(finalAuthoring) : defaultSchemaPath(finalAuthoring);
|
|
676
|
-
const writeEnv = await resolveWriteEnv({
|
|
677
|
-
flag: options.writeEnv,
|
|
678
|
-
canPrompt,
|
|
679
|
-
autoAcceptPrompts
|
|
680
|
-
});
|
|
681
|
-
const removePreviousFacade = await resolveRemovePreviousFacade({
|
|
682
|
-
baseDir,
|
|
683
|
-
target: finalTarget,
|
|
684
|
-
reinit,
|
|
685
|
-
force,
|
|
686
|
-
canPrompt,
|
|
687
|
-
autoAcceptPrompts
|
|
688
|
-
});
|
|
689
|
-
const installProjectSkill = options.skill !== false;
|
|
690
|
-
return {
|
|
691
|
-
target: finalTarget,
|
|
692
|
-
authoring: finalAuthoring,
|
|
693
|
-
schemaPath: finalSchemaPath,
|
|
694
|
-
install: options.install !== false,
|
|
695
|
-
writeEnv,
|
|
696
|
-
probeDb: Boolean(options.probeDb),
|
|
697
|
-
strictProbe: Boolean(options.strictProbe),
|
|
698
|
-
reinit,
|
|
699
|
-
removePreviousFacade,
|
|
700
|
-
installProjectSkill
|
|
701
|
-
};
|
|
581
|
+
function resolveAgentSkillBase(env) {
|
|
582
|
+
const override = env["PRISMA_NEXT_SKILLS_BASE"]?.trim();
|
|
583
|
+
return override && override.length > 0 ? override : DEFAULT_SKILL_BASE;
|
|
702
584
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
585
|
+
function isLocalPath(base) {
|
|
586
|
+
return base.startsWith("/") || /^[a-zA-Z]:[\\/]/.test(base);
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Agents passed to every project-level init install. Upstream `skills add`
|
|
590
|
+
* is the source of truth for per-agent install behaviour; the CLI lists
|
|
591
|
+
* every supported runtime on one invocation and delegates the rest.
|
|
592
|
+
*/
|
|
593
|
+
const DEFAULT_SKILL_AGENTS = [
|
|
594
|
+
"cursor",
|
|
595
|
+
"claude-code",
|
|
596
|
+
"codex",
|
|
597
|
+
"windsurf"
|
|
598
|
+
];
|
|
599
|
+
/**
|
|
600
|
+
* Build the `<base>/<subpath>[#ref]` URL the `skills` CLI will
|
|
601
|
+
* resolve. Exported for unit tests so the per-source format can be
|
|
602
|
+
* asserted without going through the full install loop.
|
|
603
|
+
*/
|
|
604
|
+
function formatSkillSourceUrl(source, env = process.env) {
|
|
605
|
+
const base = resolveAgentSkillBase(env);
|
|
606
|
+
const url = `${base}/${source.subpath}`;
|
|
607
|
+
if (source.ref === null) return url;
|
|
608
|
+
if (isLocalPath(base)) return url;
|
|
609
|
+
if (source.ref === "cli") return `${url}#v${version}`;
|
|
610
|
+
return url;
|
|
713
611
|
}
|
|
714
612
|
/**
|
|
715
|
-
*
|
|
716
|
-
*
|
|
717
|
-
*
|
|
613
|
+
* The skill-install command for one source, formatted for the
|
|
614
|
+
* project's detected package manager. `npx`/`pnpm dlx`/`bunx` are
|
|
615
|
+
* interchangeable to the user; we pick the variant that matches the
|
|
616
|
+
* rest of the install step so a single project consistently uses one
|
|
617
|
+
* runner.
|
|
718
618
|
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
721
|
-
*
|
|
722
|
-
* the removal proceeds without further prompting. Interactive runs see
|
|
723
|
-
* a `clack.confirm` with `initialValue: true` — the destructive default
|
|
724
|
-
* is correct because keeping both facades produces a project that
|
|
725
|
-
* imports from one but pays for both in the lockfile, which is a
|
|
726
|
-
* silent foot-gun the user almost never wants.
|
|
619
|
+
* `--agent` takes space-separated slugs on one flag; the explicit
|
|
620
|
+
* `--skill <name>` and `-y` skip the multi-select prompts a
|
|
621
|
+
* non-interactive scaffold step cannot show.
|
|
727
622
|
*
|
|
728
|
-
*
|
|
729
|
-
*
|
|
730
|
-
* `package.json` resolve to `null` here — `runInit`'s precondition
|
|
731
|
-
* gate surfaces a structured 5010 error for the same file shortly
|
|
732
|
-
* after, so we avoid double-reporting and keep this helper side-effect
|
|
733
|
-
* free under hostile inputs.
|
|
623
|
+
* Exported for unit tests so the per-PM dispatch can be asserted
|
|
624
|
+
* without a live subprocess.
|
|
734
625
|
*/
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
if (deps === null || typeof deps !== "object" || Array.isArray(deps)) return null;
|
|
749
|
-
const otherFacade = otherFacadeNames.find((name) => Object.hasOwn(deps, name));
|
|
750
|
-
if (otherFacade === void 0) return null;
|
|
751
|
-
if (opts.force || opts.canPrompt && opts.autoAcceptPrompts) return otherFacade;
|
|
752
|
-
if (!opts.canPrompt) return otherFacade;
|
|
753
|
-
const result = await clack.confirm({
|
|
754
|
-
message: `Switching from ${targetLabel(otherTarget)} to ${targetLabel(opts.target)} — remove ${otherFacade} from package.json dependencies?`,
|
|
755
|
-
initialValue: true,
|
|
756
|
-
output: process.stderr
|
|
757
|
-
});
|
|
758
|
-
if (clack.isCancel(result)) throw errorInitUserAborted();
|
|
759
|
-
return result === true ? otherFacade : null;
|
|
760
|
-
}
|
|
761
|
-
async function resolveReinit(opts) {
|
|
762
|
-
if (!existsSync(join(opts.baseDir, "prisma-next.config.ts"))) return false;
|
|
763
|
-
if (opts.force) return true;
|
|
764
|
-
if (!opts.canPrompt) throw errorInitReinitNeedsForce();
|
|
765
|
-
if (opts.autoAcceptPrompts) return true;
|
|
766
|
-
const result = await clack.confirm({
|
|
767
|
-
message: "This project is already initialized. Re-initialize? This will overwrite all generated files.",
|
|
768
|
-
initialValue: false,
|
|
769
|
-
output: process.stderr
|
|
770
|
-
});
|
|
771
|
-
if (clack.isCancel(result) || result !== true) throw errorInitUserAborted();
|
|
772
|
-
return true;
|
|
626
|
+
function formatSkillInstallCommand(args) {
|
|
627
|
+
const agents = args.agents ?? DEFAULT_SKILL_AGENTS;
|
|
628
|
+
const cliArgs = [
|
|
629
|
+
"skills@latest",
|
|
630
|
+
"add",
|
|
631
|
+
formatSkillSourceUrl(args.source, args.env),
|
|
632
|
+
"--agent",
|
|
633
|
+
...agents,
|
|
634
|
+
"--skill",
|
|
635
|
+
args.source.skill,
|
|
636
|
+
"-y"
|
|
637
|
+
];
|
|
638
|
+
return formatPackageManagerCommand(args.pm, cliArgs);
|
|
773
639
|
}
|
|
640
|
+
/**
|
|
641
|
+
* Ordered skill-install commands for one init run. This is both what the
|
|
642
|
+
* commander shell runs and what either shell tells the user to run when the
|
|
643
|
+
* install is skipped or fails — the commands need no scaffold and no `init`.
|
|
644
|
+
*/
|
|
645
|
+
function resolveProjectSkillInstallCommands(pm, env) {
|
|
646
|
+
return DEFAULT_SKILL_SOURCES.map((source) => formatSkillInstallCommand({
|
|
647
|
+
pm,
|
|
648
|
+
source,
|
|
649
|
+
...ifDefined("env", env)
|
|
650
|
+
}));
|
|
651
|
+
}
|
|
652
|
+
function formatPackageManagerCommand(pm, args) {
|
|
653
|
+
switch (pm) {
|
|
654
|
+
case "pnpm": return `pnpm dlx ${args.join(" ")}`;
|
|
655
|
+
case "yarn": return `yarn dlx ${args.join(" ")}`;
|
|
656
|
+
case "bun": return `bunx ${args.join(" ")}`;
|
|
657
|
+
case "deno": return `deno run -A npm:${args.join(" ")}`;
|
|
658
|
+
case "npm": return `npx ${args.join(" ")}`;
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Skill directories that predate the consolidated `prisma-8` skill:
|
|
663
|
+
* the per-workflow usage cluster (including the renamed
|
|
664
|
+
* `prisma-8-migration-review` spelling it briefly shipped under), the
|
|
665
|
+
* pre-rename spellings of the consolidated skill and the
|
|
666
|
+
* extension-author upgrade skill, and any hand-rolled `prisma-next`
|
|
667
|
+
* stub. Projects initialised before the consolidation carry these as
|
|
668
|
+
* sibling directories in each agent's install root; left in place
|
|
669
|
+
* they compete with the current skills for activation, so init
|
|
670
|
+
* removes them on every run.
|
|
671
|
+
*/
|
|
672
|
+
const RETIRED_SKILL_NAMES = [
|
|
673
|
+
"prisma-next",
|
|
674
|
+
"prisma-next-quickstart",
|
|
675
|
+
"prisma-next-contract",
|
|
676
|
+
"prisma-next-migrations",
|
|
677
|
+
"prisma-next-migration-review",
|
|
678
|
+
"prisma-8-migration-review",
|
|
679
|
+
"prisma-next-queries",
|
|
680
|
+
"prisma-next-runtime",
|
|
681
|
+
"prisma-next-build",
|
|
682
|
+
"prisma-next-supabase",
|
|
683
|
+
"prisma-next-debug",
|
|
684
|
+
"prisma-next-feedback",
|
|
685
|
+
"prisma-next-extension-upgrade"
|
|
686
|
+
];
|
|
687
|
+
/**
|
|
688
|
+
* Project-level install roots the upstream `skills` CLI uses for the
|
|
689
|
+
* agents in `DEFAULT_SKILL_AGENTS`: cursor and codex install into
|
|
690
|
+
* `.agents/skills`, claude-code into `.claude/skills`, windsurf into
|
|
691
|
+
* `.windsurf/skills`.
|
|
692
|
+
*/
|
|
693
|
+
const AGENT_SKILL_ROOTS = [
|
|
694
|
+
".agents/skills",
|
|
695
|
+
".claude/skills",
|
|
696
|
+
".windsurf/skills"
|
|
697
|
+
];
|
|
698
|
+
/**
|
|
699
|
+
* Every directory a retired per-workflow skill may occupy in a
|
|
700
|
+
* consumer project. Init deletes each (recursively) before running the
|
|
701
|
+
* skill install.
|
|
702
|
+
*/
|
|
703
|
+
function legacySkillDirs() {
|
|
704
|
+
return AGENT_SKILL_ROOTS.flatMap((root) => RETIRED_SKILL_NAMES.map((name) => `${root}/${name}`));
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* The minimum supported server version for each target. The
|
|
708
|
+
* authoritative source of truth is each target package's
|
|
709
|
+
* `package.json#prismaNext.minServerVersion` field — this module
|
|
710
|
+
* mirrors those values and a workspace-level test asserts the two
|
|
711
|
+
* never drift (`templates/tsconfig-env.test.ts`).
|
|
712
|
+
*
|
|
713
|
+
* Bumping a value here in isolation is **not** safe: edit the
|
|
714
|
+
* corresponding target package's `package.json` first, then mirror
|
|
715
|
+
* here. The scaffold's `.env.example` and the "Requirements" section
|
|
716
|
+
* of `prisma-next.md` both read from this constant, so a stale value
|
|
717
|
+
* lies to every freshly initialised user.
|
|
718
|
+
*/
|
|
719
|
+
const MIN_SERVER_VERSION = {
|
|
720
|
+
postgres: "17",
|
|
721
|
+
mongo: "8.0"
|
|
722
|
+
};
|
|
723
|
+
const TARGET_LABEL = {
|
|
724
|
+
postgres: "PostgreSQL",
|
|
725
|
+
mongo: "MongoDB"
|
|
726
|
+
};
|
|
727
|
+
/**
|
|
728
|
+
* Renders the placeholder body shared by `.env` and `.env.example`:
|
|
729
|
+
* the target-specific connection-string requirement comments and the
|
|
730
|
+
* commented-shape `DATABASE_URL` line. The output is identical for both
|
|
731
|
+
* authoring styles — the env file is orthogonal to PSL vs TS schema
|
|
732
|
+
* authoring.
|
|
733
|
+
*/
|
|
734
|
+
function envPlaceholderBody(target) {
|
|
735
|
+
const label = TARGET_LABEL[target];
|
|
736
|
+
const minVersion = MIN_SERVER_VERSION[target];
|
|
737
|
+
const lines = [];
|
|
738
|
+
lines.push(`# Connection string for ${label}.`);
|
|
739
|
+
lines.push(`# Requires ${label} >= ${minVersion}.`);
|
|
740
|
+
lines.push("");
|
|
741
|
+
if (target === "postgres") lines.push("DATABASE_URL=\"postgresql://user:password@localhost:5432/mydb\"");
|
|
742
|
+
else {
|
|
743
|
+
lines.push("# Standalone local mongod / `docker run mongo:8` — no replica set required for first-run queries.");
|
|
744
|
+
lines.push("# Transactions and change streams need a replica set; add ?replicaSet=... only after initiating one.");
|
|
745
|
+
lines.push("");
|
|
746
|
+
lines.push("DATABASE_URL=\"mongodb://user:password@localhost:27017/mydb\"");
|
|
747
|
+
}
|
|
748
|
+
lines.push("");
|
|
749
|
+
return lines.join("\n");
|
|
750
|
+
}
|
|
751
|
+
/**
|
|
752
|
+
* Renders the `.env.example` content for a given target:
|
|
753
|
+
*
|
|
754
|
+
* - Carries a "Copy this file to `.env`…" intro that only makes sense
|
|
755
|
+
* for the example file (the real `.env` is the destination of that
|
|
756
|
+
* copy and so does not get the same intro).
|
|
757
|
+
* - Documents the `DATABASE_URL` placeholder in the target's native URL
|
|
758
|
+
* shape (Postgres: standard `postgresql://`, Mongo: `mongodb://` plus
|
|
759
|
+
* a `mydb` database segment so the lazy facade has a `dbName`).
|
|
760
|
+
* - Carries a `# Requires <db> >= <version>` comment so a fresh user
|
|
761
|
+
* knows the minimum supported server before they first try to connect.
|
|
762
|
+
*/
|
|
763
|
+
function envExampleContent(target) {
|
|
764
|
+
const lines = [];
|
|
765
|
+
lines.push("# Copy this file to `.env` and replace the placeholder with your real connection string.");
|
|
766
|
+
lines.push(envPlaceholderBody(target));
|
|
767
|
+
return lines.join("\n");
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Renders the initial `.env` content for `--write-env` / interactive
|
|
771
|
+
* opt-in. Same placeholder body as `.env.example`, **without** the
|
|
772
|
+
* example file's "Copy this file to `.env`…" intro: the real `.env` is
|
|
773
|
+
* the destination of that copy, so the line would lie. Writing this
|
|
774
|
+
* file is gitignored (`.env` lands in `.gitignore` during init).
|
|
775
|
+
*/
|
|
776
|
+
function envFileContent(target) {
|
|
777
|
+
return envPlaceholderBody(target);
|
|
778
|
+
}
|
|
779
|
+
const TARGET_ALIASES = /* @__PURE__ */ new Map([
|
|
780
|
+
["postgres", "postgres"],
|
|
781
|
+
["postgresql", "postgres"],
|
|
782
|
+
["mongo", "mongo"],
|
|
783
|
+
["mongodb", "mongo"]
|
|
784
|
+
]);
|
|
785
|
+
const AUTHORING_VALUES = /* @__PURE__ */ new Map([
|
|
786
|
+
["psl", "psl"],
|
|
787
|
+
["typescript", "typescript"],
|
|
788
|
+
["ts", "typescript"]
|
|
789
|
+
]);
|
|
774
790
|
function resolveTarget(value) {
|
|
775
791
|
if (value === void 0) return void 0;
|
|
776
792
|
const mapped = TARGET_ALIASES.get(value.toLowerCase());
|
|
@@ -820,587 +836,363 @@ function validateSchemaPath(value, authoring) {
|
|
|
820
836
|
});
|
|
821
837
|
return normalize(trimmed);
|
|
822
838
|
}
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}],
|
|
833
|
-
output: process.stderr
|
|
834
|
-
});
|
|
835
|
-
if (clack.isCancel(result)) throw errorInitUserAborted();
|
|
836
|
-
return result;
|
|
837
|
-
}
|
|
838
|
-
async function promptAuthoring() {
|
|
839
|
-
const result = await clack.select({
|
|
840
|
-
message: "How do you want to write your schema?",
|
|
841
|
-
options: [{
|
|
842
|
-
value: "psl",
|
|
843
|
-
label: "Prisma Schema Language (.prisma)"
|
|
844
|
-
}, {
|
|
845
|
-
value: "typescript",
|
|
846
|
-
label: "TypeScript (.ts)"
|
|
847
|
-
}],
|
|
848
|
-
output: process.stderr
|
|
849
|
-
});
|
|
850
|
-
if (clack.isCancel(result)) throw errorInitUserAborted();
|
|
851
|
-
return result;
|
|
852
|
-
}
|
|
853
|
-
async function promptSchemaPath(authoring) {
|
|
854
|
-
const expectedExt = authoring === "typescript" ? ".ts" : ".prisma";
|
|
855
|
-
const result = await clack.text({
|
|
856
|
-
message: "Where should the schema file go?",
|
|
857
|
-
initialValue: defaultSchemaPath(authoring),
|
|
858
|
-
validate(value = "") {
|
|
859
|
-
const trimmed = value.trim();
|
|
860
|
-
if (trimmed.length === 0) return "Path cannot be empty";
|
|
861
|
-
if (trimmed.endsWith("/") || trimmed.endsWith("\\")) return "Path must be a file, not a directory";
|
|
862
|
-
const ext = extname(trimmed).toLowerCase();
|
|
863
|
-
if (ext === "") return "Path must include a file extension (e.g. .prisma or .ts)";
|
|
864
|
-
if (ext !== expectedExt) return `Schema path must end in ${expectedExt} for --authoring ${authoring} (got ${ext}).`;
|
|
865
|
-
},
|
|
866
|
-
output: process.stderr
|
|
867
|
-
});
|
|
868
|
-
if (clack.isCancel(result)) throw errorInitUserAborted();
|
|
869
|
-
return validateSchemaPath(result, authoring);
|
|
870
|
-
}
|
|
871
|
-
/**
|
|
872
|
-
* Connects (when configured) to the user's database and returns a
|
|
873
|
-
* structured outcome describing whether the server meets the declared
|
|
874
|
-
* minimum (FR8.1). Pure with respect to its inputs: no I/O happens
|
|
875
|
-
* unless `databaseUrl` is set.
|
|
876
|
-
*
|
|
877
|
-
* The outcome is shaped so that `--strict-probe` can branch on the
|
|
878
|
-
* `kind`/`meetsMinimum` pair without re-stringifying the message:
|
|
879
|
-
*
|
|
880
|
-
* - `ok` — informational; `init` continues.
|
|
881
|
-
* - `below-minimum` — warning; `init` continues regardless of
|
|
882
|
-
* `--strict-probe` (the spec scopes strict-probe to "probe
|
|
883
|
-
* *failures*", and a successful probe that finds an old server is
|
|
884
|
-
* not a failure).
|
|
885
|
-
* - `no-database-url` / `connection-failed` / `driver-missing` —
|
|
886
|
-
* warning by default, fatal under `--strict-probe`.
|
|
887
|
-
*/
|
|
888
|
-
async function probeServerVersion(ctx, overrides = {}) {
|
|
889
|
-
const { databaseUrl, minVersion, target } = ctx;
|
|
890
|
-
if (databaseUrl === void 0 || databaseUrl.trim().length === 0) return {
|
|
891
|
-
kind: "no-database-url",
|
|
892
|
-
minVersion,
|
|
893
|
-
meetsMinimum: null,
|
|
894
|
-
message: "Skipped --probe-db: DATABASE_URL is not set in the current shell environment. (init does not read .env for the probe; export the variable or drop --probe-db.)"
|
|
895
|
-
};
|
|
896
|
-
let driverResult;
|
|
897
|
-
try {
|
|
898
|
-
if (target === "postgres") driverResult = overrides.probePostgres !== void 0 ? await overrides.probePostgres(databaseUrl) : await defaultProbePostgres(databaseUrl, ctx.baseDir, overrides);
|
|
899
|
-
else driverResult = overrides.probeMongo !== void 0 ? await overrides.probeMongo(databaseUrl) : await defaultProbeMongo(databaseUrl, ctx.baseDir, overrides);
|
|
900
|
-
} catch (err) {
|
|
901
|
-
if (err instanceof DriverMissingError) return {
|
|
902
|
-
kind: "driver-missing",
|
|
903
|
-
minVersion,
|
|
904
|
-
meetsMinimum: null,
|
|
905
|
-
cause: err.message,
|
|
906
|
-
message: `Skipped --probe-db: ${err.message}. (Run with install enabled, or install the driver yourself, then re-run \`prisma-next init --probe-db\`.)`
|
|
907
|
-
};
|
|
908
|
-
const cause = redactDatabaseUrlSecrets(causeMessage$1(err));
|
|
909
|
-
return {
|
|
910
|
-
kind: "connection-failed",
|
|
911
|
-
minVersion,
|
|
912
|
-
meetsMinimum: null,
|
|
913
|
-
cause,
|
|
914
|
-
message: `--probe-db could not connect: ${cause}.`
|
|
915
|
-
};
|
|
916
|
-
}
|
|
917
|
-
if (compareVersionPrefix(driverResult.serverVersion, minVersion) < 0) return {
|
|
918
|
-
kind: "below-minimum",
|
|
919
|
-
serverVersion: driverResult.serverVersion,
|
|
920
|
-
minVersion,
|
|
921
|
-
meetsMinimum: false,
|
|
922
|
-
message: `--probe-db: server reports version ${driverResult.serverVersion}, below the declared minimum (${minVersion}). Some queries may fail until the server is upgraded.`
|
|
923
|
-
};
|
|
924
|
-
return {
|
|
925
|
-
kind: "ok",
|
|
926
|
-
serverVersion: driverResult.serverVersion,
|
|
927
|
-
minVersion,
|
|
928
|
-
meetsMinimum: true,
|
|
929
|
-
message: `--probe-db: server reports version ${driverResult.serverVersion} (>= ${minVersion}).`
|
|
930
|
-
};
|
|
839
|
+
const KNOWN = /* @__PURE__ */ new Set([
|
|
840
|
+
"pnpm",
|
|
841
|
+
"npm",
|
|
842
|
+
"yarn",
|
|
843
|
+
"bun",
|
|
844
|
+
"deno"
|
|
845
|
+
]);
|
|
846
|
+
function isPackageManager(name) {
|
|
847
|
+
return KNOWN.has(name);
|
|
931
848
|
}
|
|
932
849
|
/**
|
|
933
|
-
*
|
|
934
|
-
*
|
|
935
|
-
* is older than `b`, zero when both versions agree on every numeric
|
|
936
|
-
* component (treating missing trailing components as `0`), and a
|
|
937
|
-
* positive number when `a` is newer.
|
|
850
|
+
* Resolves the package manager `init` should drive for `add` / `install`
|
|
851
|
+
* commands. Tries, in order:
|
|
938
852
|
*
|
|
939
|
-
*
|
|
940
|
-
*
|
|
941
|
-
*
|
|
942
|
-
*
|
|
853
|
+
* 1. **`detect()`** — walks up from `cwd` looking for a lockfile, the
|
|
854
|
+
* `packageManager` field, the `devEngines.packageManager` field, or
|
|
855
|
+
* install metadata. This is the right answer whenever the user is
|
|
856
|
+
* anywhere inside an existing project, including a deep workspace
|
|
857
|
+
* subdirectory.
|
|
943
858
|
*
|
|
944
|
-
*
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
const aPart = aParts[i] ?? 0;
|
|
952
|
-
const bPart = bParts[i] ?? 0;
|
|
953
|
-
if (aPart !== bPart) return aPart - bPart;
|
|
954
|
-
}
|
|
955
|
-
return 0;
|
|
956
|
-
}
|
|
957
|
-
function parseNumericParts(version) {
|
|
958
|
-
const match = version.match(/^[^\d]*(\d+(?:\.\d+){0,3})/);
|
|
959
|
-
if (match === null) return [];
|
|
960
|
-
return (match[1] ?? "").split(".").map((part) => Number.parseInt(part, 10));
|
|
961
|
-
}
|
|
962
|
-
var DriverMissingError = class extends Error {};
|
|
963
|
-
function causeMessage$1(err) {
|
|
964
|
-
if (err instanceof Error) return err.message;
|
|
965
|
-
return String(err);
|
|
966
|
-
}
|
|
967
|
-
/**
|
|
968
|
-
* Strips `user:password@` userinfo from any URL-shaped substring before
|
|
969
|
-
* we surface the cause to the user. Mirrors `redactSecrets` in
|
|
970
|
-
* `init.ts` — the probe path has its own redactor because the inputs
|
|
971
|
-
* here include the raw connection string by construction (driver
|
|
972
|
-
* errors echo the URL back).
|
|
859
|
+
* 2. **`getUserAgent()`** — parses `npm_config_user_agent`, the env var
|
|
860
|
+
* every PM sets when it spawns a script. This catches the
|
|
861
|
+
* bare-directory case where there's no project to walk up to but the
|
|
862
|
+
* user invoked us via `pnpm dlx prisma-next init` / `bunx
|
|
863
|
+
* prisma-next init` / `yarn dlx …`. Same signal used by every
|
|
864
|
+
* `create-*` tool in the ecosystem (`create-vite`, `create-next-app`,
|
|
865
|
+
* `create-astro`, `@antfu/ni`, …).
|
|
973
866
|
*
|
|
974
|
-
*
|
|
867
|
+
* 3. **`npm`** — final fallback. Always present alongside Node.
|
|
975
868
|
*/
|
|
976
|
-
function
|
|
977
|
-
|
|
978
|
-
|
|
869
|
+
async function detectPackageManager(cwd) {
|
|
870
|
+
const detected = await detect({ cwd });
|
|
871
|
+
if (detected && isPackageManager(detected.name)) return detected.name;
|
|
872
|
+
const userAgent = getUserAgent();
|
|
873
|
+
if (userAgent !== null && isPackageManager(userAgent)) return userAgent;
|
|
874
|
+
return "npm";
|
|
979
875
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
await client.connect();
|
|
983
|
-
try {
|
|
984
|
-
const result = await client.query("SELECT version() as version");
|
|
985
|
-
return { serverVersion: parsePostgresVersion(String(result?.rows?.[0]?.version ?? "")) };
|
|
986
|
-
} finally {
|
|
987
|
-
await client.end().catch(() => void 0);
|
|
988
|
-
}
|
|
876
|
+
function hasProjectManifest(cwd) {
|
|
877
|
+
return existsSync(join(cwd, "package.json")) || existsSync(join(cwd, "deno.json")) || existsSync(join(cwd, "deno.jsonc"));
|
|
989
878
|
}
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
* `PostgreSQL 16beta1 on …` → `"16"` (we
|
|
995
|
-
* conservatively drop the suffix; minimum-version comparisons
|
|
996
|
-
* treat 16beta1 as 16, which is what every reasonable user
|
|
997
|
-
* expects).
|
|
998
|
-
*
|
|
999
|
-
* Exported for unit tests.
|
|
1000
|
-
*/
|
|
1001
|
-
function parsePostgresVersion(versionString) {
|
|
1002
|
-
const match = versionString.match(/PostgreSQL\s+(\d+(?:\.\d+)?)/i);
|
|
1003
|
-
if (match === null || match[1] === void 0) throw new CliStructuredError("CLI.INIT_PROBE_FAILED", `Could not parse PostgreSQL version from \`${versionString}\``);
|
|
1004
|
-
return match[1];
|
|
879
|
+
function formatRunCommand(pm, bin, args) {
|
|
880
|
+
if (pm === "npm") return `npx ${bin} ${args}`;
|
|
881
|
+
if (pm === "deno") return `deno run npm:${bin} ${args}`;
|
|
882
|
+
return `${pm} ${bin} ${args}`;
|
|
1005
883
|
}
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
return { serverVersion: versionString };
|
|
1014
|
-
} finally {
|
|
1015
|
-
await client.close().catch(() => void 0);
|
|
884
|
+
function formatRunScriptCommand(pm, scriptName) {
|
|
885
|
+
switch (pm) {
|
|
886
|
+
case "deno": return `deno task ${scriptName}`;
|
|
887
|
+
case "bun": return `bun run ${scriptName}`;
|
|
888
|
+
case "pnpm": return `pnpm run ${scriptName}`;
|
|
889
|
+
case "yarn": return `yarn run ${scriptName}`;
|
|
890
|
+
default: return `npm run ${scriptName}`;
|
|
1016
891
|
}
|
|
1017
892
|
}
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
* from the CLI bundle — the CLI does not depend on `pg` or `mongodb`
|
|
1022
|
-
* directly, but the user's `init`-generated `package.json` does (via
|
|
1023
|
-
* the target facade). Failure to resolve is folded into a typed
|
|
1024
|
-
* `DriverMissingError` so `probeServerVersion` can map it to a
|
|
1025
|
-
* `driver-missing` outcome rather than letting a `MODULE_NOT_FOUND`
|
|
1026
|
-
* leak as a generic connection failure.
|
|
1027
|
-
*/
|
|
1028
|
-
function requirePeer(moduleId, baseDir, overrides) {
|
|
1029
|
-
try {
|
|
1030
|
-
if (overrides.requireFromBaseDir !== void 0) return overrides.requireFromBaseDir(baseDir, moduleId);
|
|
1031
|
-
return createRequire(join(baseDir, "package.json"))(moduleId);
|
|
1032
|
-
} catch (err) {
|
|
1033
|
-
throw new DriverMissingError(`\`${moduleId}\` is not installed in this project (resolved from ${baseDir}; cause: ${causeMessage$1(err)})`);
|
|
1034
|
-
}
|
|
893
|
+
function formatAddArgs(pm, packages) {
|
|
894
|
+
if (pm === "deno") return ["add", ...packages.map((p) => `npm:${p}`)];
|
|
895
|
+
return ["add", ...packages];
|
|
1035
896
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
* add it here **and** to `ARTIFACT_FILENAMES` in `hygiene-gitattributes.ts`
|
|
1048
|
-
* — the two stay in lockstep so the file `init` advertises as
|
|
1049
|
-
* `linguist-generated` is exactly the file `init` is willing to delete on
|
|
1050
|
-
* re-init.
|
|
1051
|
-
*/
|
|
1052
|
-
const ARTIFACT_FILENAMES = [
|
|
1053
|
-
"contract.json",
|
|
1054
|
-
"contract.d.ts",
|
|
1055
|
-
"ops.json",
|
|
1056
|
-
"migration.json"
|
|
1057
|
-
];
|
|
1058
|
-
/**
|
|
1059
|
-
* Returns the schema-relative paths of stale contract artifacts the
|
|
1060
|
-
* previous `init` run (or a `contract emit`) left behind in `schemaDir`.
|
|
1061
|
-
* Paths are returned relative to `baseDir` so the caller can plumb them
|
|
1062
|
-
* into `filesWritten`-style logging without re-deriving the path.
|
|
1063
|
-
*
|
|
1064
|
-
* Pure function: no filesystem mutation. Used by `runInit`'s precondition
|
|
1065
|
-
* phase (FR6.2 / NFR3 atomicity) so a downstream parse failure leaves
|
|
1066
|
-
* the artifacts on disk and the project byte-identical to its pre-init
|
|
1067
|
-
* state.
|
|
1068
|
-
*/
|
|
1069
|
-
function findStaleArtifacts(baseDir, schemaDir) {
|
|
1070
|
-
const result = [];
|
|
1071
|
-
for (const filename of ARTIFACT_FILENAMES) {
|
|
1072
|
-
const rel = join(schemaDir, filename);
|
|
1073
|
-
if (existsSync(join(baseDir, rel))) result.push(rel);
|
|
1074
|
-
}
|
|
1075
|
-
return result;
|
|
897
|
+
function formatAddDevArgs(pm, packages) {
|
|
898
|
+
if (pm === "deno") return [
|
|
899
|
+
"add",
|
|
900
|
+
"--dev",
|
|
901
|
+
...packages.map((p) => `npm:${p}`)
|
|
902
|
+
];
|
|
903
|
+
return [
|
|
904
|
+
"add",
|
|
905
|
+
"-D",
|
|
906
|
+
...packages
|
|
907
|
+
];
|
|
1076
908
|
}
|
|
1077
909
|
/**
|
|
1078
|
-
*
|
|
1079
|
-
*
|
|
1080
|
-
*
|
|
1081
|
-
*
|
|
1082
|
-
* Used by `runInit` for the FR9.2 target-switch path: when the user
|
|
1083
|
-
* re-inits a project from `--target postgres` to `--target mongodb` (or
|
|
1084
|
-
* vice versa), the previous facade is removed from `dependencies` so the
|
|
1085
|
-
* resulting project depends only on the chosen target's facade.
|
|
1086
|
-
*
|
|
1087
|
-
* Devs/peers/optional dep groups are intentionally *not* touched — the
|
|
1088
|
-
* facades are only ever in `dependencies` (FR4 / FR7), and broadening
|
|
1089
|
-
* the search would risk clobbering an unrelated dep with the same name
|
|
1090
|
-
* in `peerDependencies`.
|
|
910
|
+
* The `.gitattributes` entries written for a freshly initialised project
|
|
911
|
+
* (FR3.4). Mirrors the relevant subset of the repo-root
|
|
912
|
+
* [`.gitattributes`](../../../../../../../../.gitattributes):
|
|
1091
913
|
*
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
const next = { ...deps };
|
|
1102
|
-
delete next[depName];
|
|
1103
|
-
parsed["dependencies"] = next;
|
|
1104
|
-
const trailingNewline = existing.endsWith("\n") ? "\n" : "";
|
|
1105
|
-
return `${JSON.stringify(parsed, null, 2)}${trailingNewline}`;
|
|
1106
|
-
}
|
|
1107
|
-
const exec = promisify(execFile);
|
|
1108
|
-
/**
|
|
1109
|
-
* Default base for the GitHub-URL form `<owner>/<repo>` consumed by
|
|
1110
|
-
* upstream `skills add`. Each `SkillSource` joins this base with its
|
|
1111
|
-
* own subpath (and optional `#ref` for version-pinned clusters).
|
|
1112
|
-
*/
|
|
1113
|
-
const DEFAULT_SKILL_BASE = "prisma/prisma";
|
|
1114
|
-
const DEFAULT_SKILL_SOURCES = [
|
|
1115
|
-
{
|
|
1116
|
-
subpath: "skills",
|
|
1117
|
-
skill: "prisma-8",
|
|
1118
|
-
ref: "cli",
|
|
1119
|
-
description: "usage skill (version-locked to installed Prisma Next)"
|
|
1120
|
-
},
|
|
1121
|
-
{
|
|
1122
|
-
subpath: "skills",
|
|
1123
|
-
skill: "prisma-next-upgrade",
|
|
1124
|
-
ref: null,
|
|
1125
|
-
description: "upgrade skill (always tracks `main`)"
|
|
1126
|
-
},
|
|
1127
|
-
{
|
|
1128
|
-
subpath: "skills",
|
|
1129
|
-
skill: "prisma-8-extension-upgrade",
|
|
1130
|
-
ref: null,
|
|
1131
|
-
description: "extension-author upgrade skill (always tracks `main`)"
|
|
1132
|
-
}
|
|
1133
|
-
];
|
|
1134
|
-
/**
|
|
1135
|
-
* Test-only escape hatch for pinning the install base to a local
|
|
1136
|
-
* checkout. Production runs leave this unset, so installs always use
|
|
1137
|
-
* `DEFAULT_SKILL_BASE`.
|
|
914
|
+
* - **Today**: `contract.json`, `contract.d.ts` are emitted on every
|
|
915
|
+
* `prisma-next contract emit`. Marking them `linguist-generated`
|
|
916
|
+
* keeps GitHub's diff stats honest and collapses the file in code
|
|
917
|
+
* review by default.
|
|
918
|
+
* - **Forward-looking**: `ops.json`, `migration.json` are not yet emitted
|
|
919
|
+
* by `init` flows but will be produced by adjacent commands (lower /
|
|
920
|
+
* migration tooling). Adding them now matches Decision 5
|
|
921
|
+
* (forward-looking subset) so the file does not need to be amended
|
|
922
|
+
* every time a new artifact lands.
|
|
1138
923
|
*
|
|
1139
|
-
*
|
|
1140
|
-
*
|
|
1141
|
-
*
|
|
1142
|
-
*
|
|
1143
|
-
* `
|
|
924
|
+
* `ARTIFACT_FILENAMES` entries are written relative to the schema
|
|
925
|
+
* directory so a user who runs `init --schema-path db/contract.prisma`
|
|
926
|
+
* gets `db/contract.json linguist-generated` — not the workspace-glob
|
|
927
|
+
* form `<glob>/contract.json` (which would over-match any unrelated
|
|
928
|
+
* `contract.json` the user has elsewhere) and not the absolute
|
|
929
|
+
* `DEFAULT_CONTRACT_SOURCE_DIR/contract.json` (which would silently
|
|
930
|
+
* break for a non-default schema path).
|
|
931
|
+
*
|
|
932
|
+
* The migration contract snapshot store (`migrations/snapshots/<hex>/`)
|
|
933
|
+
* is anchored to the migrations root instead, not the schema directory:
|
|
934
|
+
* migration package depth under `migrations/` varies (`app/<pkg>`,
|
|
935
|
+
* `<space>/<pkg>`, or a bare `<pkg>` in extension source repos), so no
|
|
936
|
+
* single schema-dir-relative pattern can reach every snapshot. See
|
|
937
|
+
* `STORE_GITATTRIBUTES_LINES` below.
|
|
1144
938
|
*/
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
939
|
+
const ARTIFACT_FILENAMES$1 = [
|
|
940
|
+
"contract.json",
|
|
941
|
+
"contract.d.ts",
|
|
942
|
+
"ops.json",
|
|
943
|
+
"migration.json"
|
|
944
|
+
];
|
|
945
|
+
const ATTRIBUTE = "linguist-generated";
|
|
1152
946
|
/**
|
|
1153
|
-
*
|
|
1154
|
-
*
|
|
1155
|
-
*
|
|
947
|
+
* Full `.gitattributes` lines for the migration contract snapshot store,
|
|
948
|
+
* already anchored to the migrations root — unlike `ARTIFACT_FILENAMES`,
|
|
949
|
+
* these are not combined with the schema-relative prefix.
|
|
1156
950
|
*/
|
|
1157
|
-
const
|
|
1158
|
-
"cursor",
|
|
1159
|
-
"claude-code",
|
|
1160
|
-
"codex",
|
|
1161
|
-
"windsurf"
|
|
1162
|
-
];
|
|
951
|
+
const STORE_GITATTRIBUTES_LINES = [`migrations/snapshots/**/contract.json ${ATTRIBUTE}`, `migrations/snapshots/**/contract.d.ts ${ATTRIBUTE}`];
|
|
1163
952
|
/**
|
|
1164
|
-
*
|
|
1165
|
-
*
|
|
1166
|
-
*
|
|
953
|
+
* Computes the `.gitattributes` lines this scaffold expects to own. Each
|
|
954
|
+
* line has the shape `<path> linguist-generated`. The `target` parameter
|
|
955
|
+
* is currently unused but accepted for symmetry with the other hygiene
|
|
956
|
+
* helpers and to leave room for target-specific entries (e.g. a future
|
|
957
|
+
* family-specific artifact) without a signature break.
|
|
1167
958
|
*/
|
|
1168
|
-
function
|
|
1169
|
-
const
|
|
1170
|
-
const
|
|
1171
|
-
|
|
1172
|
-
if (isLocalPath(base)) return url;
|
|
1173
|
-
if (source.ref === "cli") return `${url}#v${version}`;
|
|
1174
|
-
return url;
|
|
959
|
+
function requiredGitattributesLines(schemaDir, _target) {
|
|
960
|
+
const dir = schemaDir === "." ? "" : schemaDir.replace(/\/+$/, "");
|
|
961
|
+
const prefix = dir === "" ? "" : `${dir}/`;
|
|
962
|
+
return [...ARTIFACT_FILENAMES$1.map((file) => `${prefix}${file} ${ATTRIBUTE}`), ...STORE_GITATTRIBUTES_LINES];
|
|
1175
963
|
}
|
|
1176
964
|
/**
|
|
1177
|
-
*
|
|
1178
|
-
*
|
|
1179
|
-
*
|
|
1180
|
-
* rest of the install step so a single project consistently uses one
|
|
1181
|
-
* runner.
|
|
965
|
+
* Idempotent `.gitattributes` merge (FR3.4 / FR9.3). Returns the new file
|
|
966
|
+
* content given the existing content (or `undefined` if the file does
|
|
967
|
+
* not yet exist).
|
|
1182
968
|
*
|
|
1183
|
-
*
|
|
1184
|
-
*
|
|
1185
|
-
*
|
|
969
|
+
* Equivalence is exact-line: a user-customised line like
|
|
970
|
+
* `prisma/*.json linguist-generated` is *not* recognised as covering
|
|
971
|
+
* `DEFAULT_CONTRACT_SOURCE_DIR/contract.json linguist-generated`. We accept that
|
|
972
|
+
* over-specification — preserving the user's broad pattern *and*
|
|
973
|
+
* appending the narrow one — because the narrow lines are what the
|
|
974
|
+
* acceptance criteria pin (FR3.4 AC).
|
|
1186
975
|
*
|
|
1187
|
-
*
|
|
1188
|
-
*
|
|
976
|
+
* Returns `null` when no changes are required (file already contains
|
|
977
|
+
* every required entry).
|
|
1189
978
|
*/
|
|
1190
|
-
function
|
|
1191
|
-
|
|
1192
|
-
const
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
"--agent",
|
|
1197
|
-
...agents,
|
|
1198
|
-
"--skill",
|
|
1199
|
-
args.source.skill,
|
|
1200
|
-
"-y"
|
|
1201
|
-
];
|
|
1202
|
-
return formatPackageManagerCommand(args.pm, cliArgs);
|
|
979
|
+
function mergeGitattributes(existing, required) {
|
|
980
|
+
if (existing === void 0) return `${required.join("\n")}\n`;
|
|
981
|
+
const presentLines = new Set(existing.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")));
|
|
982
|
+
const missing = required.filter((line) => !presentLines.has(line));
|
|
983
|
+
if (missing.length === 0) return null;
|
|
984
|
+
return `${existing}${existing.length === 0 || existing.endsWith("\n") ? "" : "\n"}${missing.join("\n")}\n`;
|
|
1203
985
|
}
|
|
1204
986
|
/**
|
|
1205
|
-
*
|
|
987
|
+
* The minimal `.gitignore` lines a Prisma Next scaffold needs (FR3.3).
|
|
988
|
+
* Order matches what Node tooling typically writes today.
|
|
989
|
+
*
|
|
990
|
+
* `node_modules/` first because it's the byte-largest miss; `dist/`
|
|
991
|
+
* because the scaffolded `tsconfig.json` writes there; `.env` last so
|
|
992
|
+
* the secret-bearing file is the one most-recently visible in any diff
|
|
993
|
+
* (a paranoid-correct ordering — humans skim from the top).
|
|
1206
994
|
*/
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
}
|
|
1213
|
-
function formatPackageManagerCommand(pm, args) {
|
|
1214
|
-
switch (pm) {
|
|
1215
|
-
case "pnpm": return `pnpm dlx ${args.join(" ")}`;
|
|
1216
|
-
case "yarn": return `yarn dlx ${args.join(" ")}`;
|
|
1217
|
-
case "bun": return `bunx ${args.join(" ")}`;
|
|
1218
|
-
case "deno": return `deno run -A npm:${args.join(" ")}`;
|
|
1219
|
-
case "npm": return `npx ${args.join(" ")}`;
|
|
1220
|
-
}
|
|
1221
|
-
}
|
|
995
|
+
const REQUIRED_GITIGNORE_ENTRIES = [
|
|
996
|
+
"node_modules/",
|
|
997
|
+
"dist/",
|
|
998
|
+
".env"
|
|
999
|
+
];
|
|
1222
1000
|
/**
|
|
1223
|
-
*
|
|
1224
|
-
*
|
|
1225
|
-
*
|
|
1226
|
-
*
|
|
1227
|
-
*
|
|
1228
|
-
*
|
|
1229
|
-
*
|
|
1001
|
+
* Idempotent `.gitignore` merge (FR3.3 / FR9.3). Returns the new file
|
|
1002
|
+
* content given the existing content (or `undefined` if the file does
|
|
1003
|
+
* not yet exist). Adds only entries that are not already present and
|
|
1004
|
+
* never duplicates a line. Existing comments and blank lines are
|
|
1005
|
+
* preserved verbatim — `.gitignore` is parsed by `git` without a tree,
|
|
1006
|
+
* so any line modification risks changing semantics.
|
|
1007
|
+
*
|
|
1008
|
+
* Pattern equivalence is line-literal: `node_modules/` and `node_modules`
|
|
1009
|
+
* are treated as different entries. This is intentional — `git` treats
|
|
1010
|
+
* them differently (the trailing slash restricts the match to
|
|
1011
|
+
* directories), and the AC pins the trailing-slash form.
|
|
1012
|
+
*
|
|
1013
|
+
* Returns `null` when no changes are required (file already contains
|
|
1014
|
+
* every required entry). The caller can use this to decide whether to
|
|
1015
|
+
* include `.gitignore` in `filesWritten`.
|
|
1230
1016
|
*/
|
|
1231
|
-
function
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
}
|
|
1017
|
+
function mergeGitignore(existing) {
|
|
1018
|
+
if (existing === void 0) return `${REQUIRED_GITIGNORE_ENTRIES.join("\n")}\n`;
|
|
1019
|
+
const present = new Set(existing.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")));
|
|
1020
|
+
const missing = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !present.has(entry));
|
|
1021
|
+
if (missing.length === 0) return null;
|
|
1022
|
+
return `${existing}${existing.length === 0 || existing.endsWith("\n") ? "" : "\n"}${missing.join("\n")}\n`;
|
|
1237
1023
|
}
|
|
1024
|
+
const REQUIRED_SCRIPTS = [{
|
|
1025
|
+
name: "contract:emit",
|
|
1026
|
+
command: "prisma-next contract emit"
|
|
1027
|
+
}];
|
|
1238
1028
|
/**
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1243
|
-
*
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1246
|
-
*
|
|
1029
|
+
* Idempotent `package.json#scripts` merge with collision detection
|
|
1030
|
+
* (FR3.5 / FR9.3):
|
|
1031
|
+
*
|
|
1032
|
+
* - If a required script is **missing**, append it.
|
|
1033
|
+
* - If a required script is **already present and identical**, leave
|
|
1034
|
+
* the file alone (idempotency).
|
|
1035
|
+
* - If a required script is **present but maps to a different command**,
|
|
1036
|
+
* skip the write for that script and surface a structured warning.
|
|
1037
|
+
* The user's override is sacred — `init` should never silently
|
|
1038
|
+
* overwrite a custom build pipeline.
|
|
1039
|
+
*
|
|
1040
|
+
* Preserves the existing key order (so a user who has alphabetised
|
|
1041
|
+
* their scripts does not see them reshuffled) and appends new entries
|
|
1042
|
+
* at the end.
|
|
1043
|
+
*
|
|
1044
|
+
* The `package.json` is parsed and re-stringified through `JSON` —
|
|
1045
|
+
* comments are not preserved (package.json does not support them per
|
|
1046
|
+
* spec). Trailing newline matches the original input's trailing
|
|
1047
|
+
* newline behaviour.
|
|
1247
1048
|
*/
|
|
1248
|
-
|
|
1249
|
-
const
|
|
1250
|
-
const
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
filesWritten: ctx.filesWritten,
|
|
1260
|
-
cause: redactSecrets$1(readChildStderr$1(err)) || (err instanceof Error ? err.message : String(err))
|
|
1261
|
-
});
|
|
1049
|
+
function mergePackageScripts(existing, required = REQUIRED_SCRIPTS) {
|
|
1050
|
+
const parsed = blindCast(JSON.parse(existing));
|
|
1051
|
+
const scripts = typeof parsed["scripts"] === "object" && parsed["scripts"] !== null ? { ...blindCast(parsed["scripts"]) } : {};
|
|
1052
|
+
const warnings = [];
|
|
1053
|
+
let mutated = false;
|
|
1054
|
+
for (const { name, command } of required) {
|
|
1055
|
+
const existingValue = scripts[name];
|
|
1056
|
+
if (existingValue === void 0) {
|
|
1057
|
+
scripts[name] = command;
|
|
1058
|
+
mutated = true;
|
|
1059
|
+
continue;
|
|
1262
1060
|
}
|
|
1061
|
+
if (existingValue !== command) warnings.push(`package.json already has a "${name}" script with a different command — keeping yours.\n existing: ${existingValue}\n expected: ${command}\nIf you want the default, remove your "${name}" script and re-run \`init\`.`);
|
|
1263
1062
|
}
|
|
1063
|
+
if (!mutated) return {
|
|
1064
|
+
content: null,
|
|
1065
|
+
warnings
|
|
1066
|
+
};
|
|
1067
|
+
parsed["scripts"] = scripts;
|
|
1068
|
+
const trailingNewline = existing.endsWith("\n") ? "\n" : "";
|
|
1264
1069
|
return {
|
|
1265
|
-
|
|
1266
|
-
|
|
1070
|
+
content: `${JSON.stringify(parsed, null, 2)}${trailingNewline}`,
|
|
1071
|
+
warnings
|
|
1267
1072
|
};
|
|
1268
1073
|
}
|
|
1269
|
-
function readChildStderr$1(err) {
|
|
1270
|
-
if (err instanceof Error && "stderr" in err) return String(err.stderr ?? "");
|
|
1271
|
-
return "";
|
|
1272
|
-
}
|
|
1273
1074
|
/**
|
|
1274
|
-
*
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
1277
|
-
* those bubble into the structured `errorInitSkillInstallFailed`
|
|
1278
|
-
* envelope, which ends up in logs and CI output. Redact at the
|
|
1279
|
-
* boundary so we never re-emit a secret.
|
|
1075
|
+
* Idempotently sets `"type": "module"` on a `package.json` so the
|
|
1076
|
+
* scaffolded `prisma/db.ts` — which uses the ESM-only `with { type: 'json' }`
|
|
1077
|
+
* import attribute — loads as ES module under Node's loader (TML-2494).
|
|
1280
1078
|
*
|
|
1281
|
-
*
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
*
|
|
1289
|
-
*
|
|
1290
|
-
*
|
|
1291
|
-
*
|
|
1292
|
-
*
|
|
1293
|
-
*
|
|
1294
|
-
*
|
|
1295
|
-
*
|
|
1296
|
-
*
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
"prisma-next",
|
|
1300
|
-
"prisma-next-quickstart",
|
|
1301
|
-
"prisma-next-contract",
|
|
1302
|
-
"prisma-next-migrations",
|
|
1303
|
-
"prisma-next-migration-review",
|
|
1304
|
-
"prisma-8-migration-review",
|
|
1305
|
-
"prisma-next-queries",
|
|
1306
|
-
"prisma-next-runtime",
|
|
1307
|
-
"prisma-next-build",
|
|
1308
|
-
"prisma-next-supabase",
|
|
1309
|
-
"prisma-next-debug",
|
|
1310
|
-
"prisma-next-feedback",
|
|
1311
|
-
"prisma-next-extension-upgrade"
|
|
1312
|
-
];
|
|
1313
|
-
/**
|
|
1314
|
-
* Project-level install roots the upstream `skills` CLI uses for the
|
|
1315
|
-
* agents in `DEFAULT_SKILL_AGENTS`: cursor and codex install into
|
|
1316
|
-
* `.agents/skills`, claude-code into `.claude/skills`, windsurf into
|
|
1317
|
-
* `.windsurf/skills`.
|
|
1318
|
-
*/
|
|
1319
|
-
const AGENT_SKILL_ROOTS = [
|
|
1320
|
-
".agents/skills",
|
|
1321
|
-
".claude/skills",
|
|
1322
|
-
".windsurf/skills"
|
|
1323
|
-
];
|
|
1324
|
-
/**
|
|
1325
|
-
* Every directory a retired per-workflow skill may occupy in a
|
|
1326
|
-
* consumer project. Init deletes each (recursively) before running the
|
|
1327
|
-
* skill install.
|
|
1079
|
+
* Without this field Node either:
|
|
1080
|
+
*
|
|
1081
|
+
* - emits `MODULE_TYPELESS_PACKAGE_JSON` and reparses the file as ESM
|
|
1082
|
+
* with a perf penalty (Node 22+ with `--experimental-strip-types`), or
|
|
1083
|
+
* - hard-fails with `ERR_*` because the CJS loader cannot parse the
|
|
1084
|
+
* import-attribute syntax (older Node, or any tool that doesn't
|
|
1085
|
+
* reparse).
|
|
1086
|
+
*
|
|
1087
|
+
* Behaviour:
|
|
1088
|
+
*
|
|
1089
|
+
* - **Field missing** → set to `"module"`. New entry is inserted right
|
|
1090
|
+
* after `"name"` (when present) so the diff lands in a conventional
|
|
1091
|
+
* spot for human review; falls through to the natural append position
|
|
1092
|
+
* otherwise.
|
|
1093
|
+
* - **Field already `"module"`** → no-op (idempotent).
|
|
1094
|
+
* - **Field set to anything else** (e.g. `"commonjs"`) → leave it alone
|
|
1095
|
+
* and surface a structured warning. The user explicitly opted out of
|
|
1096
|
+
* ESM and we don't silently overwrite that.
|
|
1328
1097
|
*/
|
|
1329
|
-
function
|
|
1330
|
-
|
|
1098
|
+
function ensureEsmModuleType(existing) {
|
|
1099
|
+
const parsed = blindCast(JSON.parse(existing));
|
|
1100
|
+
const currentType = parsed["type"];
|
|
1101
|
+
if (currentType === "module") return {
|
|
1102
|
+
content: null,
|
|
1103
|
+
warning: null
|
|
1104
|
+
};
|
|
1105
|
+
if (typeof currentType === "string" && currentType !== "module") return {
|
|
1106
|
+
content: null,
|
|
1107
|
+
warning: `package.json declares "type": "${currentType}" — keeping yours, but the scaffolded prisma/db.ts uses an ESM-only import attribute (\`with { type: 'json' }\`) and will not load under that module type.\nIf you want the default, set "type": "module" in package.json.`
|
|
1108
|
+
};
|
|
1109
|
+
const next = {};
|
|
1110
|
+
let inserted = false;
|
|
1111
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
1112
|
+
if (key === "type") continue;
|
|
1113
|
+
next[key] = value;
|
|
1114
|
+
if (!inserted && key === "name") {
|
|
1115
|
+
next["type"] = "module";
|
|
1116
|
+
inserted = true;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
if (!inserted) next["type"] = "module";
|
|
1120
|
+
const trailingNewline = existing.endsWith("\n") ? "\n" : "";
|
|
1121
|
+
return {
|
|
1122
|
+
content: `${JSON.stringify(next, null, 2)}${trailingNewline}`,
|
|
1123
|
+
warning: null
|
|
1124
|
+
};
|
|
1331
1125
|
}
|
|
1332
1126
|
/**
|
|
1333
|
-
*
|
|
1334
|
-
*
|
|
1335
|
-
* `
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1127
|
+
* Filenames the contract pipeline emits next to the user's schema source
|
|
1128
|
+
* (`<schemaDir>/contract.json`, `<schemaDir>/contract.d.ts`, …). Mirrors
|
|
1129
|
+
* the schema-dir-relative `ARTIFACT_FILENAMES` in `hygiene-gitattributes.ts`
|
|
1130
|
+
* (not that file's migrations-root-anchored store lines, which are outside
|
|
1131
|
+
* this command's scope — reinit only ever touches `schemaDir`); kept as a
|
|
1132
|
+
* separate constant here because the cleanup contract is target-agnostic
|
|
1133
|
+
* and we deliberately do not want a stale artifact from a previous target
|
|
1134
|
+
* lingering after a re-init.
|
|
1338
1135
|
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1341
|
-
*
|
|
1342
|
-
*
|
|
1343
|
-
*
|
|
1136
|
+
* If a future emit pipeline produces an additional schema-dir artifact,
|
|
1137
|
+
* add it here **and** to `ARTIFACT_FILENAMES` in `hygiene-gitattributes.ts`
|
|
1138
|
+
* — the two stay in lockstep so the file `init` advertises as
|
|
1139
|
+
* `linguist-generated` is exactly the file `init` is willing to delete on
|
|
1140
|
+
* re-init.
|
|
1344
1141
|
*/
|
|
1345
|
-
const
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
mongo: "MongoDB"
|
|
1352
|
-
};
|
|
1142
|
+
const ARTIFACT_FILENAMES = [
|
|
1143
|
+
"contract.json",
|
|
1144
|
+
"contract.d.ts",
|
|
1145
|
+
"ops.json",
|
|
1146
|
+
"migration.json"
|
|
1147
|
+
];
|
|
1353
1148
|
/**
|
|
1354
|
-
*
|
|
1355
|
-
*
|
|
1356
|
-
*
|
|
1357
|
-
*
|
|
1358
|
-
*
|
|
1149
|
+
* Returns the schema-relative paths of stale contract artifacts the
|
|
1150
|
+
* previous `init` run (or a `contract emit`) left behind in `schemaDir`.
|
|
1151
|
+
* Paths are returned relative to `baseDir` so the caller can plumb them
|
|
1152
|
+
* into `filesWritten`-style logging without re-deriving the path.
|
|
1153
|
+
*
|
|
1154
|
+
* Pure function: no filesystem mutation. Used by `runInit`'s precondition
|
|
1155
|
+
* phase (FR6.2 / NFR3 atomicity) so a downstream parse failure leaves
|
|
1156
|
+
* the artifacts on disk and the project byte-identical to its pre-init
|
|
1157
|
+
* state.
|
|
1359
1158
|
*/
|
|
1360
|
-
function
|
|
1361
|
-
const
|
|
1362
|
-
const
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
lines.push(`# Requires ${label} >= ${minVersion}.`);
|
|
1366
|
-
lines.push("");
|
|
1367
|
-
if (target === "postgres") lines.push("DATABASE_URL=\"postgresql://user:password@localhost:5432/mydb\"");
|
|
1368
|
-
else {
|
|
1369
|
-
lines.push("# Standalone local mongod / `docker run mongo:8` — no replica set required for first-run queries.");
|
|
1370
|
-
lines.push("# Transactions and change streams need a replica set; add ?replicaSet=... only after initiating one.");
|
|
1371
|
-
lines.push("");
|
|
1372
|
-
lines.push("DATABASE_URL=\"mongodb://user:password@localhost:27017/mydb\"");
|
|
1159
|
+
function findStaleArtifacts(baseDir, schemaDir) {
|
|
1160
|
+
const result = [];
|
|
1161
|
+
for (const filename of ARTIFACT_FILENAMES) {
|
|
1162
|
+
const rel = join(schemaDir, filename);
|
|
1163
|
+
if (existsSync(join(baseDir, rel))) result.push(rel);
|
|
1373
1164
|
}
|
|
1374
|
-
|
|
1375
|
-
return lines.join("\n");
|
|
1165
|
+
return result;
|
|
1376
1166
|
}
|
|
1377
1167
|
/**
|
|
1378
|
-
*
|
|
1168
|
+
* Drops a single key from `package.json#dependencies`, returning the new
|
|
1169
|
+
* file content. Returns `null` when the dependency was already absent —
|
|
1170
|
+
* the caller can skip the write to keep re-init idempotent (FR9.3).
|
|
1379
1171
|
*
|
|
1380
|
-
*
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
1383
|
-
*
|
|
1384
|
-
*
|
|
1385
|
-
*
|
|
1386
|
-
*
|
|
1387
|
-
*
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
return lines.join("\n");
|
|
1394
|
-
}
|
|
1395
|
-
/**
|
|
1396
|
-
* Renders the initial `.env` content for `--write-env` / interactive
|
|
1397
|
-
* opt-in. Same placeholder body as `.env.example`, **without** the
|
|
1398
|
-
* example file's "Copy this file to `.env`…" intro: the real `.env` is
|
|
1399
|
-
* the destination of that copy, so the line would lie. Writing this
|
|
1400
|
-
* file is gitignored (`.env` lands in `.gitignore` during init).
|
|
1172
|
+
* Used by `runInit` for the FR9.2 target-switch path: when the user
|
|
1173
|
+
* re-inits a project from `--target postgres` to `--target mongodb` (or
|
|
1174
|
+
* vice versa), the previous facade is removed from `dependencies` so the
|
|
1175
|
+
* resulting project depends only on the chosen target's facade.
|
|
1176
|
+
*
|
|
1177
|
+
* Devs/peers/optional dep groups are intentionally *not* touched — the
|
|
1178
|
+
* facades are only ever in `dependencies` (FR4 / FR7), and broadening
|
|
1179
|
+
* the search would risk clobbering an unrelated dep with the same name
|
|
1180
|
+
* in `peerDependencies`.
|
|
1181
|
+
*
|
|
1182
|
+
* Throws `SyntaxError` if `existing` is not parseable as JSON; the
|
|
1183
|
+
* caller (`runInit`) already guards on that with a structured 5010
|
|
1184
|
+
* error before this helper is reached.
|
|
1401
1185
|
*/
|
|
1402
|
-
function
|
|
1403
|
-
|
|
1186
|
+
function removeDependency(existing, depName) {
|
|
1187
|
+
const parsed = JSON.parse(existing);
|
|
1188
|
+
const deps = parsed["dependencies"];
|
|
1189
|
+
if (deps === null || typeof deps !== "object" || Array.isArray(deps)) return null;
|
|
1190
|
+
if (!Object.hasOwn(deps, depName)) return null;
|
|
1191
|
+
const next = { ...deps };
|
|
1192
|
+
delete next[depName];
|
|
1193
|
+
parsed["dependencies"] = next;
|
|
1194
|
+
const trailingNewline = existing.endsWith("\n") ? "\n" : "";
|
|
1195
|
+
return `${JSON.stringify(parsed, null, 2)}${trailingNewline}`;
|
|
1404
1196
|
}
|
|
1405
1197
|
function renderTemplate(templateFile, variableNames, vars) {
|
|
1406
1198
|
let result = readFileSync(join(import.meta.dirname, templateFile), "utf-8");
|
|
@@ -1646,481 +1438,6 @@ function mergeTypesArray(existing) {
|
|
|
1646
1438
|
return result;
|
|
1647
1439
|
}
|
|
1648
1440
|
/**
|
|
1649
|
-
* Runs the `init` command end-to-end and returns the exit code. Catches
|
|
1650
|
-
* structured CLI errors raised at every phase (input resolution, install,
|
|
1651
|
-
* emit) and renders them via the same UI surface as success output
|
|
1652
|
-
* (`--json` to stdout, human to stderr). Exit codes follow the documented
|
|
1653
|
-
* stable set in `./exit-codes.ts` and the
|
|
1654
|
-
* [Style Guide § Exit Codes](../../../../../../../docs/CLI%20Style%20Guide.md#exit-codes).
|
|
1655
|
-
*
|
|
1656
|
-
* Layered for testability: the action handler in `./index.ts` is
|
|
1657
|
-
* responsible for parsing flags and constructing `runOptions`; this
|
|
1658
|
-
* function does no flag parsing of its own.
|
|
1659
|
-
*/
|
|
1660
|
-
async function runInit(baseDir, runOptions) {
|
|
1661
|
-
const { options, flags, canPrompt, probeOverrides } = runOptions;
|
|
1662
|
-
const ui = createTerminalUI(flags);
|
|
1663
|
-
const warnings = [];
|
|
1664
|
-
const filesWritten = [];
|
|
1665
|
-
const filesDeleted = [];
|
|
1666
|
-
if (!flags.json && !flags.quiet) clack.intro("prisma-next init", { output: process.stderr });
|
|
1667
|
-
let inputs;
|
|
1668
|
-
try {
|
|
1669
|
-
inputs = await resolveInitInputs({
|
|
1670
|
-
baseDir,
|
|
1671
|
-
options,
|
|
1672
|
-
flags,
|
|
1673
|
-
canPrompt
|
|
1674
|
-
});
|
|
1675
|
-
} catch (error) {
|
|
1676
|
-
if (CliStructuredError$1.is(error)) return emitError(ui, flags, error);
|
|
1677
|
-
throw error;
|
|
1678
|
-
}
|
|
1679
|
-
const pm = await detectPackageManager(baseDir);
|
|
1680
|
-
const pkgRun = formatRunCommand(pm, "prisma-next", "").trimEnd();
|
|
1681
|
-
const schemaDir = dirname(inputs.schemaPath);
|
|
1682
|
-
const configContractPath = isAbsolute(inputs.schemaPath) ? inputs.schemaPath : `./${inputs.schemaPath}`;
|
|
1683
|
-
const resolveImportSpecifier = scaffoldSpecifierResolverFor(inputs.target);
|
|
1684
|
-
const filesToWrite = [
|
|
1685
|
-
{
|
|
1686
|
-
path: inputs.schemaPath,
|
|
1687
|
-
content: starterSchema(inputs.target, inputs.authoring, resolveImportSpecifier)
|
|
1688
|
-
},
|
|
1689
|
-
{
|
|
1690
|
-
path: "prisma-next.config.ts",
|
|
1691
|
-
content: configFile(inputs.target, configContractPath, resolveImportSpecifier)
|
|
1692
|
-
},
|
|
1693
|
-
{
|
|
1694
|
-
path: join(schemaDir, "db.ts"),
|
|
1695
|
-
content: dbFile(inputs.target, resolveImportSpecifier)
|
|
1696
|
-
},
|
|
1697
|
-
{
|
|
1698
|
-
path: "prisma-next.md",
|
|
1699
|
-
content: quickReferenceMd(inputs.target, inputs.authoring, inputs.schemaPath, pkgRun, resolveImportSpecifier)
|
|
1700
|
-
},
|
|
1701
|
-
{
|
|
1702
|
-
path: ".env.example",
|
|
1703
|
-
content: envExampleContent(inputs.target)
|
|
1704
|
-
}
|
|
1705
|
-
];
|
|
1706
|
-
const filesToDelete = inputs.reinit ? [...findStaleArtifacts(baseDir, schemaDir)] : [];
|
|
1707
|
-
const legacyDirsToDelete = legacySkillDirs().filter((rel) => existsSync(join(baseDir, rel)));
|
|
1708
|
-
if (inputs.writeEnv) if (!existsSync(join(baseDir, ".env"))) filesToWrite.push({
|
|
1709
|
-
path: ".env",
|
|
1710
|
-
content: envFileContent(inputs.target)
|
|
1711
|
-
});
|
|
1712
|
-
else warnings.push(".env already exists; leaving it untouched. Compare with .env.example for any new keys.");
|
|
1713
|
-
const tsconfigPath = join(baseDir, "tsconfig.json");
|
|
1714
|
-
if (existsSync(tsconfigPath)) {
|
|
1715
|
-
const existing = readFileSync(tsconfigPath, "utf-8");
|
|
1716
|
-
let merged;
|
|
1717
|
-
try {
|
|
1718
|
-
merged = mergeTsConfig(existing);
|
|
1719
|
-
} catch (err) {
|
|
1720
|
-
if (err instanceof TsConfigParseError) return emitError(ui, flags, errorInitInvalidTsconfig({
|
|
1721
|
-
path: "tsconfig.json",
|
|
1722
|
-
cause: err.message
|
|
1723
|
-
}));
|
|
1724
|
-
throw err;
|
|
1725
|
-
}
|
|
1726
|
-
filesToWrite.push({
|
|
1727
|
-
path: "tsconfig.json",
|
|
1728
|
-
content: merged,
|
|
1729
|
-
logMessage: "Updated tsconfig.json with required compiler options."
|
|
1730
|
-
});
|
|
1731
|
-
} else filesToWrite.push({
|
|
1732
|
-
path: "tsconfig.json",
|
|
1733
|
-
content: defaultTsConfig()
|
|
1734
|
-
});
|
|
1735
|
-
const gitignorePath = join(baseDir, ".gitignore");
|
|
1736
|
-
const newGitignore = mergeGitignore(existsSync(gitignorePath) ? readFileSync(gitignorePath, "utf-8") : void 0);
|
|
1737
|
-
if (newGitignore !== null) filesToWrite.push({
|
|
1738
|
-
path: ".gitignore",
|
|
1739
|
-
content: newGitignore
|
|
1740
|
-
});
|
|
1741
|
-
const gitattributesPath = join(baseDir, ".gitattributes");
|
|
1742
|
-
const newGitattributes = mergeGitattributes(existsSync(gitattributesPath) ? readFileSync(gitattributesPath, "utf-8") : void 0, requiredGitattributesLines(schemaDir, inputs.target));
|
|
1743
|
-
if (newGitattributes !== null) filesToWrite.push({
|
|
1744
|
-
path: ".gitattributes",
|
|
1745
|
-
content: newGitattributes
|
|
1746
|
-
});
|
|
1747
|
-
const packageJsonPath = join(baseDir, "package.json");
|
|
1748
|
-
const packageJsonExisted = existsSync(packageJsonPath);
|
|
1749
|
-
const synthesisePackageJson = !packageJsonExisted && !hasProjectManifest(baseDir);
|
|
1750
|
-
let parsedPackageJson = null;
|
|
1751
|
-
if (packageJsonExisted || synthesisePackageJson) {
|
|
1752
|
-
const pkgRaw = packageJsonExisted ? readFileSync(packageJsonPath, "utf-8") : defaultPackageJsonContent(basename(baseDir));
|
|
1753
|
-
try {
|
|
1754
|
-
parsedPackageJson = JSON.parse(pkgRaw);
|
|
1755
|
-
} catch (err) {
|
|
1756
|
-
if (err instanceof SyntaxError) return emitError(ui, flags, errorInitInvalidManifest({
|
|
1757
|
-
path: "package.json",
|
|
1758
|
-
cause: err.message
|
|
1759
|
-
}));
|
|
1760
|
-
throw err;
|
|
1761
|
-
}
|
|
1762
|
-
let workingPkg = pkgRaw;
|
|
1763
|
-
let pkgChanged = synthesisePackageJson;
|
|
1764
|
-
if (inputs.removePreviousFacade !== null) {
|
|
1765
|
-
const next = removeDependency(workingPkg, inputs.removePreviousFacade);
|
|
1766
|
-
if (next !== null) {
|
|
1767
|
-
workingPkg = next;
|
|
1768
|
-
pkgChanged = true;
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
const { content: nextPkg, warnings: scriptWarnings } = mergePackageScripts(workingPkg, REQUIRED_SCRIPTS);
|
|
1772
|
-
if (nextPkg !== null) {
|
|
1773
|
-
workingPkg = nextPkg;
|
|
1774
|
-
pkgChanged = true;
|
|
1775
|
-
}
|
|
1776
|
-
const { content: typedPkg, warning: typeWarning } = ensureEsmModuleType(workingPkg);
|
|
1777
|
-
if (typedPkg !== null) {
|
|
1778
|
-
workingPkg = typedPkg;
|
|
1779
|
-
pkgChanged = true;
|
|
1780
|
-
}
|
|
1781
|
-
if (pkgChanged) filesToWrite.push({
|
|
1782
|
-
path: "package.json",
|
|
1783
|
-
content: workingPkg
|
|
1784
|
-
});
|
|
1785
|
-
warnings.push(...scriptWarnings);
|
|
1786
|
-
if (typeWarning !== null) warnings.push(typeWarning);
|
|
1787
|
-
if (synthesisePackageJson) warnings.push("No package.json found in the target directory; created a minimal one. Edit `name` / `version` to taste.");
|
|
1788
|
-
}
|
|
1789
|
-
if (existsSync(join(baseDir, "src/index.ts"))) if (!existsSync(join(baseDir, "README.md"))) {
|
|
1790
|
-
const rawName = parsedPackageJson !== null && typeof parsedPackageJson["name"] === "string" ? parsedPackageJson["name"] : basename(baseDir);
|
|
1791
|
-
filesToWrite.push({
|
|
1792
|
-
path: "README.md",
|
|
1793
|
-
content: minimalProjectReadmeMd(inputs.target, inputs.schemaPath, sanitisePackageName(rawName), pm)
|
|
1794
|
-
});
|
|
1795
|
-
} else warnings.push("README.md already exists; leaving it untouched.");
|
|
1796
|
-
for (const file of filesToWrite) {
|
|
1797
|
-
const fullPath = join(baseDir, file.path);
|
|
1798
|
-
mkdirSync(dirname(fullPath), { recursive: true });
|
|
1799
|
-
writeFileSync(fullPath, file.content, "utf-8");
|
|
1800
|
-
filesWritten.push(file.path);
|
|
1801
|
-
if (file.logMessage !== void 0 && !flags.json && !flags.quiet) ui.log(file.logMessage);
|
|
1802
|
-
}
|
|
1803
|
-
for (const rel of filesToDelete) {
|
|
1804
|
-
const fullPath = join(baseDir, rel);
|
|
1805
|
-
if (!existsSync(fullPath)) continue;
|
|
1806
|
-
try {
|
|
1807
|
-
unlinkSync(fullPath);
|
|
1808
|
-
filesDeleted.push(rel);
|
|
1809
|
-
} catch (err) {
|
|
1810
|
-
if (!(err instanceof Error && "code" in err && err.code === "ENOENT")) throw err;
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
for (const rel of legacyDirsToDelete) {
|
|
1814
|
-
rmSync(join(baseDir, rel), {
|
|
1815
|
-
recursive: true,
|
|
1816
|
-
force: true
|
|
1817
|
-
});
|
|
1818
|
-
filesDeleted.push(rel);
|
|
1819
|
-
}
|
|
1820
|
-
const emitCommand = formatRunCommand(pm, "prisma-next", "contract emit");
|
|
1821
|
-
let install;
|
|
1822
|
-
try {
|
|
1823
|
-
install = await runInstall({
|
|
1824
|
-
baseDir,
|
|
1825
|
-
pm,
|
|
1826
|
-
target: inputs.target,
|
|
1827
|
-
install: inputs.install,
|
|
1828
|
-
flags,
|
|
1829
|
-
ui,
|
|
1830
|
-
filesWritten,
|
|
1831
|
-
resolveImportSpecifier,
|
|
1832
|
-
hasTypesNode: parsedPackageJson !== null ? hasDirectDep(parsedPackageJson, "@types/node") : false
|
|
1833
|
-
});
|
|
1834
|
-
} catch (error) {
|
|
1835
|
-
if (CliStructuredError$1.is(error)) return emitError(ui, flags, error);
|
|
1836
|
-
throw error;
|
|
1837
|
-
}
|
|
1838
|
-
warnings.push(...install.warnings);
|
|
1839
|
-
let contractEmitted = false;
|
|
1840
|
-
if (!install.skipped) try {
|
|
1841
|
-
await runEmit({
|
|
1842
|
-
baseDir,
|
|
1843
|
-
ui,
|
|
1844
|
-
filesWritten,
|
|
1845
|
-
emitCommand
|
|
1846
|
-
});
|
|
1847
|
-
contractEmitted = true;
|
|
1848
|
-
} catch (error) {
|
|
1849
|
-
if (CliStructuredError$1.is(error)) return emitError(ui, flags, error);
|
|
1850
|
-
throw error;
|
|
1851
|
-
}
|
|
1852
|
-
if (inputs.probeDb) {
|
|
1853
|
-
const escalated = applyProbeOutcome(await probeServerVersion({
|
|
1854
|
-
baseDir,
|
|
1855
|
-
target: inputs.target,
|
|
1856
|
-
databaseUrl: process.env["DATABASE_URL"],
|
|
1857
|
-
minVersion: MIN_SERVER_VERSION[inputs.target]
|
|
1858
|
-
}, probeOverrides ?? {}), {
|
|
1859
|
-
strictProbe: inputs.strictProbe,
|
|
1860
|
-
warnings
|
|
1861
|
-
});
|
|
1862
|
-
if (escalated !== null) return emitError(ui, flags, errorInitProbeFailed({
|
|
1863
|
-
cause: escalated,
|
|
1864
|
-
filesWritten
|
|
1865
|
-
}));
|
|
1866
|
-
}
|
|
1867
|
-
const manualProjectSkillSummary = DEFAULT_SKILL_SOURCES.map((source) => formatSkillInstallCommand({
|
|
1868
|
-
pm: install.effectivePm,
|
|
1869
|
-
source
|
|
1870
|
-
})).map((c) => `\`${c}\``).join(" && ");
|
|
1871
|
-
let skillRegistered = false;
|
|
1872
|
-
if (!inputs.installProjectSkill) warnings.push(`Skipped Prisma Next skills install (--no-skill). To install the skills later, run: ${manualProjectSkillSummary}`);
|
|
1873
|
-
else {
|
|
1874
|
-
const spinner = ui.spinner();
|
|
1875
|
-
spinner.start("Registering Prisma Next skills with the agent runtime...");
|
|
1876
|
-
try {
|
|
1877
|
-
const project = await runProjectLevelSkillInstall({
|
|
1878
|
-
baseDir,
|
|
1879
|
-
pm: install.effectivePm,
|
|
1880
|
-
filesWritten
|
|
1881
|
-
});
|
|
1882
|
-
spinner.stop(`Registered Prisma Next skills (project level) — ran ${project.commands.map((c) => `\`${c}\``).join(", ")}`);
|
|
1883
|
-
skillRegistered = true;
|
|
1884
|
-
} catch (error) {
|
|
1885
|
-
spinner.stop("Agent-skill install failed");
|
|
1886
|
-
if (CliStructuredError$1.is(error)) return emitError(ui, flags, error);
|
|
1887
|
-
throw error;
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
const output = {
|
|
1891
|
-
ok: true,
|
|
1892
|
-
target: inputs.target === "mongo" ? "mongodb" : "postgres",
|
|
1893
|
-
authoring: inputs.authoring,
|
|
1894
|
-
schemaPath: inputs.schemaPath,
|
|
1895
|
-
filesWritten,
|
|
1896
|
-
filesDeleted,
|
|
1897
|
-
packagesInstalled: {
|
|
1898
|
-
skipped: install.skipped,
|
|
1899
|
-
deps: [...install.deps],
|
|
1900
|
-
devDeps: [...install.devDeps]
|
|
1901
|
-
},
|
|
1902
|
-
contractEmitted,
|
|
1903
|
-
nextSteps: buildNextSteps({
|
|
1904
|
-
target: inputs.target === "mongo" ? "mongodb" : "postgres",
|
|
1905
|
-
contractEmitted,
|
|
1906
|
-
emitCommand,
|
|
1907
|
-
schemaPath: inputs.schemaPath,
|
|
1908
|
-
skillRegistered
|
|
1909
|
-
}),
|
|
1910
|
-
warnings
|
|
1911
|
-
};
|
|
1912
|
-
const validated = InitOutputSchema(output);
|
|
1913
|
-
if (validated instanceof Error || validated.problems !== void 0) return emitError(ui, flags, new CliStructuredError$1("CLI.INIT_INVALID_OUTPUT_DOCUMENT", "Init produced an invalid output document", {
|
|
1914
|
-
why: `The success document failed schema validation: ${String(validated)}`,
|
|
1915
|
-
fix: "This is a bug in prisma-next. Please report it with the full `-v` output.",
|
|
1916
|
-
docsUrl: docsUrlFor("CLI.INIT_INVALID_OUTPUT_DOCUMENT")
|
|
1917
|
-
}));
|
|
1918
|
-
if (flags.json) ui.output(formatInitJson(output));
|
|
1919
|
-
else {
|
|
1920
|
-
renderInitOutro(ui, output, flags);
|
|
1921
|
-
if (!flags.quiet) clack.outro("Done. Open prisma-next.md to get started.", { output: process.stderr });
|
|
1922
|
-
}
|
|
1923
|
-
return 0;
|
|
1924
|
-
}
|
|
1925
|
-
/**
|
|
1926
|
-
* Renders a structured CLI error to the right channel and returns the exit
|
|
1927
|
-
* code derived from the error's PN code. JSON-mode errors go to stdout
|
|
1928
|
-
* (so consumers always parse from one place); human-mode errors go to
|
|
1929
|
-
* stderr. Mirrors `handleResult` but returns init-specific exit codes
|
|
1930
|
-
* rather than the CLI/RUN binary.
|
|
1931
|
-
*/
|
|
1932
|
-
function emitError(ui, flags, error) {
|
|
1933
|
-
const envelope = error.toEnvelope();
|
|
1934
|
-
if (flags.json) ui.output(formatErrorJson(envelope));
|
|
1935
|
-
else ui.error(formatErrorOutput(envelope, flags));
|
|
1936
|
-
return exitCodeForError(error);
|
|
1937
|
-
}
|
|
1938
|
-
/**
|
|
1939
|
-
* Maps a structured init error to its documented exit code. Centralised so
|
|
1940
|
-
* the error → exit-code contract lives next to the codes themselves.
|
|
1941
|
-
*
|
|
1942
|
-
* `CLI.INIT_INVALID_OUTPUT_DOCUMENT` (and the unknown-code default branch)
|
|
1943
|
-
* routes to `INIT_EXIT_INTERNAL_ERROR` because those represent prisma-next
|
|
1944
|
-
* bugs the user did not cause — surfacing them as `PRECONDITION` would
|
|
1945
|
-
* mislead automation into thinking the caller mis-invoked the CLI.
|
|
1946
|
-
*
|
|
1947
|
-
* See [exit-codes.ts](./exit-codes.ts) for the canonical list and
|
|
1948
|
-
* [Style Guide § Exit Codes](../../../../../../../docs/CLI%20Style%20Guide.md#exit-codes)
|
|
1949
|
-
* for the reservation policy.
|
|
1950
|
-
*
|
|
1951
|
-
* Exported for unit tests so the mapping can be asserted without
|
|
1952
|
-
* round-tripping a full `runInit` invocation.
|
|
1953
|
-
*/
|
|
1954
|
-
function exitCodeForError(error) {
|
|
1955
|
-
switch (error.code) {
|
|
1956
|
-
case "CLI.INIT_REINIT_NEEDS_FORCE":
|
|
1957
|
-
case "CLI.INIT_MISSING_FLAGS":
|
|
1958
|
-
case "CLI.INIT_INVALID_FLAG_VALUE":
|
|
1959
|
-
case "CLI.INIT_STRICT_PROBE_WITHOUT_PROBE":
|
|
1960
|
-
case "CLI.INIT_INVALID_MANIFEST":
|
|
1961
|
-
case "CLI.INIT_INVALID_TSCONFIG":
|
|
1962
|
-
case "CLI.INIT_PROBE_FAILED":
|
|
1963
|
-
case "CLI.INIT_AUTHORING_SCHEMA_PATH_MISMATCH": return 2;
|
|
1964
|
-
case "CLI.INIT_USER_ABORTED": return 3;
|
|
1965
|
-
case "CLI.INIT_INSTALL_FAILED": return 4;
|
|
1966
|
-
case "CLI.INIT_EMIT_FAILED": return 5;
|
|
1967
|
-
case "CLI.INIT_INVALID_OUTPUT_DOCUMENT": return 1;
|
|
1968
|
-
case "CLI.INIT_SKILL_INSTALL_FAILED": return 6;
|
|
1969
|
-
default: return 1;
|
|
1970
|
-
}
|
|
1971
|
-
}
|
|
1972
|
-
/**
|
|
1973
|
-
* Folds a `ProbeOutcome` into init's warning channel and returns the
|
|
1974
|
-
* fatal cause string when `--strict-probe` should escalate. Mirrors
|
|
1975
|
-
* the FR8.3 contract:
|
|
1976
|
-
*
|
|
1977
|
-
* - `ok` — informational; nothing surfaced unless verbose. (We could
|
|
1978
|
-
* plumb a `note` here, but the spec only requires the warning side
|
|
1979
|
-
* of the contract; an "all good" line would just be noise on the
|
|
1980
|
-
* common path.)
|
|
1981
|
-
* - `below-minimum` — warning regardless of `--strict-probe`. The
|
|
1982
|
-
* probe ran successfully and found an old server; that is not a
|
|
1983
|
-
* probe *failure* (which is what `--strict-probe` escalates), it
|
|
1984
|
-
* is the probe doing its job.
|
|
1985
|
-
* - `no-database-url` / `connection-failed` / `driver-missing` —
|
|
1986
|
-
* warning by default, fatal under `--strict-probe`.
|
|
1987
|
-
*
|
|
1988
|
-
* Exported for unit tests so the branching contract can be asserted
|
|
1989
|
-
* without spinning up a full `runInit` round trip.
|
|
1990
|
-
*/
|
|
1991
|
-
function applyProbeOutcome(outcome, ctx) {
|
|
1992
|
-
switch (outcome.kind) {
|
|
1993
|
-
case "ok": return null;
|
|
1994
|
-
case "below-minimum":
|
|
1995
|
-
ctx.warnings.push(outcome.message);
|
|
1996
|
-
return null;
|
|
1997
|
-
case "no-database-url":
|
|
1998
|
-
case "connection-failed":
|
|
1999
|
-
case "driver-missing":
|
|
2000
|
-
if (ctx.strictProbe) return outcome.message;
|
|
2001
|
-
ctx.warnings.push(outcome.message);
|
|
2002
|
-
return null;
|
|
2003
|
-
}
|
|
2004
|
-
}
|
|
2005
|
-
/**
|
|
2006
|
-
* Drives the `pnpm add` / `npm install` step. Failures are escalated to
|
|
2007
|
-
* a structured `errorInitInstallFailed` (exit code 4) — the spec treats
|
|
2008
|
-
* an unrecoverable install as a hard outcome rather than a warning so
|
|
2009
|
-
* CI/agents can branch on the exit code (FR1.6).
|
|
2010
|
-
*
|
|
2011
|
-
* For pnpm specifically, we additionally implement the FR7.2 fallback:
|
|
2012
|
-
* if pnpm fails with a recognised workspace/catalog resolution error
|
|
2013
|
-
* class (typically caused by a registry version that leaked
|
|
2014
|
-
* `workspace:*` or `catalog:` specifiers), we retry the install using
|
|
2015
|
-
* `npm` and surface a non-fatal warning explaining the swap.
|
|
2016
|
-
*/
|
|
2017
|
-
async function runInstall(ctx) {
|
|
2018
|
-
const { baseDir, pm, target, install, flags, ui, filesWritten, hasTypesNode } = ctx;
|
|
2019
|
-
const deps = [targetPackageName(target, ctx.resolveImportSpecifier), "dotenv"];
|
|
2020
|
-
const devDeps = hasTypesNode ? ["prisma-next"] : ["prisma-next", "@types/node"];
|
|
2021
|
-
const addCommand = `${pm} ${formatAddArgs(pm, deps).join(" ")}`;
|
|
2022
|
-
const addDevCommand = `${pm} ${formatAddDevArgs(pm, devDeps).join(" ")}`;
|
|
2023
|
-
const emitCommand = formatRunCommand(pm, "prisma-next", "contract emit");
|
|
2024
|
-
const catalogWarnings = pm === "pnpm" ? buildCatalogWarnings(baseDir, [...deps, ...devDeps]) : [];
|
|
2025
|
-
if (!install) {
|
|
2026
|
-
if (!flags.json && !flags.quiet) ui.note([
|
|
2027
|
-
"Run the following commands to complete setup:",
|
|
2028
|
-
"",
|
|
2029
|
-
" 1. Install dependencies:",
|
|
2030
|
-
` ${addCommand}`,
|
|
2031
|
-
` ${addDevCommand}`,
|
|
2032
|
-
"",
|
|
2033
|
-
" 2. Emit the contract:",
|
|
2034
|
-
` ${emitCommand}`
|
|
2035
|
-
].join("\n"), "Manual steps");
|
|
2036
|
-
return {
|
|
2037
|
-
skipped: true,
|
|
2038
|
-
deps: [],
|
|
2039
|
-
devDeps: [],
|
|
2040
|
-
warnings: catalogWarnings,
|
|
2041
|
-
effectivePm: pm
|
|
2042
|
-
};
|
|
2043
|
-
}
|
|
2044
|
-
const exec = promisify(execFile);
|
|
2045
|
-
const runPair = async (manager) => {
|
|
2046
|
-
await exec(manager, formatAddArgs(manager, deps), { cwd: baseDir });
|
|
2047
|
-
await exec(manager, formatAddDevArgs(manager, devDeps), { cwd: baseDir });
|
|
2048
|
-
};
|
|
2049
|
-
const allPackages = [...deps, ...devDeps].join(", ");
|
|
2050
|
-
const spinner = ui.spinner();
|
|
2051
|
-
spinner.start(`Installing ${allPackages}...`);
|
|
2052
|
-
try {
|
|
2053
|
-
await runPair(pm);
|
|
2054
|
-
spinner.stop(`Installed ${allPackages}`);
|
|
2055
|
-
return {
|
|
2056
|
-
skipped: false,
|
|
2057
|
-
deps,
|
|
2058
|
-
devDeps,
|
|
2059
|
-
warnings: catalogWarnings,
|
|
2060
|
-
effectivePm: pm
|
|
2061
|
-
};
|
|
2062
|
-
} catch (err) {
|
|
2063
|
-
const stderrText = redactSecrets(readChildStderr(err));
|
|
2064
|
-
if (pm === "pnpm" && isRecognisedPnpmResolutionError(stderrText)) {
|
|
2065
|
-
spinner.message("pnpm could not resolve a workspace/catalog dependency, retrying with npm...");
|
|
2066
|
-
try {
|
|
2067
|
-
await runPair("npm");
|
|
2068
|
-
spinner.stop(`Installed ${allPackages} via npm (pnpm fallback)`);
|
|
2069
|
-
return {
|
|
2070
|
-
skipped: false,
|
|
2071
|
-
deps,
|
|
2072
|
-
devDeps,
|
|
2073
|
-
warnings: [[
|
|
2074
|
-
"pnpm could not install: a published Prisma Next dependency leaked a `workspace:*` or `catalog:` specifier.",
|
|
2075
|
-
"Falling back to `npm install` so init can complete.",
|
|
2076
|
-
stderrText ? ` pnpm error: ${stderrText.trim().split("\n")[0]}` : "",
|
|
2077
|
-
"Once the offending package republishes a clean version, re-run `pnpm install` to switch back."
|
|
2078
|
-
].filter(Boolean).join("\n")],
|
|
2079
|
-
effectivePm: "npm"
|
|
2080
|
-
};
|
|
2081
|
-
} catch (npmErr) {
|
|
2082
|
-
spinner.stop("Installation failed");
|
|
2083
|
-
throw errorInitInstallFailed({
|
|
2084
|
-
addCommand,
|
|
2085
|
-
addDevCommand,
|
|
2086
|
-
emitCommand,
|
|
2087
|
-
filesWritten,
|
|
2088
|
-
stderrLines: [stderrText, redactSecrets(readChildStderr(npmErr))]
|
|
2089
|
-
});
|
|
2090
|
-
}
|
|
2091
|
-
}
|
|
2092
|
-
spinner.stop("Installation failed");
|
|
2093
|
-
throw errorInitInstallFailed({
|
|
2094
|
-
addCommand,
|
|
2095
|
-
addDevCommand,
|
|
2096
|
-
emitCommand,
|
|
2097
|
-
filesWritten,
|
|
2098
|
-
stderrLines: [stderrText]
|
|
2099
|
-
});
|
|
2100
|
-
}
|
|
2101
|
-
}
|
|
2102
|
-
/**
|
|
2103
|
-
* Builds the FR7.3 catalog-honoured warning(s) for the surrounding pnpm
|
|
2104
|
-
* workspace, if any. Returns an empty array when no `pnpm-workspace.yaml`
|
|
2105
|
-
* exists in any ancestor or when the workspace's catalog has no entry
|
|
2106
|
-
* for any of the packages `init` is about to install.
|
|
2107
|
-
*
|
|
2108
|
-
* Exported for unit tests.
|
|
2109
|
-
*/
|
|
2110
|
-
function buildCatalogWarnings(baseDir, packages) {
|
|
2111
|
-
const result = detectPnpmCatalogOverrides(baseDir, packages);
|
|
2112
|
-
if (result === null || result.entries.length === 0) return [];
|
|
2113
|
-
return [formatCatalogWarning(result.workspaceFile, result.entries)];
|
|
2114
|
-
}
|
|
2115
|
-
function formatCatalogWarning(workspaceFile, entries) {
|
|
2116
|
-
return [
|
|
2117
|
-
"pnpm workspace catalog overrides detected — pnpm will install these versions instead of `latest`:",
|
|
2118
|
-
entries.map((entry) => ` • ${entry.name}: ${entry.version}`).join("\n"),
|
|
2119
|
-
`Catalog source: ${workspaceFile}`,
|
|
2120
|
-
"To use the published `latest` instead, remove or update the catalog entry, then re-run `pnpm install`."
|
|
2121
|
-
].join("\n");
|
|
2122
|
-
}
|
|
2123
|
-
/**
|
|
2124
1441
|
* Recognised pnpm error signatures that justify a fallback to npm.
|
|
2125
1442
|
*
|
|
2126
1443
|
* These patterns indicate the published artifact itself is at fault
|
|
@@ -2129,124 +1446,31 @@ function formatCatalogWarning(workspaceFile, entries) {
|
|
|
2129
1446
|
* registry version", and npm is willing to install it because npm
|
|
2130
1447
|
* doesn't care about the protocol prefix when there's a fallback range.
|
|
2131
1448
|
*
|
|
2132
|
-
*
|
|
2133
|
-
* command
|
|
1449
|
+
* The predicate lives on its own so both shells' `init` share one list:
|
|
1450
|
+
* the commander command matches it against captured child stderr, the
|
|
1451
|
+
* engine command against the stderr the package-manager capability
|
|
1452
|
+
* returns on a failed install.
|
|
2134
1453
|
*/
|
|
2135
1454
|
function isRecognisedPnpmResolutionError(stderr) {
|
|
2136
1455
|
if (!stderr) return false;
|
|
2137
1456
|
return stderr.includes("ERR_PNPM_WORKSPACE_PKG_NOT_FOUND") || stderr.includes("ERR_PNPM_NO_MATCHING_VERSION") || /No matching version found for .* in the catalog/i.test(stderr) || /workspace:[^\s]+ is not a valid (version|spec)/i.test(stderr) || /catalog:[^\s]* is not a valid (version|spec)/i.test(stderr);
|
|
2138
1457
|
}
|
|
2139
1458
|
/**
|
|
2140
|
-
*
|
|
2141
|
-
*
|
|
2142
|
-
*
|
|
2143
|
-
*
|
|
2144
|
-
*
|
|
2145
|
-
*
|
|
2146
|
-
* Exported for unit tests.
|
|
2147
|
-
*/
|
|
2148
|
-
function hasDirectDep(parsed, name) {
|
|
2149
|
-
for (const field of ["dependencies", "devDependencies"]) {
|
|
2150
|
-
const value = parsed[field];
|
|
2151
|
-
if (value !== null && typeof value === "object" && name in value) return true;
|
|
2152
|
-
}
|
|
2153
|
-
return false;
|
|
2154
|
-
}
|
|
2155
|
-
function readChildStderr(err) {
|
|
2156
|
-
if (err instanceof Error && "stderr" in err) return String(err.stderr ?? "");
|
|
2157
|
-
return "";
|
|
2158
|
-
}
|
|
2159
|
-
/**
|
|
2160
|
-
* Redacts userinfo (`user:password@`) from any URL-shaped substring inside
|
|
2161
|
-
* package-manager stderr before we surface it in a warning or error
|
|
2162
|
-
* meta. pnpm and npm both include the offending registry URL in resolve
|
|
2163
|
-
* errors, and that URL can carry an auth token (e.g. corporate registry
|
|
2164
|
-
* mirrors that bake `_authToken` into the URL). The Style Guide
|
|
2165
|
-
* (Testing & Accessibility — "Security: never print secrets") requires
|
|
2166
|
-
* we never surface those.
|
|
1459
|
+
* Strips credentials out of package-manager stderr before it reaches a warning,
|
|
1460
|
+
* an error's meta, or a log. Two shapes carry them: userinfo inside a registry
|
|
1461
|
+
* URL (`https://user:token@registry…`), and the npmrc settings npm and pnpm
|
|
1462
|
+
* echo back when authentication fails (`//registry.npmjs.org/:_authToken=…`,
|
|
1463
|
+
* or the top-level `_authToken=…` with no registry scope), which are not
|
|
1464
|
+
* URL-shaped and survive a userinfo-only pass.
|
|
2167
1465
|
*
|
|
2168
|
-
*
|
|
1466
|
+
* Both shells' `init` call this, and both call it on stderr they already
|
|
1467
|
+
* received from somewhere else: redaction is cheap and doubling it is harmless,
|
|
1468
|
+
* while trusting another layer to have done it is not.
|
|
2169
1469
|
*/
|
|
2170
1470
|
function redactSecrets(stderr) {
|
|
2171
|
-
|
|
2172
|
-
return stderr.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^/@\s]+)@/g, "$1***@");
|
|
2173
|
-
}
|
|
2174
|
-
/**
|
|
2175
|
-
* Drives `prisma-next contract emit` against the freshly scaffolded
|
|
2176
|
-
* project. On failure, throws `errorInitEmitFailed` with the underlying
|
|
2177
|
-
* cause embedded in `meta.cause` so the user can re-run with `-v` to see
|
|
2178
|
-
* the full envelope and follow the fix steps. Maps to exit code
|
|
2179
|
-
* `5 = EMIT_FAILED` (FR1.6).
|
|
2180
|
-
*/
|
|
2181
|
-
async function runEmit(ctx) {
|
|
2182
|
-
const spinner = ctx.ui.spinner();
|
|
2183
|
-
spinner.start("Emitting contract...");
|
|
2184
|
-
try {
|
|
2185
|
-
const { executeContractEmit } = await import("./contract-emit-Bzt11N4v-BQ2Pr8KG.mjs");
|
|
2186
|
-
const { loadConfigForSections } = await import("./exports-BZLNbtkY.mjs").then((n) => n.t);
|
|
2187
|
-
const configFilePath = join(ctx.baseDir, "prisma-next.config.ts");
|
|
2188
|
-
const configResult = await loadConfigForSections(configFilePath, [
|
|
2189
|
-
"contract",
|
|
2190
|
-
"family",
|
|
2191
|
-
"target",
|
|
2192
|
-
"adapter",
|
|
2193
|
-
"extensions"
|
|
2194
|
-
]);
|
|
2195
|
-
if (!configResult.ok) throw configResult.failure;
|
|
2196
|
-
await executeContractEmit({
|
|
2197
|
-
config: configResult.value,
|
|
2198
|
-
cwd: process.cwd(),
|
|
2199
|
-
configPath: configFilePath
|
|
2200
|
-
});
|
|
2201
|
-
spinner.stop("Contract emitted");
|
|
2202
|
-
} catch (err) {
|
|
2203
|
-
spinner.stop("Contract emission failed");
|
|
2204
|
-
throw errorInitEmitFailed({
|
|
2205
|
-
emitCommand: ctx.emitCommand,
|
|
2206
|
-
filesWritten: ctx.filesWritten,
|
|
2207
|
-
cause: causeMessage(err)
|
|
2208
|
-
});
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
function causeMessage(err) {
|
|
2212
|
-
if (err instanceof Error) return err.message;
|
|
2213
|
-
return String(err);
|
|
2214
|
-
}
|
|
2215
|
-
/**
|
|
2216
|
-
* Minimal `package.json` content used when init runs in a directory
|
|
2217
|
-
* that has no project manifest (TML-2496). Mirrors the npm 11 `init -y`
|
|
2218
|
-
* defaults, with two deliberate deviations:
|
|
2219
|
-
*
|
|
2220
|
-
* - `"private": true` so a stray `npm publish` cannot leak the
|
|
2221
|
-
* placeholder. Users who want to publish have to opt in by removing
|
|
2222
|
-
* the field.
|
|
2223
|
-
* - `"type": "module"` so the scaffolded ESM imports in
|
|
2224
|
-
* `prisma-next.config.ts` and `db.ts` typecheck and run without
|
|
2225
|
-
* additional tsconfig coercion.
|
|
2226
|
-
*
|
|
2227
|
-
* Exported for unit tests so the canonical shape is asserted in one
|
|
2228
|
-
* place rather than re-derived at every call site.
|
|
2229
|
-
*/
|
|
2230
|
-
function defaultPackageJsonContent(rawName) {
|
|
2231
|
-
return `${JSON.stringify({
|
|
2232
|
-
name: sanitisePackageName(rawName),
|
|
2233
|
-
version: "0.0.0",
|
|
2234
|
-
private: true,
|
|
2235
|
-
type: "module"
|
|
2236
|
-
}, null, 2)}\n`;
|
|
2237
|
-
}
|
|
2238
|
-
/**
|
|
2239
|
-
* npm package names are restricted to lowercase, no leading dot/underscore,
|
|
2240
|
-
* and a small URL-safe character set. `basename(cwd)` happily returns
|
|
2241
|
-
* "My Project" or ".hidden" — both rejected by `npm install` validation.
|
|
2242
|
-
* Coerce to a safe fallback rather than emit a manifest npm refuses to
|
|
2243
|
-
* read.
|
|
2244
|
-
*/
|
|
2245
|
-
function sanitisePackageName(raw) {
|
|
2246
|
-
const trimmed = raw.toLowerCase().replace(/[^a-z0-9._~-]/g, "-").replace(/^[._-]+/, "").replace(/-+/g, "-");
|
|
2247
|
-
return trimmed.length > 0 ? trimmed : "my-app";
|
|
1471
|
+
return stderr.replace(/([a-zA-Z][a-zA-Z0-9+.-]*:\/\/)([^/@\s]+)@/g, "$1***@").replace(/(\b_(?:authToken|auth|password)=)\S+/gi, "$1***");
|
|
2248
1472
|
}
|
|
2249
1473
|
//#endregion
|
|
2250
|
-
export {
|
|
1474
|
+
export { isRecognisedPnpmResolutionError as A, removeDependency as B, findStaleArtifacts as C, formatSkillInstallCommand as D, formatRunCommand as E, mergeTsConfig as F, validateSchemaPath as G, resolveAuthoring as H, minimalProjectReadmeMd as I, probeServerVersion as L, mergeGitattributes as M, mergeGitignore as N, formatSkillSourceUrl as O, mergePackageScripts as P, quickReferenceMd as R, errorInitWriteFailed as S, formatAddDevArgs as T, resolveProjectSkillInstallCommands as U, requiredGitattributesLines as V, resolveTarget as W, errorInitProbeFailed as _, TsConfigParseError as a, errorInitStrictProbeWithoutProbe as b, detectPackageManager as c, envFileContent as d, errorInitEmitFailed as f, errorInitMissingFlags as g, errorInitInvalidTsconfig as h, REQUIRED_SCRIPTS as i, legacySkillDirs as j, hasProjectManifest as k, ensureEsmModuleType as l, errorInitInvalidManifest as m, DEFAULT_SKILL_SOURCES as n, buildCatalogWarnings as o, errorInitInstallFailed as p, MIN_SERVER_VERSION as r, defaultTsConfig as s, DEFAULT_SKILL_AGENTS as t, envExampleContent as u, errorInitReinitNeedsForce as v, formatAddArgs as w, errorInitUserAborted as x, errorInitSkillInstallFailed as y, redactSecrets as z };
|
|
2251
1475
|
|
|
2252
|
-
//# sourceMappingURL=
|
|
1476
|
+
//# sourceMappingURL=redact-secrets-kQ0KKV1u-MnVSKe-u.mjs.map
|