@prisma-next/cli 0.5.0-dev.4 → 0.5.0-dev.40
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/README.md +56 -21
- package/dist/agent-skill-mongo.md +63 -31
- package/dist/agent-skill-postgres.md +1 -1
- package/dist/cli-errors-By1iVE3z.mjs +34 -0
- package/dist/cli-errors-By1iVE3z.mjs.map +1 -0
- package/dist/{cli-errors-C0JhVj0c.d.mts → cli-errors-DDeVsP2Y.d.mts} +1 -0
- package/dist/cli.mjs +123 -15
- package/dist/cli.mjs.map +1 -1
- package/dist/{client-TG7rbCWT.mjs → client-1JqqkiC7.mjs} +45 -20
- package/dist/client-1JqqkiC7.mjs.map +1 -0
- package/dist/commands/contract-emit.d.mts.map +1 -1
- package/dist/commands/contract-emit.mjs +2 -2
- package/dist/commands/contract-infer.d.mts.map +1 -1
- package/dist/commands/contract-infer.mjs +2 -2
- package/dist/commands/db-init.d.mts.map +1 -1
- package/dist/commands/db-init.mjs +10 -9
- package/dist/commands/db-init.mjs.map +1 -1
- package/dist/commands/db-schema.mjs +5 -5
- package/dist/commands/db-sign.mjs +7 -7
- package/dist/commands/db-update.mjs +9 -9
- package/dist/commands/db-update.mjs.map +1 -1
- package/dist/commands/db-verify.mjs +9 -9
- package/dist/commands/migration-apply.d.mts +5 -2
- package/dist/commands/migration-apply.d.mts.map +1 -1
- package/dist/commands/migration-apply.mjs +55 -56
- package/dist/commands/migration-apply.mjs.map +1 -1
- package/dist/commands/migration-new.d.mts.map +1 -1
- package/dist/commands/migration-new.mjs +26 -32
- package/dist/commands/migration-new.mjs.map +1 -1
- package/dist/commands/migration-plan.d.mts +14 -5
- package/dist/commands/migration-plan.d.mts.map +1 -1
- package/dist/commands/migration-plan.mjs +45 -48
- package/dist/commands/migration-plan.mjs.map +1 -1
- package/dist/commands/migration-ref.d.mts +1 -1
- package/dist/commands/migration-ref.d.mts.map +1 -1
- package/dist/commands/migration-ref.mjs +6 -10
- package/dist/commands/migration-ref.mjs.map +1 -1
- package/dist/commands/migration-show.d.mts +13 -7
- package/dist/commands/migration-show.d.mts.map +1 -1
- package/dist/commands/migration-show.mjs +27 -29
- package/dist/commands/migration-show.mjs.map +1 -1
- package/dist/commands/migration-status.d.mts +23 -5
- package/dist/commands/migration-status.d.mts.map +1 -1
- package/dist/commands/migration-status.mjs +3 -3
- package/dist/{config-loader-_W4T21X1.mjs → config-loader-ih8ViDb_.mjs} +2 -2
- package/dist/config-loader-ih8ViDb_.mjs.map +1 -0
- package/dist/config-loader.mjs +1 -1
- package/dist/contract-emit-LjzCoicC.mjs +4 -0
- package/dist/contract-emit-RZBWzkop.mjs +329 -0
- package/dist/contract-emit-RZBWzkop.mjs.map +1 -0
- package/dist/contract-emit-rt_Nmdwq.mjs +150 -0
- package/dist/contract-emit-rt_Nmdwq.mjs.map +1 -0
- package/dist/{contract-enrichment-CGW6mm-E.mjs → contract-enrichment-4Ptgw3Pe.mjs} +1 -1
- package/dist/{contract-enrichment-CGW6mm-E.mjs.map → contract-enrichment-4Ptgw3Pe.mjs.map} +1 -1
- package/dist/{contract-infer-BS4kIX9c.mjs → contract-infer-Cf5J2wVg.mjs} +11 -19
- package/dist/contract-infer-Cf5J2wVg.mjs.map +1 -0
- package/dist/exports/control-api.d.mts +86 -21
- package/dist/exports/control-api.d.mts.map +1 -1
- package/dist/exports/control-api.mjs +5 -5
- package/dist/exports/index.mjs +3 -3
- package/dist/exports/init-output.d.mts +39 -0
- package/dist/exports/init-output.d.mts.map +1 -0
- package/dist/exports/init-output.mjs +3 -0
- package/dist/{framework-components-DfZKQBQ2.mjs → framework-components-Bgcre3Z6.mjs} +2 -2
- package/dist/{framework-components-DfZKQBQ2.mjs.map → framework-components-Bgcre3Z6.mjs.map} +1 -1
- package/dist/init-C7dE9KOJ.mjs +2062 -0
- package/dist/init-C7dE9KOJ.mjs.map +1 -0
- package/dist/{inspect-live-schema-BsoFVoS1.mjs → inspect-live-schema-LWtXfxm_.mjs} +9 -9
- package/dist/inspect-live-schema-LWtXfxm_.mjs.map +1 -0
- package/dist/migration-cli.d.mts +41 -11
- package/dist/migration-cli.d.mts.map +1 -1
- package/dist/migration-cli.mjs +308 -84
- package/dist/migration-cli.mjs.map +1 -1
- package/dist/{migration-command-scaffold-DOXnheFa.mjs → migration-command-scaffold-CU452v9h.mjs} +7 -7
- package/dist/{migration-command-scaffold-DOXnheFa.mjs.map → migration-command-scaffold-CU452v9h.mjs.map} +1 -1
- package/dist/{migration-status-Ry3TnEya.mjs → migration-status-DoPrFIOQ.mjs} +114 -57
- package/dist/migration-status-DoPrFIOQ.mjs.map +1 -0
- package/dist/{migrations-fU0xoKjS.mjs → migrations-MEoKMiV5.mjs} +42 -21
- package/dist/migrations-MEoKMiV5.mjs.map +1 -0
- package/dist/output-BpcQrnnq.mjs +103 -0
- package/dist/output-BpcQrnnq.mjs.map +1 -0
- package/dist/{progress-adapter-B-YvmcDu.mjs → progress-adapter-DgRGldpT.mjs} +1 -1
- package/dist/{progress-adapter-B-YvmcDu.mjs.map → progress-adapter-DgRGldpT.mjs.map} +1 -1
- package/dist/quick-reference-mongo.md +34 -13
- package/dist/quick-reference-postgres.md +11 -9
- package/dist/{result-handler-BJwA7ufw.mjs → result-handler-Ch6hVnOo.mjs} +35 -93
- package/dist/result-handler-Ch6hVnOo.mjs.map +1 -0
- package/dist/{terminal-ui-C5k88MmW.mjs → terminal-ui-u2YgKghu.mjs} +76 -2
- package/dist/terminal-ui-u2YgKghu.mjs.map +1 -0
- package/dist/{verify-bl__PkXk.mjs → verify-BT9tgCOH.mjs} +2 -2
- package/dist/{verify-bl__PkXk.mjs.map → verify-BT9tgCOH.mjs.map} +1 -1
- package/package.json +23 -17
- package/src/cli.ts +32 -6
- package/src/commands/contract-emit.ts +67 -163
- package/src/commands/contract-infer.ts +7 -20
- package/src/commands/db-init.ts +1 -0
- package/src/commands/db-update.ts +1 -1
- package/src/commands/init/detect-pnpm-catalog.ts +141 -0
- package/src/commands/init/errors.ts +254 -0
- package/src/commands/init/exit-codes.ts +62 -0
- package/src/commands/init/hygiene-gitattributes.ts +97 -0
- package/src/commands/init/hygiene-gitignore.ts +48 -0
- package/src/commands/init/hygiene-package-scripts.ts +91 -0
- package/src/commands/init/index.ts +112 -7
- package/src/commands/init/init.ts +766 -144
- package/src/commands/init/inputs.ts +421 -0
- package/src/commands/init/output.ts +147 -0
- package/src/commands/init/probe-db.ts +308 -0
- package/src/commands/init/reinit-cleanup.ts +83 -0
- package/src/commands/init/templates/agent-skill-mongo.md +63 -31
- package/src/commands/init/templates/agent-skill-postgres.md +1 -1
- package/src/commands/init/templates/agent-skill.ts +25 -3
- package/src/commands/init/templates/code-templates.ts +125 -32
- package/src/commands/init/templates/env.ts +80 -0
- package/src/commands/init/templates/quick-reference-mongo.md +34 -13
- package/src/commands/init/templates/quick-reference-postgres.md +11 -9
- package/src/commands/init/templates/quick-reference.ts +42 -3
- package/src/commands/init/templates/tsconfig.ts +167 -5
- package/src/commands/inspect-live-schema.ts +10 -5
- package/src/commands/migration-apply.ts +84 -63
- package/src/commands/migration-new.ts +28 -34
- package/src/commands/migration-plan.ts +80 -56
- package/src/commands/migration-ref.ts +8 -7
- package/src/commands/migration-show.ts +53 -36
- package/src/commands/migration-status.ts +194 -58
- package/src/config-path-validation.ts +0 -1
- package/src/control-api/client.ts +21 -0
- package/src/control-api/operations/contract-emit.ts +198 -115
- package/src/control-api/operations/db-init.ts +10 -6
- package/src/control-api/operations/db-update.ts +10 -6
- package/src/control-api/operations/migration-apply.ts +30 -9
- package/src/control-api/types.ts +69 -7
- package/src/exports/control-api.ts +2 -1
- package/src/exports/init-output.ts +10 -0
- package/src/migration-cli.ts +445 -122
- package/src/utils/cli-errors.ts +49 -2
- package/src/utils/command-helpers.ts +45 -23
- package/src/utils/emit-queue.ts +26 -0
- package/src/utils/formatters/graph-migration-mapper.ts +7 -3
- package/src/utils/formatters/migrations.ts +62 -26
- package/src/utils/publish-contract-artifact-pair.ts +134 -0
- package/dist/cli-errors-DHq6GQGu.mjs +0 -5
- package/dist/client-TG7rbCWT.mjs.map +0 -1
- package/dist/config-loader-_W4T21X1.mjs.map +0 -1
- package/dist/contract-emit-CQfj7xJn.mjs +0 -122
- package/dist/contract-emit-CQfj7xJn.mjs.map +0 -1
- package/dist/contract-emit-DpPjuFy-.mjs +0 -195
- package/dist/contract-emit-DpPjuFy-.mjs.map +0 -1
- package/dist/contract-emit-fhNwwhkQ.mjs +0 -4
- package/dist/contract-infer-BS4kIX9c.mjs.map +0 -1
- package/dist/extract-operation-statements-DZUJNmL3.mjs +0 -13
- package/dist/extract-operation-statements-DZUJNmL3.mjs.map +0 -1
- package/dist/extract-sql-ddl-DDMX-9mz.mjs +0 -26
- package/dist/extract-sql-ddl-DDMX-9mz.mjs.map +0 -1
- package/dist/init-CQfo_4Ro.mjs +0 -430
- package/dist/init-CQfo_4Ro.mjs.map +0 -1
- package/dist/inspect-live-schema-BsoFVoS1.mjs.map +0 -1
- package/dist/migration-status-Ry3TnEya.mjs.map +0 -1
- package/dist/migrations-fU0xoKjS.mjs.map +0 -1
- package/dist/result-handler-BJwA7ufw.mjs.map +0 -1
- package/dist/terminal-ui-C5k88MmW.mjs.map +0 -1
- package/dist/validate-contract-deps-esa-VQ0h.mjs +0 -37
- package/dist/validate-contract-deps-esa-VQ0h.mjs.map +0 -1
- package/src/control-api/operations/extract-operation-statements.ts +0 -14
- package/src/control-api/operations/extract-sql-ddl.ts +0 -47
|
@@ -112,7 +112,7 @@ async function executeDbUpdateCommand(
|
|
|
112
112
|
label: op.label,
|
|
113
113
|
operationClass: op.operationClass,
|
|
114
114
|
})),
|
|
115
|
-
...ifDefined('
|
|
115
|
+
...ifDefined('preview', result.value.plan.preview),
|
|
116
116
|
},
|
|
117
117
|
...ifDefined(
|
|
118
118
|
'execution',
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'pathe';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Catalog entry detected in a `pnpm-workspace.yaml` that overrides one of
|
|
6
|
+
* the packages `init` installs. The `version` is the raw value as written
|
|
7
|
+
* in the workspace file (no normalisation), so the warning surfaces the
|
|
8
|
+
* exact text the user can search for if they want to find the override.
|
|
9
|
+
*/
|
|
10
|
+
export interface PnpmCatalogOverride {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly version: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Result of scanning for a pnpm workspace catalog that overrides any of
|
|
17
|
+
* the packages `init` is about to install. Returns `null` when no
|
|
18
|
+
* `pnpm-workspace.yaml` is found in `baseDir` or any ancestor; an empty
|
|
19
|
+
* `entries` array means a workspace exists but contains none of our
|
|
20
|
+
* packages.
|
|
21
|
+
*/
|
|
22
|
+
export interface PnpmCatalogScanResult {
|
|
23
|
+
/** Absolute path of the `pnpm-workspace.yaml` that was consulted. */
|
|
24
|
+
readonly workspaceFile: string;
|
|
25
|
+
readonly entries: readonly PnpmCatalogOverride[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Walks up from `baseDir` looking for `pnpm-workspace.yaml`, then scans
|
|
30
|
+
* its top-level `catalog:` block for entries that match any of `packages`.
|
|
31
|
+
*
|
|
32
|
+
* Implements FR7.3 / Spec Decision 8 (honour-and-warn): when `init` runs
|
|
33
|
+
* inside a pnpm workspace whose catalog overrides one of the packages it
|
|
34
|
+
* installs, surface a structured warning so the user knows the catalog
|
|
35
|
+
* version (not the published `latest`) is what ended up in their
|
|
36
|
+
* `node_modules`. pnpm itself does this silently; the warning closes the
|
|
37
|
+
* "looks fine, must be wrong version six months later" gap.
|
|
38
|
+
*
|
|
39
|
+
* Notes / scope:
|
|
40
|
+
*
|
|
41
|
+
* - We only inspect the unnamed top-level `catalog:` block. pnpm also
|
|
42
|
+
* supports `catalogs:` (plural — *named* catalogs referenced via
|
|
43
|
+
* `catalog:foo` specifiers); those don't apply to a vanilla
|
|
44
|
+
* `pnpm add prisma-next` invocation, so we skip them.
|
|
45
|
+
* - We don't validate YAML syntax exhaustively. The file format pnpm
|
|
46
|
+
* ships is line-oriented and well-known; a minimal regex is more
|
|
47
|
+
* robust than depending on a YAML parser for one warning.
|
|
48
|
+
* - We don't compare against the registry's `latest` — pnpm uses the
|
|
49
|
+
* catalog version regardless, so the warning fires whenever a match
|
|
50
|
+
* exists. The user-facing copy explains how to opt out.
|
|
51
|
+
*/
|
|
52
|
+
export function detectPnpmCatalogOverrides(
|
|
53
|
+
baseDir: string,
|
|
54
|
+
packages: readonly string[],
|
|
55
|
+
): PnpmCatalogScanResult | null {
|
|
56
|
+
const workspaceFile = findNearestPnpmWorkspaceFile(baseDir);
|
|
57
|
+
if (workspaceFile === null) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const contents = readFileSync(workspaceFile, 'utf-8');
|
|
62
|
+
const catalog = extractCatalogBlock(contents);
|
|
63
|
+
if (catalog === null) {
|
|
64
|
+
return { workspaceFile, entries: [] };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const wanted = new Set(packages);
|
|
68
|
+
const entries: PnpmCatalogOverride[] = [];
|
|
69
|
+
for (const [name, version] of catalog) {
|
|
70
|
+
if (wanted.has(name)) {
|
|
71
|
+
entries.push({ name, version });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { workspaceFile, entries };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function findNearestPnpmWorkspaceFile(baseDir: string): string | null {
|
|
78
|
+
let dir = baseDir;
|
|
79
|
+
let prev = '';
|
|
80
|
+
while (dir !== prev) {
|
|
81
|
+
const candidate = join(dir, 'pnpm-workspace.yaml');
|
|
82
|
+
if (existsSync(candidate)) {
|
|
83
|
+
return candidate;
|
|
84
|
+
}
|
|
85
|
+
prev = dir;
|
|
86
|
+
dir = dirname(dir);
|
|
87
|
+
}
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Returns the entries inside the top-level `catalog:` block as `[name, version]`
|
|
93
|
+
* pairs in document order, or `null` when no `catalog:` block exists.
|
|
94
|
+
*
|
|
95
|
+
* The parser is intentionally minimal: it reads line-by-line, locates the
|
|
96
|
+
* top-level `catalog:` line (no leading whitespace), then collects every
|
|
97
|
+
* subsequent indented line of the form `<key>: <value>` until the next
|
|
98
|
+
* top-level key (or end of file). Quotes around `<key>` and `<value>`
|
|
99
|
+
* are stripped; comments (`#…`) are ignored.
|
|
100
|
+
*/
|
|
101
|
+
function extractCatalogBlock(contents: string): Array<[string, string]> | null {
|
|
102
|
+
const lines = contents.split(/\r?\n/);
|
|
103
|
+
const startIdx = lines.findIndex((line) => /^catalog\s*:\s*$/.test(line));
|
|
104
|
+
if (startIdx === -1) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const entries: Array<[string, string]> = [];
|
|
109
|
+
for (let i = startIdx + 1; i < lines.length; i++) {
|
|
110
|
+
const raw = lines[i] ?? '';
|
|
111
|
+
if (raw.trim() === '' || /^\s*#/.test(raw)) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (!/^\s/.test(raw)) {
|
|
115
|
+
// Hit the next top-level key — catalog block ended.
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
const match = raw.match(/^\s+(?:'([^']+)'|"([^"]+)"|([^:\s'"]+))\s*:\s*(.*?)\s*(?:#.*)?$/);
|
|
119
|
+
if (!match) {
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const name = match[1] ?? match[2] ?? match[3];
|
|
123
|
+
if (name === undefined) continue;
|
|
124
|
+
const rawValue = match[4] ?? '';
|
|
125
|
+
const version = stripQuotes(rawValue.trim());
|
|
126
|
+
if (version === '') continue;
|
|
127
|
+
entries.push([name, version]);
|
|
128
|
+
}
|
|
129
|
+
return entries;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function stripQuotes(value: string): string {
|
|
133
|
+
if (value.length >= 2) {
|
|
134
|
+
const first = value[0];
|
|
135
|
+
const last = value[value.length - 1];
|
|
136
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
137
|
+
return value.slice(1, -1);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return value;
|
|
141
|
+
}
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { CliStructuredError } from '../../utils/cli-errors';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* No `package.json` / `deno.json` / `deno.jsonc` in the target directory.
|
|
5
|
+
*
|
|
6
|
+
* `init` cannot bootstrap a fresh project from a bare directory (NG1) — that
|
|
7
|
+
* gap is tracked separately. The fix is for the user to run `npm init` (or
|
|
8
|
+
* the equivalent for their package manager) first.
|
|
9
|
+
*/
|
|
10
|
+
export function errorInitMissingManifest(): CliStructuredError {
|
|
11
|
+
return new CliStructuredError('5001', 'No project manifest found', {
|
|
12
|
+
domain: 'CLI',
|
|
13
|
+
why: 'No package.json or deno.json found in the target directory. `prisma-next init` requires an existing project to attach to.',
|
|
14
|
+
fix: 'Initialize your project first (e.g. `npm init -y` or `deno init`), then re-run `prisma-next init`.',
|
|
15
|
+
docsUrl: 'https://prisma-next.dev/docs/cli/init',
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Re-init in non-interactive mode without `--force`. Distinct from the
|
|
21
|
+
* decline-the-prompt path (which is `errorInitUserAborted`) because here
|
|
22
|
+
* the user was never given the choice — `--force` is the contract.
|
|
23
|
+
*/
|
|
24
|
+
export function errorInitReinitNeedsForce(): CliStructuredError {
|
|
25
|
+
return new CliStructuredError('5002', 'Project is already initialized', {
|
|
26
|
+
domain: 'CLI',
|
|
27
|
+
why: 'A `prisma-next.config.ts` already exists in this directory. Re-running `init` would overwrite the scaffolded files; in non-interactive mode `init` will not do that without `--force`.',
|
|
28
|
+
fix: 'Pass `--force` to overwrite the existing scaffold, or run `init` interactively to confirm.',
|
|
29
|
+
docsUrl: 'https://prisma-next.dev/docs/cli/init',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Non-interactive mode is missing one or more required inputs. Lists every
|
|
35
|
+
* missing flag in the error so an agent / CI script can react without
|
|
36
|
+
* needing to parse English.
|
|
37
|
+
*
|
|
38
|
+
* @param missing — kebab-case flag names without leading dashes
|
|
39
|
+
* @param why — additional context (e.g. "stdin is not a TTY") that helps
|
|
40
|
+
* the user understand why interactive fallback was skipped.
|
|
41
|
+
*/
|
|
42
|
+
export function errorInitMissingFlags(options: {
|
|
43
|
+
readonly missing: readonly string[];
|
|
44
|
+
readonly why: string;
|
|
45
|
+
}): CliStructuredError {
|
|
46
|
+
const flagList = options.missing.map((flag) => `--${flag}`).join(', ');
|
|
47
|
+
const fixList = options.missing
|
|
48
|
+
.map((flag) => {
|
|
49
|
+
switch (flag) {
|
|
50
|
+
case 'target':
|
|
51
|
+
return '--target postgres|mongodb';
|
|
52
|
+
case 'authoring':
|
|
53
|
+
return '--authoring psl|typescript';
|
|
54
|
+
case 'schema-path':
|
|
55
|
+
return '--schema-path <path>';
|
|
56
|
+
default:
|
|
57
|
+
return `--${flag} <value>`;
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
.join(' ');
|
|
61
|
+
return new CliStructuredError('5003', 'Missing required flags', {
|
|
62
|
+
domain: 'CLI',
|
|
63
|
+
why: `${options.why} Missing required flag(s): ${flagList}.`,
|
|
64
|
+
fix: `Re-run with the missing flag(s) supplied, e.g. \`prisma-next init --yes ${fixList}\`. Use \`prisma-next init --help\` to see every flag.`,
|
|
65
|
+
docsUrl: 'https://prisma-next.dev/docs/cli/init',
|
|
66
|
+
meta: { missingFlags: options.missing },
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A flag value was supplied but is not in the allowed set. Lists the
|
|
72
|
+
* allowed values in `meta` for machine-readable consumption.
|
|
73
|
+
*/
|
|
74
|
+
export function errorInitInvalidFlagValue(options: {
|
|
75
|
+
readonly flag: string;
|
|
76
|
+
readonly value: string;
|
|
77
|
+
readonly allowed: readonly string[];
|
|
78
|
+
}): CliStructuredError {
|
|
79
|
+
return new CliStructuredError('5004', `Invalid value for --${options.flag}`, {
|
|
80
|
+
domain: 'CLI',
|
|
81
|
+
why: `\`--${options.flag} ${options.value}\` is not one of: ${options.allowed.join(', ')}.`,
|
|
82
|
+
fix: `Use one of: ${options.allowed.map((v) => `--${options.flag} ${v}`).join(', ')}.`,
|
|
83
|
+
docsUrl: 'https://prisma-next.dev/docs/cli/init',
|
|
84
|
+
meta: { flag: options.flag, value: options.value, allowed: options.allowed },
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The user cancelled an interactive prompt (Ctrl-C, escape, declined a
|
|
90
|
+
* selection). Distinct from `errorInitReinitNeedsForce` because that path
|
|
91
|
+
* applies to non-interactive mode where the user was never given the
|
|
92
|
+
* choice; this one is the generic "user said no" path. Maps to exit code
|
|
93
|
+
* 3 (USER_ABORTED).
|
|
94
|
+
*/
|
|
95
|
+
export function errorInitUserAborted(): CliStructuredError {
|
|
96
|
+
return new CliStructuredError('5006', 'Init cancelled', {
|
|
97
|
+
domain: 'CLI',
|
|
98
|
+
why: 'The interactive prompt was cancelled before all required inputs were supplied. No files were modified.',
|
|
99
|
+
fix: 'Re-run `prisma-next init` and complete the prompts, or pass the required inputs as flags (see `--help`) for a non-interactive run.',
|
|
100
|
+
severity: 'info',
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* `--strict-probe` was supplied without `--probe-db`. Per FR8.3 / NFR9
|
|
106
|
+
* (offline-by-default), `--strict-probe` is a no-op without `--probe-db` —
|
|
107
|
+
* but rather than silently ignoring it we tell the user what they probably
|
|
108
|
+
* meant. Without this guard, the flag combination silently does nothing,
|
|
109
|
+
* which is exactly the kind of "looks like it worked" trap that a strict
|
|
110
|
+
* mode is supposed to prevent.
|
|
111
|
+
*/
|
|
112
|
+
export function errorInitStrictProbeWithoutProbe(): CliStructuredError {
|
|
113
|
+
return new CliStructuredError('5005', '`--strict-probe` requires `--probe-db`', {
|
|
114
|
+
domain: 'CLI',
|
|
115
|
+
why: '`--strict-probe` only changes how a *failed* probe is reported; without `--probe-db` no probe is attempted in the first place. (`init` is offline-by-default — it never opens a connection to your database without explicit consent.)',
|
|
116
|
+
fix: 'Add `--probe-db` to opt in to the probe, or drop `--strict-probe` if you do not need the version check.',
|
|
117
|
+
docsUrl: 'https://prisma-next.dev/docs/cli/init',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Dependency installation failed and the pnpm → npm fallback (FR7.2)
|
|
123
|
+
* either did not apply (pm ≠ pnpm or stderr did not match a recognised
|
|
124
|
+
* leak) or also failed. Files scaffolded before the install step are
|
|
125
|
+
* already on disk; `meta.filesWritten` carries the list so a follow-up
|
|
126
|
+
* agent can resume manually. Maps to exit code `4 = INSTALL_FAILED`.
|
|
127
|
+
*/
|
|
128
|
+
export function errorInitInstallFailed(options: {
|
|
129
|
+
readonly addCommand: string;
|
|
130
|
+
readonly addDevCommand: string;
|
|
131
|
+
readonly emitCommand: string;
|
|
132
|
+
readonly filesWritten: readonly string[];
|
|
133
|
+
readonly stderrLines: readonly string[];
|
|
134
|
+
}): CliStructuredError {
|
|
135
|
+
const trimmed = options.stderrLines.map((s) => s.trim()).filter(Boolean);
|
|
136
|
+
const why =
|
|
137
|
+
trimmed.length === 0
|
|
138
|
+
? 'The package manager exited with an error and no recoverable fallback applied.'
|
|
139
|
+
: `The package manager exited with: ${trimmed[0]}`;
|
|
140
|
+
return new CliStructuredError('5007', 'Failed to install dependencies', {
|
|
141
|
+
domain: 'CLI',
|
|
142
|
+
why,
|
|
143
|
+
fix: `Install manually:\n ${options.addCommand}\n ${options.addDevCommand}\nThen run \`${options.emitCommand}\` to emit the contract.`,
|
|
144
|
+
docsUrl: 'https://prisma-next.dev/docs/cli/init',
|
|
145
|
+
meta: {
|
|
146
|
+
filesWritten: options.filesWritten,
|
|
147
|
+
stderr: trimmed,
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The user's project manifest (typically `package.json`) failed to parse
|
|
154
|
+
* as JSON. Init reads the manifest to merge `scripts` (FR3.5) and to
|
|
155
|
+
* skip `@types/node` when it is already declared (FR2.1); a malformed
|
|
156
|
+
* file would otherwise surface as an `INTERNAL_ERROR` with a raw
|
|
157
|
+
* `SyntaxError` stack, which violates the FR1.6 contract that every
|
|
158
|
+
* documented failure mode maps to a stable exit code.
|
|
159
|
+
*
|
|
160
|
+
* Maps to exit code `2 = PRECONDITION` — the user can fix the manifest
|
|
161
|
+
* and re-run.
|
|
162
|
+
*/
|
|
163
|
+
export function errorInitInvalidManifest(options: {
|
|
164
|
+
readonly path: string;
|
|
165
|
+
readonly cause: string;
|
|
166
|
+
}): CliStructuredError {
|
|
167
|
+
return new CliStructuredError('5010', `Failed to parse ${options.path}`, {
|
|
168
|
+
domain: 'CLI',
|
|
169
|
+
why: `\`${options.path}\` is not valid JSON: ${options.cause}`,
|
|
170
|
+
fix: `Fix the JSON syntax in \`${options.path}\` (a missing comma or unbalanced brace is the most common cause), then re-run \`prisma-next init\`.`,
|
|
171
|
+
docsUrl: 'https://prisma-next.dev/docs/cli/init',
|
|
172
|
+
meta: { path: options.path, cause: options.cause },
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* The user's existing `tsconfig.json` could not be parsed even with JSONC
|
|
178
|
+
* tolerance (comments + trailing commas) enabled. Init merges the
|
|
179
|
+
* minimum compiler options the scaffolded files need (FR2.2), so an
|
|
180
|
+
* unparseable tsconfig is a hard precondition failure: we cannot
|
|
181
|
+
* faithfully edit a file we cannot read.
|
|
182
|
+
*
|
|
183
|
+
* Init must surface this **before** writing any scaffold file so the
|
|
184
|
+
* user's working tree stays byte-identical (FR6.2 / NFR3) — see
|
|
185
|
+
* `runInit` for the precondition gate.
|
|
186
|
+
*
|
|
187
|
+
* Maps to exit code `2 = PRECONDITION` — the user can fix the file and
|
|
188
|
+
* re-run.
|
|
189
|
+
*/
|
|
190
|
+
export function errorInitInvalidTsconfig(options: {
|
|
191
|
+
readonly path: string;
|
|
192
|
+
readonly cause: string;
|
|
193
|
+
}): CliStructuredError {
|
|
194
|
+
return new CliStructuredError('5011', `Failed to parse ${options.path}`, {
|
|
195
|
+
domain: 'CLI',
|
|
196
|
+
why: `\`${options.path}\` is not valid JSON or JSONC: ${options.cause}`,
|
|
197
|
+
fix: `Fix the syntax in \`${options.path}\` and re-run \`prisma-next init\`. \`init\` accepts JSONC (comments and trailing commas) but cannot recover from unbalanced braces or missing commas.`,
|
|
198
|
+
docsUrl: 'https://prisma-next.dev/docs/cli/init',
|
|
199
|
+
meta: { path: options.path, cause: options.cause },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* `--probe-db` was supplied along with `--strict-probe` and the probe
|
|
205
|
+
* could not complete (no `DATABASE_URL`, network/auth error, the target
|
|
206
|
+
* driver was not installed, …). Without `--strict-probe` the probe
|
|
207
|
+
* surfaces these as warnings; `--strict-probe` escalates them to
|
|
208
|
+
* fatal so a CI gate can rely on "init exit code 2 means something
|
|
209
|
+
* about the runtime environment is wrong" (FR8.3).
|
|
210
|
+
*
|
|
211
|
+
* Maps to exit code `2 = PRECONDITION`. The caller's project files
|
|
212
|
+
* are already on disk by this point — the probe runs after the write
|
|
213
|
+
* phase — but the install/emit steps may or may not have completed
|
|
214
|
+
* depending on `--no-install` and the exact failure mode; `meta`
|
|
215
|
+
* carries `filesWritten` so a follow-up agent can resume manually.
|
|
216
|
+
*/
|
|
217
|
+
export function errorInitProbeFailed(options: {
|
|
218
|
+
readonly cause: string;
|
|
219
|
+
readonly filesWritten: readonly string[];
|
|
220
|
+
}): CliStructuredError {
|
|
221
|
+
return new CliStructuredError('5012', 'Database probe failed', {
|
|
222
|
+
domain: 'CLI',
|
|
223
|
+
why: `\`--probe-db\` could not complete and \`--strict-probe\` was set: ${options.cause}`,
|
|
224
|
+
fix: 'Confirm `DATABASE_URL` points at a reachable server, or drop `--strict-probe` to treat probe failures as warnings.',
|
|
225
|
+
docsUrl: 'https://prisma-next.dev/docs/cli/init',
|
|
226
|
+
meta: {
|
|
227
|
+
filesWritten: options.filesWritten,
|
|
228
|
+
cause: options.cause,
|
|
229
|
+
},
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* `prisma-next contract emit` failed after a successful install. Surface
|
|
235
|
+
* the underlying error so the user can fix it and re-run; files and
|
|
236
|
+
* dependencies remain on disk untouched. Maps to exit code
|
|
237
|
+
* `5 = EMIT_FAILED`.
|
|
238
|
+
*/
|
|
239
|
+
export function errorInitEmitFailed(options: {
|
|
240
|
+
readonly emitCommand: string;
|
|
241
|
+
readonly filesWritten: readonly string[];
|
|
242
|
+
readonly cause: string;
|
|
243
|
+
}): CliStructuredError {
|
|
244
|
+
return new CliStructuredError('5008', 'Failed to emit contract', {
|
|
245
|
+
domain: 'CLI',
|
|
246
|
+
why: `\`prisma-next contract emit\` failed: ${options.cause}`,
|
|
247
|
+
fix: `Inspect your contract file, fix the underlying issue, then re-run \`${options.emitCommand}\`. Pass \`-v\` for the full error envelope.`,
|
|
248
|
+
docsUrl: 'https://prisma-next.dev/docs/cli/contract-emit',
|
|
249
|
+
meta: {
|
|
250
|
+
filesWritten: options.filesWritten,
|
|
251
|
+
cause: options.cause,
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable exit codes for the `init` command.
|
|
3
|
+
*
|
|
4
|
+
* These are part of the command's public contract. AI agents and CI scripts
|
|
5
|
+
* branch on them (FR1.6), so the values must remain stable across versions.
|
|
6
|
+
*
|
|
7
|
+
* Codes 0–3 are the CLI-wide reserved values per the [CLI Style Guide
|
|
8
|
+
* Exit Codes section](../../../../../../../docs/CLI%20Style%20Guide.md#exit-codes):
|
|
9
|
+
* `OK = 0`, `INTERNAL_ERROR = 1`, `PRECONDITION = 2`, `USER_ABORTED = 3`.
|
|
10
|
+
* Codes 4 and 5 are command-specific outcomes for `init`'s two fallible
|
|
11
|
+
* side effects (install + emit). Documented in `--help` via
|
|
12
|
+
* `setCommandDescriptions` in `./index.ts`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
export const INIT_EXIT_OK = 0;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Anything we did not anticipate — a bug in prisma-next, not something
|
|
19
|
+
* the caller did wrong. Includes the structured error code `5009`
|
|
20
|
+
* (invalid output document) and any unrecognised internal error code,
|
|
21
|
+
* so callers can distinguish "tool is broken" from "your invocation
|
|
22
|
+
* was wrong" (`PRECONDITION = 2`). Maps to the generic "RUN" error
|
|
23
|
+
* domain.
|
|
24
|
+
*/
|
|
25
|
+
export const INIT_EXIT_INTERNAL_ERROR = 1;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Preconditions not met. The caller asked for something we cannot do
|
|
29
|
+
* without more input or a different environment. Examples:
|
|
30
|
+
* - missing `package.json` / `deno.json`
|
|
31
|
+
* - non-interactive mode without enough flags to proceed
|
|
32
|
+
* - re-init without `--force` in non-interactive mode
|
|
33
|
+
*/
|
|
34
|
+
export const INIT_EXIT_PRECONDITION = 2;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* The user actively aborted an interactive prompt (Ctrl-C, declined the
|
|
38
|
+
* re-init confirmation, etc.). Distinct from PRECONDITION because the user
|
|
39
|
+
* was given the choice and made it; no diagnostic is needed.
|
|
40
|
+
*/
|
|
41
|
+
export const INIT_EXIT_USER_ABORTED = 3;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Dependency installation step failed without a recoverable fallback.
|
|
45
|
+
* `init` automatically falls back from `pnpm` to `npm` on a recognised
|
|
46
|
+
* workspace/catalog leak (FR7.2); this code is returned only when the
|
|
47
|
+
* fallback also fails, or when the package manager is not pnpm and the
|
|
48
|
+
* single attempt failed. Files written before the install step (config,
|
|
49
|
+
* schema, db client, etc.) remain on disk so the user can fix the
|
|
50
|
+
* environment and re-run; the error envelope's `meta.filesWritten` lists
|
|
51
|
+
* them.
|
|
52
|
+
*/
|
|
53
|
+
export const INIT_EXIT_INSTALL_FAILED = 4;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Contract emit step failed after a successful install. Files written
|
|
57
|
+
* before emit (including any installed dependencies) are still on disk;
|
|
58
|
+
* the user can fix the underlying issue (typically a contract syntax
|
|
59
|
+
* error or a missing extension pack) and re-run `prisma-next contract
|
|
60
|
+
* emit` manually.
|
|
61
|
+
*/
|
|
62
|
+
export const INIT_EXIT_EMIT_FAILED = 5;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { TargetId } from './templates/code-templates';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The schema-relative `.gitattributes` entries written for a freshly
|
|
5
|
+
* initialised project (FR3.4). Mirrors the relevant subset of the
|
|
6
|
+
* repo-root [`.gitattributes`](../../../../../../../../.gitattributes):
|
|
7
|
+
*
|
|
8
|
+
* - **Today**: `contract.json`, `contract.d.ts` are emitted on every
|
|
9
|
+
* `prisma-next contract emit`. Marking them `linguist-generated`
|
|
10
|
+
* keeps GitHub's diff stats honest and collapses the file in code
|
|
11
|
+
* review by default.
|
|
12
|
+
* - **Forward-looking**: `end-contract.*`, `start-contract.*`, `ops.json`,
|
|
13
|
+
* `migration.json` are not yet emitted by `init` flows but will be
|
|
14
|
+
* produced by adjacent commands (lower / migration tooling). Adding
|
|
15
|
+
* them now matches Decision 5 (forward-looking subset) so the file
|
|
16
|
+
* does not need to be amended every time a new artefact lands.
|
|
17
|
+
*
|
|
18
|
+
* Patterns are written relative to the schema directory so a user
|
|
19
|
+
* who runs `init --schema-path db/contract.prisma` gets
|
|
20
|
+
* `db/contract.json linguist-generated` — not the workspace-glob form
|
|
21
|
+
* `<glob>/contract.json` (which would over-match any unrelated
|
|
22
|
+
* `contract.json` the user has elsewhere) and not the absolute
|
|
23
|
+
* `prisma/contract.json` (which would silently break for a non-default
|
|
24
|
+
* schema path).
|
|
25
|
+
*/
|
|
26
|
+
const ARTEFACT_FILENAMES: readonly string[] = [
|
|
27
|
+
'contract.json',
|
|
28
|
+
'contract.d.ts',
|
|
29
|
+
'end-contract.json',
|
|
30
|
+
'end-contract.d.ts',
|
|
31
|
+
'start-contract.json',
|
|
32
|
+
'start-contract.d.ts',
|
|
33
|
+
'ops.json',
|
|
34
|
+
'migration.json',
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
const ATTRIBUTE = 'linguist-generated';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Computes the `.gitattributes` lines this scaffold expects to own. Each
|
|
41
|
+
* line has the shape `<path> linguist-generated`. The `target` parameter
|
|
42
|
+
* is currently unused but accepted for symmetry with the other hygiene
|
|
43
|
+
* helpers and to leave room for target-specific entries (e.g. a future
|
|
44
|
+
* Mongo-only artefact) without a signature break.
|
|
45
|
+
*/
|
|
46
|
+
export function requiredGitattributesLines(
|
|
47
|
+
schemaDir: string,
|
|
48
|
+
_target: TargetId,
|
|
49
|
+
): readonly string[] {
|
|
50
|
+
const dir = schemaDir === '.' ? '' : schemaDir.replace(/\/+$/, '');
|
|
51
|
+
const prefix = dir === '' ? '' : `${dir}/`;
|
|
52
|
+
return ARTEFACT_FILENAMES.map((file) => `${prefix}${file} ${ATTRIBUTE}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Idempotent `.gitattributes` merge (FR3.4 / FR9.3). Returns the new file
|
|
57
|
+
* content given the existing content (or `undefined` if the file does
|
|
58
|
+
* not yet exist).
|
|
59
|
+
*
|
|
60
|
+
* Equivalence is exact-line: a user-customised line like
|
|
61
|
+
* `prisma/*.json linguist-generated` is *not* recognised as covering
|
|
62
|
+
* `prisma/contract.json linguist-generated`. We accept that
|
|
63
|
+
* over-specification — preserving the user's broad pattern *and*
|
|
64
|
+
* appending the narrow one — because the narrow lines are what the
|
|
65
|
+
* acceptance criteria pin (FR3.4 AC).
|
|
66
|
+
*
|
|
67
|
+
* Returns `null` when no changes are required (file already contains
|
|
68
|
+
* every required entry).
|
|
69
|
+
*/
|
|
70
|
+
export function mergeGitattributes(
|
|
71
|
+
existing: string | undefined,
|
|
72
|
+
required: readonly string[],
|
|
73
|
+
): string | null {
|
|
74
|
+
if (existing === undefined) {
|
|
75
|
+
return `${required.join('\n')}\n`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const presentLines = new Set(
|
|
79
|
+
existing
|
|
80
|
+
.split('\n')
|
|
81
|
+
.map((line) => line.trim())
|
|
82
|
+
.filter((line) => line.length > 0 && !line.startsWith('#')),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
const missing = required.filter((line) => !presentLines.has(line));
|
|
86
|
+
if (missing.length === 0) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Mirrors `mergeGitignore`: a zero-byte existing file would otherwise
|
|
91
|
+
// gain a leading blank line, because `''.endsWith('\n')` is false. The
|
|
92
|
+
// empty-file case is uncommon (most projects either don't have a
|
|
93
|
+
// `.gitattributes` or have one with content), but symmetric handling
|
|
94
|
+
// keeps the two mergers' invariants identical.
|
|
95
|
+
const separator = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
|
|
96
|
+
return `${existing}${separator}${missing.join('\n')}\n`;
|
|
97
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The minimal `.gitignore` lines a Prisma Next scaffold needs (FR3.3).
|
|
3
|
+
* Order matches what Node tooling typically writes today.
|
|
4
|
+
*
|
|
5
|
+
* `node_modules/` first because it's the byte-largest miss; `dist/`
|
|
6
|
+
* because the scaffolded `tsconfig.json` writes there; `.env` last so
|
|
7
|
+
* the secret-bearing file is the one most-recently visible in any diff
|
|
8
|
+
* (a paranoid-correct ordering — humans skim from the top).
|
|
9
|
+
*/
|
|
10
|
+
export const REQUIRED_GITIGNORE_ENTRIES: readonly string[] = ['node_modules/', 'dist/', '.env'];
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Idempotent `.gitignore` merge (FR3.3 / FR9.3). Returns the new file
|
|
14
|
+
* content given the existing content (or `undefined` if the file does
|
|
15
|
+
* not yet exist). Adds only entries that are not already present and
|
|
16
|
+
* never duplicates a line. Existing comments and blank lines are
|
|
17
|
+
* preserved verbatim — `.gitignore` is parsed by `git` without a tree,
|
|
18
|
+
* so any line modification risks changing semantics.
|
|
19
|
+
*
|
|
20
|
+
* Pattern equivalence is line-literal: `node_modules/` and `node_modules`
|
|
21
|
+
* are treated as different entries. This is intentional — `git` treats
|
|
22
|
+
* them differently (the trailing slash restricts the match to
|
|
23
|
+
* directories), and the AC pins the trailing-slash form.
|
|
24
|
+
*
|
|
25
|
+
* Returns `null` when no changes are required (file already contains
|
|
26
|
+
* every required entry). The caller can use this to decide whether to
|
|
27
|
+
* include `.gitignore` in `filesWritten`.
|
|
28
|
+
*/
|
|
29
|
+
export function mergeGitignore(existing: string | undefined): string | null {
|
|
30
|
+
if (existing === undefined) {
|
|
31
|
+
return `${REQUIRED_GITIGNORE_ENTRIES.join('\n')}\n`;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const present = new Set(
|
|
35
|
+
existing
|
|
36
|
+
.split('\n')
|
|
37
|
+
.map((line) => line.trim())
|
|
38
|
+
.filter((line) => line.length > 0 && !line.startsWith('#')),
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const missing = REQUIRED_GITIGNORE_ENTRIES.filter((entry) => !present.has(entry));
|
|
42
|
+
if (missing.length === 0) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const separator = existing.length === 0 || existing.endsWith('\n') ? '' : '\n';
|
|
47
|
+
return `${existing}${separator}${missing.join('\n')}\n`;
|
|
48
|
+
}
|