@aventara/cli 0.0.0-stage → 0.1.0-pilot.1

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.
Files changed (54) hide show
  1. package/LICENSE +91 -0
  2. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  3. package/README.md +120 -2
  4. package/dist/apply/app-module.anchor.d.ts +30 -0
  5. package/dist/apply/app-module.anchor.js +185 -0
  6. package/dist/apply/conflict.confirmer.d.ts +24 -0
  7. package/dist/apply/conflict.confirmer.js +39 -0
  8. package/dist/apply/e2e-spec.anchor.d.ts +18 -0
  9. package/dist/apply/e2e-spec.anchor.js +40 -0
  10. package/dist/apply/manifest.merger.d.ts +47 -0
  11. package/dist/apply/manifest.merger.js +139 -0
  12. package/dist/aventara.bin.d.ts +2 -0
  13. package/dist/aventara.bin.js +15 -0
  14. package/dist/catalog/adapter.catalog.generated.d.ts +19 -0
  15. package/dist/catalog/adapter.catalog.generated.js +35 -0
  16. package/dist/catalog/catalog-entry.interface.d.ts +46 -0
  17. package/dist/catalog/catalog-entry.interface.js +1 -0
  18. package/dist/catalog/catalog.matcher.d.ts +46 -0
  19. package/dist/catalog/catalog.matcher.js +83 -0
  20. package/dist/catalog/range.reader.d.ts +17 -0
  21. package/dist/catalog/range.reader.js +77 -0
  22. package/dist/cli.d.ts +30 -0
  23. package/dist/cli.js +96 -0
  24. package/dist/command/command.parser.d.ts +36 -0
  25. package/dist/command/command.parser.js +163 -0
  26. package/dist/node-version.guard.d.ts +8 -0
  27. package/dist/node-version.guard.js +59 -0
  28. package/dist/plan/project.planner.d.ts +59 -0
  29. package/dist/plan/project.planner.js +201 -0
  30. package/dist/project/package-manager.detector.d.ts +12 -0
  31. package/dist/project/package-manager.detector.js +14 -0
  32. package/dist/project/project.inspector.d.ts +38 -0
  33. package/dist/project/project.inspector.js +84 -0
  34. package/dist/project/service.detector.d.ts +45 -0
  35. package/dist/project/service.detector.js +106 -0
  36. package/dist/project/source.scanner.d.ts +16 -0
  37. package/dist/project/source.scanner.js +134 -0
  38. package/dist/run/command.runner.d.ts +15 -0
  39. package/dist/run/command.runner.js +20 -0
  40. package/dist/run/init.orchestrator.d.ts +29 -0
  41. package/dist/run/init.orchestrator.js +222 -0
  42. package/dist/run/new.orchestrator.d.ts +21 -0
  43. package/dist/run/new.orchestrator.js +88 -0
  44. package/dist/templates/prisma7/prisma7.templates.d.ts +34 -0
  45. package/dist/templates/prisma7/prisma7.templates.js +397 -0
  46. package/dist/templates/template.registry.d.ts +156 -0
  47. package/dist/templates/template.registry.js +17 -0
  48. package/dist/wizard/answer.resolver.d.ts +49 -0
  49. package/dist/wizard/answer.resolver.js +106 -0
  50. package/dist/wizard/readline.prompter.d.ts +24 -0
  51. package/dist/wizard/readline.prompter.js +76 -0
  52. package/dist/wizard/wizard.questions.d.ts +114 -0
  53. package/dist/wizard/wizard.questions.js +261 -0
  54. package/package.json +28 -3
@@ -0,0 +1,163 @@
1
+ import { INIT_FLAG_QUESTIONS, NEW_QUESTIONS, } from "../wizard/wizard.questions.js";
2
+ /** The command line cannot be understood. A refusal: one sentence, exit 1. */
3
+ export class CliCommandError extends Error {
4
+ name = "CliCommandError";
5
+ }
6
+ /** The options that are not wizard questions. */
7
+ const OPTIONS = [
8
+ {
9
+ flag: "--skip-install",
10
+ key: "skipInstall",
11
+ commands: ["new", "init"],
12
+ summary: "write the files, and print the install command instead of running it",
13
+ },
14
+ {
15
+ flag: "--skip-git",
16
+ key: "skipGit",
17
+ commands: ["new"],
18
+ summary: "create no git repository (passed to nest new)",
19
+ },
20
+ {
21
+ flag: "--yes",
22
+ alias: "-y",
23
+ key: "yes",
24
+ commands: ["new", "init"],
25
+ summary: "accept every unanswered question's default, and confirm overwriting existing content",
26
+ },
27
+ ];
28
+ const QUESTIONS = {
29
+ new: NEW_QUESTIONS,
30
+ init: INIT_FLAG_QUESTIONS,
31
+ };
32
+ const HELP = "run `aventara --help` for usage";
33
+ function usageLines(command) {
34
+ const rows = [
35
+ ...QUESTIONS[command]
36
+ .filter((question) => question.flag.startsWith("--"))
37
+ .map((question) => [
38
+ `${question.flag} ${question.placeholder ?? "<value>"}`,
39
+ `${question.label}; asked when omitted`,
40
+ ]),
41
+ ...OPTIONS.filter((option) => option.commands.includes(command)).map((option) => [
42
+ `${option.alias === undefined ? "" : `${option.alias}, `}${option.flag}`,
43
+ option.summary,
44
+ ]),
45
+ ];
46
+ const width = Math.max(...rows.map(([left]) => left.length));
47
+ return rows
48
+ .map(([left, right]) => ` ${left.padEnd(width)} ${right}`)
49
+ .join("\n");
50
+ }
51
+ /** What each command does, in one line: the top-level usage and its own. */
52
+ const SUMMARY = {
53
+ new: "Create a NestJS project with Aventara (nest new, then init).",
54
+ init: "Add Aventara to the NestJS 12 project in this directory.",
55
+ };
56
+ const SYNOPSIS = {
57
+ new: "aventara new <name> [options]",
58
+ init: "aventara init [options]",
59
+ };
60
+ const ASKED = `On a terminal every unanswered question is asked; anywhere else, pass its flag
61
+ or --yes, or the run stops before writing anything.
62
+ `;
63
+ /** `aventara <command> --help` (pilot.1): that command's usage alone. */
64
+ export function commandUsage(command) {
65
+ return `Usage: ${SYNOPSIS[command]}
66
+
67
+ ${SUMMARY[command]}
68
+
69
+ Options:
70
+ ${usageLines(command)}
71
+
72
+ ${ASKED}`;
73
+ }
74
+ export const USAGE = `aventara — scaffold an Aventara server on NestJS.
75
+
76
+ Usage:
77
+ ${SYNOPSIS.new} ${SUMMARY.new}
78
+ ${usageLines("new")}
79
+
80
+ ${SYNOPSIS.init} ${SUMMARY.init}
81
+ ${usageLines("init")}
82
+
83
+ aventara --help Print this and exit 0.
84
+ aventara <command> --help Print that command's usage and exit 0.
85
+ aventara --version Print this CLI's version and exit 0.
86
+
87
+ ${ASKED}`;
88
+ /** @throws CliCommandError when `argv` is not a command this CLI has. */
89
+ export function parseCliCommand(argv) {
90
+ const [command, ...rest] = argv;
91
+ if (command === undefined) {
92
+ throw new CliCommandError(`a command is required: new or init; ${HELP}`);
93
+ }
94
+ if (command === "--help" || command === "-h") {
95
+ return { command: "help" };
96
+ }
97
+ if (command === "--version") {
98
+ return { command: "version" };
99
+ }
100
+ if (command !== "new" && command !== "init") {
101
+ throw new CliCommandError(`unknown command ${JSON.stringify(command)}: the commands are new and init; ${HELP}`);
102
+ }
103
+ if (rest.includes("--help") || rest.includes("-h")) {
104
+ return { command: "help", topic: command };
105
+ }
106
+ const questions = QUESTIONS[command];
107
+ const given = {};
108
+ const options = {};
109
+ const seen = new Set();
110
+ const positional = questions.find((question) => !question.flag.startsWith("--"));
111
+ for (let index = 0; index < rest.length; index += 1) {
112
+ const argument = rest[index];
113
+ if (!argument.startsWith("-")) {
114
+ if (positional === undefined || given[positional.id] !== undefined) {
115
+ throw new CliCommandError(`unexpected argument ${JSON.stringify(argument)}: aventara ${command} takes ${positional === undefined ? "no" : "one"} positional argument; ${HELP}`);
116
+ }
117
+ given[positional.id] = argument;
118
+ continue;
119
+ }
120
+ const equals = argument.indexOf("=");
121
+ const flag = equals === -1 ? argument : argument.slice(0, equals);
122
+ const inline = equals === -1 ? undefined : argument.slice(equals + 1);
123
+ const question = questions.find((candidate) => candidate.flag === flag);
124
+ const option = OPTIONS.find((candidate) => candidate.flag === flag || candidate.alias === flag);
125
+ const canonical = question?.flag ?? option?.flag ?? flag;
126
+ if (question === undefined && option === undefined) {
127
+ const elsewhere = ["new", "init"].filter((other) => QUESTIONS[other].some((candidate) => candidate.flag === flag));
128
+ throw new CliCommandError(elsewhere.length === 0
129
+ ? `unknown flag ${flag}; ${HELP}`
130
+ : `${flag} is a flag of aventara ${elsewhere.join(" and ")}, not aventara ${command}; ${HELP}`);
131
+ }
132
+ if (option !== undefined && !option.commands.includes(command)) {
133
+ throw new CliCommandError(`${option.flag} is a flag of aventara ${option.commands.join(" and ")}, not aventara ${command}; ${HELP}`);
134
+ }
135
+ if (seen.has(canonical)) {
136
+ throw new CliCommandError(`${canonical} was given twice; ${HELP}`);
137
+ }
138
+ seen.add(canonical);
139
+ if (question === undefined) {
140
+ if (inline !== undefined) {
141
+ throw new CliCommandError(`${canonical} takes no value; ${HELP}`);
142
+ }
143
+ options[option.key] = true;
144
+ continue;
145
+ }
146
+ const value = inline ?? rest[index + 1];
147
+ if (value === undefined ||
148
+ (inline === undefined && value.startsWith("-"))) {
149
+ throw new CliCommandError(`${canonical} takes a value; ${HELP}`);
150
+ }
151
+ if (inline === undefined) {
152
+ index += 1;
153
+ }
154
+ given[question.id] = value;
155
+ }
156
+ return {
157
+ command,
158
+ given,
159
+ skipInstall: options.skipInstall === true,
160
+ skipGit: options.skipGit === true,
161
+ yes: options.yes === true,
162
+ };
163
+ }
@@ -0,0 +1,8 @@
1
+ /** The sentence a bin answers on `version`, or `undefined` when `range` admits it. */
2
+ export declare function nodeVersionRefusal(bin: string, packageName: string, range: string, version: string): string | undefined;
3
+ /**
4
+ * Reads the manifest at `manifestUrl`; on a Node its `engines.node` does not
5
+ * admit, writes the sentence to stderr, sets exit code 1 and answers `true` —
6
+ * the bin then loads nothing else.
7
+ */
8
+ export declare function refuseUnsupportedNode(bin: string, manifestUrl: URL): boolean;
@@ -0,0 +1,59 @@
1
+ // biome-ignore lint/style/useNodejsImportProtocol: Node 14.0–14.13.0 resolves no "node:" specifier in an ES module, and this module runs on the Nodes the packages do not support.
2
+ import { readFileSync } from "fs";
3
+ function versionOf(text) {
4
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(text);
5
+ if (match === null) {
6
+ throw new Error(`"${text}" is not a Node version`);
7
+ }
8
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
9
+ }
10
+ function compareVersions(left, right) {
11
+ for (let index = 0; index < 3; index += 1) {
12
+ const difference = left[index] - right[index];
13
+ if (difference !== 0) {
14
+ return difference;
15
+ }
16
+ }
17
+ return 0;
18
+ }
19
+ /**
20
+ * Whether `range` admits `version`. Reads `^x.y.z` (the same major, from the
21
+ * floor; majors ≥ 1) and `>=x.y.z`, joined by `||`; anything else throws rather
22
+ * than admit or refuse by guess.
23
+ */
24
+ function admits(range, version) {
25
+ return range.split("||").some((part) => {
26
+ const comparator = /^\s*(\^|>=)(\d+\.\d+\.\d+)\s*$/.exec(part);
27
+ if (comparator === null) {
28
+ throw new Error(`engines.node "${range}": the bin's Node guard reads only "^x.y.z" and ">=x.y.z" joined by "||"`);
29
+ }
30
+ const floor = versionOf(comparator[2]);
31
+ return (compareVersions(version, floor) >= 0 &&
32
+ (comparator[1] === ">=" || version[0] === floor[0]));
33
+ });
34
+ }
35
+ /** The sentence a bin answers on `version`, or `undefined` when `range` admits it. */
36
+ export function nodeVersionRefusal(bin, packageName, range, version) {
37
+ return admits(range, versionOf(version))
38
+ ? undefined
39
+ : `${bin}: Node ${version} is not supported; ${packageName} needs Node ${range}.`;
40
+ }
41
+ /**
42
+ * Reads the manifest at `manifestUrl`; on a Node its `engines.node` does not
43
+ * admit, writes the sentence to stderr, sets exit code 1 and answers `true` —
44
+ * the bin then loads nothing else.
45
+ */
46
+ export function refuseUnsupportedNode(bin, manifestUrl) {
47
+ const manifest = JSON.parse(readFileSync(manifestUrl, "utf8"));
48
+ const range = manifest.engines === undefined ? undefined : manifest.engines.node;
49
+ if (range === undefined) {
50
+ throw new Error(`${manifest.name} declares no engines.node for its bin's Node guard`);
51
+ }
52
+ const refusal = nodeVersionRefusal(bin, manifest.name, range, process.version);
53
+ if (refusal === undefined) {
54
+ return false;
55
+ }
56
+ process.stderr.write(`${refusal}\n`);
57
+ process.exitCode = 1;
58
+ return true;
59
+ }
@@ -0,0 +1,59 @@
1
+ import type { CatalogEntry } from "../catalog/catalog-entry.interface.js";
2
+ import { type ProjectInspection } from "../project/project.inspector.js";
3
+ import { type OrmScaffold } from "../templates/template.registry.js";
4
+ import type { PackageManager } from "../wizard/wizard.questions.js";
5
+ /**
6
+ * §4.1's `plan`: everything `aventara init` will write, decided before anything
7
+ * is written — the files, the anchored `app.module.ts` edit (or the printed
8
+ * instructions), the merged manifests — and every **conflict**, computed both
9
+ * ways so the confirmation can choose. Pure apart from reading the files it
10
+ * would replace.
11
+ *
12
+ * What is Nest's (its default start scripts, `AventaraModule`'s import) and what
13
+ * is Aventara's (its packages at this CLI's version, D1; the `aventara:prepare`
14
+ * and `postinstall` scripts, Q16-22, D2) is decided here; what is the ORM's
15
+ * comes from its templates (D7).
16
+ */
17
+ /** The name of the script that regenerates the ORM client and the discovery artifact (Q16-22). */
18
+ export declare const PREPARE_SCRIPT = "aventara:prepare";
19
+ export type PlannedWrite = {
20
+ readonly path: string;
21
+ readonly content: string;
22
+ /** `create` a new file, or `replace` one whose content changes. */
23
+ readonly change: "create" | "replace";
24
+ };
25
+ export type ProjectPlan = {
26
+ /** What is written when the conflicts are kept as they are. */
27
+ readonly keeping: readonly PlannedWrite[];
28
+ /** What is written once the conflicts are confirmed (`--yes`, or a yes). */
29
+ readonly replacing: readonly PlannedWrite[];
30
+ /** Existing content that differs from what would be written. */
31
+ readonly conflicts: readonly string[];
32
+ readonly warnings: readonly string[];
33
+ /** Printed when the anchored edit could not be made. */
34
+ readonly instructions: string | undefined;
35
+ readonly nextSteps: readonly string[];
36
+ };
37
+ export type PlanInput = {
38
+ readonly inspection: ProjectInspection;
39
+ readonly entry: CatalogEntry;
40
+ readonly scaffold: OrmScaffold;
41
+ readonly packageManager: PackageManager;
42
+ /** This CLI's version: every Aventara package is pinned to it (D1). */
43
+ readonly aventaraVersion: string;
44
+ readonly skipInstall: boolean;
45
+ /** The current text of a project file, or `undefined` when absent. */
46
+ readonly read: (file: string) => string | undefined;
47
+ /** The anchored edit is not attempted; the wiring is printed (a service holding its client, §4.4.4). */
48
+ readonly wiringByHand?: string;
49
+ /** Said before the write, beside the planner's own. */
50
+ readonly warnings?: readonly string[];
51
+ };
52
+ /**
53
+ * pilot.1 — the frontend's first command, as the developer can run it: the
54
+ * package manager's own one-off runner (`npx` or `pnpm dlx`), and — while this
55
+ * CLI is a prerelease — its dist-tag, so the client comes from the same release
56
+ * (`0.1.0-pilot.1` → `@aventara/client@pilot`). A release has no tag to name.
57
+ */
58
+ export declare function frontendInitCommand(packageManager: PackageManager, cliVersion: string): string;
59
+ export declare function planInit(input: PlanInput): ProjectPlan;
@@ -0,0 +1,201 @@
1
+ import { spliceAppModule, wiringInstructions, } from "../apply/app-module.anchor.js";
2
+ import { E2E_SPEC, spliceContractTest } from "../apply/e2e-spec.anchor.js";
3
+ import { mergeEnvFile, mergeGitignore, mergePackageManifest, mergePnpmWorkspace, } from "../apply/manifest.merger.js";
4
+ import { APP_MODULE, } from "../project/project.inspector.js";
5
+ import { SCAFFOLD_ENTRYPOINT, } from "../templates/template.registry.js";
6
+ /**
7
+ * §4.1's `plan`: everything `aventara init` will write, decided before anything
8
+ * is written — the files, the anchored `app.module.ts` edit (or the printed
9
+ * instructions), the merged manifests — and every **conflict**, computed both
10
+ * ways so the confirmation can choose. Pure apart from reading the files it
11
+ * would replace.
12
+ *
13
+ * What is Nest's (its default start scripts, `AventaraModule`'s import) and what
14
+ * is Aventara's (its packages at this CLI's version, D1; the `aventara:prepare`
15
+ * and `postinstall` scripts, Q16-22, D2) is decided here; what is the ORM's
16
+ * comes from its templates (D7).
17
+ */
18
+ /** The name of the script that regenerates the ORM client and the discovery artifact (Q16-22). */
19
+ export const PREPARE_SCRIPT = "aventara:prepare";
20
+ /** D3: `nest new`'s start scripts (B1), and what each becomes so `.env` reaches the server. */
21
+ const START_SCRIPTS = {
22
+ start: { from: "nest start", to: "nest start --env-file .env" },
23
+ "start:dev": {
24
+ from: "nest start --watch",
25
+ to: "nest start --watch --env-file .env",
26
+ },
27
+ "start:debug": {
28
+ from: "nest start --debug --watch",
29
+ to: "nest start --debug --watch --env-file .env",
30
+ },
31
+ "start:prod": {
32
+ from: "node dist/main",
33
+ to: "node --env-file-if-exists=.env dist/main",
34
+ },
35
+ };
36
+ /**
37
+ * F-853: `nest new`'s e2e script per module kind (B1, B28), and the same run with
38
+ * the `.env` the start scripts load — the e2e spec boots `AppModule`, whose
39
+ * service needs `DATABASE_URL`, and neither vitest nor jest loads `.env`.
40
+ */
41
+ const E2E_SCRIPTS = {
42
+ esm: {
43
+ from: "vitest run --config ./vitest.config.e2e.ts",
44
+ to: "node --env-file-if-exists=.env ./node_modules/vitest/vitest.mjs run --config ./vitest.config.e2e.ts",
45
+ },
46
+ cjs: {
47
+ from: "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config ./test/jest-e2e.json",
48
+ to: "node --env-file-if-exists=.env --experimental-vm-modules ./node_modules/jest/bin/jest.js --config ./test/jest-e2e.json",
49
+ },
50
+ };
51
+ const NEST_HOST = "@aventara/nest";
52
+ const CORE = "@aventara/core";
53
+ function writeOf(path, before, after) {
54
+ return before === after
55
+ ? []
56
+ : [
57
+ {
58
+ path,
59
+ content: after,
60
+ change: before === undefined ? "create" : "replace",
61
+ },
62
+ ];
63
+ }
64
+ /**
65
+ * pilot.1 — the frontend's first command, as the developer can run it: the
66
+ * package manager's own one-off runner (`npx` or `pnpm dlx`), and — while this
67
+ * CLI is a prerelease — its dist-tag, so the client comes from the same release
68
+ * (`0.1.0-pilot.1` → `@aventara/client@pilot`). A release has no tag to name.
69
+ */
70
+ export function frontendInitCommand(packageManager, cliVersion) {
71
+ const tag = /^\d+\.\d+\.\d+-([0-9A-Za-z-]+)/.exec(cliVersion)?.[1];
72
+ const runner = packageManager === "pnpm" ? "pnpm dlx" : "npx";
73
+ return `${runner} @aventara/client${tag === undefined ? "" : `@${tag}`} init`;
74
+ }
75
+ export function planInit(input) {
76
+ const { inspection, scaffold, packageManager } = input;
77
+ const conflicts = new Set();
78
+ const keeping = [];
79
+ const replacing = [];
80
+ const warnings = [...(input.warnings ?? [])];
81
+ const both = (path, before, merge) => {
82
+ const kept = merge(false);
83
+ const replaced = merge(true);
84
+ for (const conflict of replaced.conflicts) {
85
+ conflicts.add(conflict);
86
+ }
87
+ keeping.push(...writeOf(path, before, kept.text));
88
+ replacing.push(...writeOf(path, before, replaced.text));
89
+ };
90
+ // The ORM's new files: identical content is no change (P3), other content a conflict.
91
+ for (const file of scaffold.files) {
92
+ const before = input.read(file.path);
93
+ if (before === undefined) {
94
+ keeping.push({
95
+ path: file.path,
96
+ content: file.content,
97
+ change: "create",
98
+ });
99
+ replacing.push({
100
+ path: file.path,
101
+ content: file.content,
102
+ change: "create",
103
+ });
104
+ }
105
+ else if (before !== file.content) {
106
+ conflicts.add(file.path);
107
+ replacing.push({
108
+ path: file.path,
109
+ content: file.content,
110
+ change: "replace",
111
+ });
112
+ }
113
+ }
114
+ // The one edit to developer code (R2).
115
+ const wiring = {
116
+ imports: [
117
+ `import { AventaraModule } from '${NEST_HOST}';`,
118
+ ...scaffold.appModule.imports,
119
+ ],
120
+ entries: scaffold.appModule.entries,
121
+ };
122
+ const appModule = input.read(APP_MODULE);
123
+ const edit = input.wiringByHand !== undefined
124
+ ? { kind: "not-anchored", reason: input.wiringByHand }
125
+ : appModule === undefined
126
+ ? {
127
+ kind: "not-anchored",
128
+ reason: `there is no ${APP_MODULE}`,
129
+ }
130
+ : spliceAppModule(appModule, wiring);
131
+ let instructions;
132
+ if (edit.kind === "edited") {
133
+ keeping.push(...writeOf(APP_MODULE, appModule, edit.text));
134
+ replacing.push(...writeOf(APP_MODULE, appModule, edit.text));
135
+ // pilot.1: Nest's own e2e spec also asks for the contract — only when
136
+ // AventaraModule was wired in, so the test proves what it claims.
137
+ const spec = input.read(E2E_SPEC);
138
+ const tested = spec === undefined
139
+ ? undefined
140
+ : spliceContractTest(spec, SCAFFOLD_ENTRYPOINT);
141
+ if (tested !== undefined) {
142
+ keeping.push(...writeOf(E2E_SPEC, spec, tested));
143
+ replacing.push(...writeOf(E2E_SPEC, spec, tested));
144
+ }
145
+ }
146
+ else {
147
+ instructions = wiringInstructions(wiring);
148
+ warnings.push(`${APP_MODULE} was not edited: ${edit.reason}; add AventaraModule by hand, as printed below`);
149
+ }
150
+ const versions = {
151
+ [CORE]: input.aventaraVersion,
152
+ [NEST_HOST]: input.aventaraVersion,
153
+ [input.entry.adapterPackage]: input.aventaraVersion,
154
+ };
155
+ both("package.json", inspection.manifestText, (replace) => mergePackageManifest(inspection.manifestText, {
156
+ dependencies: { ...versions, ...scaffold.dependencies },
157
+ devDependencies: scaffold.devDependencies,
158
+ scripts: {
159
+ [PREPARE_SCRIPT]: scaffold.prepare,
160
+ // D2: the generated trees are ignored and rebuilt on every install.
161
+ postinstall: `${packageManager} run ${PREPARE_SCRIPT}`,
162
+ },
163
+ rewrites: inspection.usesNestConfig
164
+ ? {}
165
+ : {
166
+ ...START_SCRIPTS,
167
+ "test:e2e": E2E_SCRIPTS[inspection.moduleKind],
168
+ },
169
+ }, replace));
170
+ const env = input.read(".env");
171
+ if (Object.keys(scaffold.env).length > 0) {
172
+ both(".env", env, (replace) => mergeEnvFile(env, scaffold.env, replace));
173
+ }
174
+ const gitignore = input.read(".gitignore");
175
+ both(".gitignore", gitignore, () => mergeGitignore(gitignore, scaffold.gitignore));
176
+ if (packageManager === "pnpm" && scaffold.nativeBuilds.length > 0) {
177
+ const workspace = input.read("pnpm-workspace.yaml");
178
+ both("pnpm-workspace.yaml", workspace, () => mergePnpmWorkspace(workspace, scaffold.nativeBuilds));
179
+ }
180
+ const run = packageManager === "pnpm" ? "pnpm" : "npm";
181
+ return {
182
+ keeping,
183
+ replacing,
184
+ conflicts: [...conflicts],
185
+ warnings,
186
+ instructions,
187
+ nextSteps: [
188
+ ...(input.skipInstall
189
+ ? [`Install, which also runs ${PREPARE_SCRIPT}: ${run} install`]
190
+ : []),
191
+ ...scaffold.nextSteps,
192
+ ...(inspection.usesNestConfig
193
+ ? [
194
+ "The start scripts were left as they are: @nestjs/config loads .env, so make sure it does before Prisma connects.",
195
+ ]
196
+ : []),
197
+ `Start the server: ${run} run start:dev`,
198
+ `In your frontend: ${frontendInitCommand(packageManager, input.aventaraVersion)}`,
199
+ ],
200
+ };
201
+ }
@@ -0,0 +1,12 @@
1
+ import type { PackageManager } from "../wizard/wizard.questions.js";
2
+ /**
3
+ * Q16-5's default package manager: a lockfile says what a project already uses
4
+ * (`init`); otherwise the package manager that launched this CLI says what the
5
+ * developer uses — `npm_config_user_agent` is `npm/…` under `npx` and
6
+ * `npm exec`, `pnpm/…` under `pnpm dlx` and `pnpm exec`, and unset when the bin
7
+ * runs directly (B11); otherwise npm.
8
+ */
9
+ export declare function detectPackageManager(signals: {
10
+ readonly lockfile?: PackageManager | undefined;
11
+ readonly userAgent?: string | undefined;
12
+ }): PackageManager;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Q16-5's default package manager: a lockfile says what a project already uses
3
+ * (`init`); otherwise the package manager that launched this CLI says what the
4
+ * developer uses — `npm_config_user_agent` is `npm/…` under `npx` and
5
+ * `npm exec`, `pnpm/…` under `pnpm dlx` and `pnpm exec`, and unset when the bin
6
+ * runs directly (B11); otherwise npm.
7
+ */
8
+ export function detectPackageManager(signals) {
9
+ if (signals.lockfile !== undefined) {
10
+ return signals.lockfile;
11
+ }
12
+ const launcher = signals.userAgent?.split("/")[0];
13
+ return launcher === "pnpm" ? "pnpm" : "npm";
14
+ }
@@ -0,0 +1,38 @@
1
+ import type { PackageManifest } from "../apply/manifest.merger.js";
2
+ import type { AdapterCatalog, CatalogEntry } from "../catalog/catalog-entry.interface.js";
3
+ import type { ModuleKind } from "../templates/template.registry.js";
4
+ import type { PackageManager } from "../wizard/wizard.questions.js";
5
+ /**
6
+ * §4.1's `inspect`: what `aventara init` needs to know about the project in the
7
+ * working directory, read before anything is asked or written — so every
8
+ * refusal here leaves the directory exactly as it was (it is a property of the
9
+ * order, not of a rollback).
10
+ */
11
+ /** The project is not one `aventara init` serves. A refusal: one sentence, nothing written. */
12
+ export declare class ProjectRefusedError extends Error {
13
+ readonly name = "ProjectRefusedError";
14
+ }
15
+ /** The ORM a project already has, by the catalog's family packages (R14, D10). */
16
+ export type OrmPresence = {
17
+ readonly kind: "absent";
18
+ } | {
19
+ readonly kind: "present";
20
+ /** The family packages found among its dependencies. */
21
+ readonly packages: readonly string[];
22
+ /** The catalog entries whose family they belong to. */
23
+ readonly families: readonly CatalogEntry["orm"]["label"][];
24
+ };
25
+ export type ProjectInspection = {
26
+ readonly directory: string;
27
+ readonly manifest: PackageManifest;
28
+ readonly manifestText: string;
29
+ readonly moduleKind: ModuleKind;
30
+ /** The package manager the lockfile names, if exactly one does. */
31
+ readonly lockfile: PackageManager | undefined;
32
+ readonly orm: OrmPresence;
33
+ /** `@nestjs/config` loads `.env` itself, so the start scripts are left alone (P5). */
34
+ readonly usesNestConfig: boolean;
35
+ };
36
+ /** The file the anchored edit opens (R2). */
37
+ export declare const APP_MODULE = "src/app.module.ts";
38
+ export declare function inspectProject(directory: string, catalog: AdapterCatalog): ProjectInspection;
@@ -0,0 +1,84 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { lowestMajorOf } from "../catalog/range.reader.js";
4
+ /**
5
+ * §4.1's `inspect`: what `aventara init` needs to know about the project in the
6
+ * working directory, read before anything is asked or written — so every
7
+ * refusal here leaves the directory exactly as it was (it is a property of the
8
+ * order, not of a rollback).
9
+ */
10
+ /** The project is not one `aventara init` serves. A refusal: one sentence, nothing written. */
11
+ export class ProjectRefusedError extends Error {
12
+ name = "ProjectRefusedError";
13
+ }
14
+ /** Nest 12 only (Phase 11 Q16). */
15
+ const NEST_MAJOR = 12;
16
+ /** D4: TypeScript 7 has no classic compiler API. */
17
+ const FIRST_UNSUPPORTED_TYPESCRIPT = 7;
18
+ /** The file the anchored edit opens (R2). */
19
+ export const APP_MODULE = "src/app.module.ts";
20
+ const LOCKFILES = [
21
+ ["package-lock.json", "npm"],
22
+ ["pnpm-lock.yaml", "pnpm"],
23
+ ["yarn.lock", "yarn"],
24
+ ];
25
+ function dependencyRange(manifest, name) {
26
+ return manifest.dependencies?.[name] ?? manifest.devDependencies?.[name];
27
+ }
28
+ function matchesFamily(pattern, name) {
29
+ return pattern.endsWith("*")
30
+ ? name.startsWith(pattern.slice(0, -1))
31
+ : name === pattern;
32
+ }
33
+ export function inspectProject(directory, catalog) {
34
+ const manifestPath = path.join(directory, "package.json");
35
+ if (!existsSync(manifestPath)) {
36
+ throw new ProjectRefusedError(`aventara init runs in a NestJS ${NEST_MAJOR} project, and there is no package.json in ${directory}`);
37
+ }
38
+ const manifestText = readFileSync(manifestPath, "utf8");
39
+ const manifest = JSON.parse(manifestText);
40
+ const nest = dependencyRange(manifest, "@nestjs/core");
41
+ if (nest === undefined) {
42
+ throw new ProjectRefusedError(`aventara init runs in a NestJS ${NEST_MAJOR} project, and this package.json has no @nestjs/core dependency`);
43
+ }
44
+ if (lowestMajorOf(nest) !== NEST_MAJOR) {
45
+ throw new ProjectRefusedError(`aventara init supports NestJS ${NEST_MAJOR} only, and this project depends on @nestjs/core ${nest}`);
46
+ }
47
+ const typescript = dependencyRange(manifest, "typescript");
48
+ const typescriptMajor = typescript === undefined ? undefined : lowestMajorOf(typescript);
49
+ if (typescriptMajor !== undefined &&
50
+ typescriptMajor >= FIRST_UNSUPPORTED_TYPESCRIPT) {
51
+ throw new ProjectRefusedError(`this project declares typescript ${typescript}, and TypeScript ${FIRST_UNSUPPORTED_TYPESCRIPT} has no classic compiler API for Aventara's generate steps to read types with; use TypeScript 6 or 5`);
52
+ }
53
+ const lockfiles = LOCKFILES.filter(([file]) => existsSync(path.join(directory, file))).map(([, manager]) => manager);
54
+ const known = lockfiles.filter((manager) => manager !== "yarn");
55
+ if (lockfiles.includes("yarn") && known.length === 0) {
56
+ throw new ProjectRefusedError("this project has only a yarn.lock, and aventara init supports npm and pnpm");
57
+ }
58
+ const appModule = path.join(directory, APP_MODULE);
59
+ if (existsSync(appModule) &&
60
+ /\bAventaraModule\b/.test(readFileSync(appModule, "utf8"))) {
61
+ throw new ProjectRefusedError(`this project is already initialized: ${APP_MODULE} uses AventaraModule`);
62
+ }
63
+ const dependencies = [
64
+ ...Object.keys(manifest.dependencies ?? {}),
65
+ ...Object.keys(manifest.devDependencies ?? {}),
66
+ ];
67
+ const present = catalog.filter((entry) => entry.familyPackages.some((pattern) => dependencies.some((name) => matchesFamily(pattern, name))));
68
+ const orm = present.length === 0
69
+ ? { kind: "absent" }
70
+ : {
71
+ kind: "present",
72
+ packages: dependencies.filter((name) => present.some((entry) => entry.familyPackages.some((pattern) => matchesFamily(pattern, name)))),
73
+ families: [...new Set(present.map((entry) => entry.orm.label))],
74
+ };
75
+ return {
76
+ directory,
77
+ manifest,
78
+ manifestText,
79
+ moduleKind: manifest.type === "module" ? "esm" : "cjs",
80
+ lockfile: known.length === 1 ? known[0] : undefined,
81
+ orm,
82
+ usesNestConfig: dependencyRange(manifest, "@nestjs/config") !== undefined,
83
+ };
84
+ }