@aventara/cli 0.1.0-pilot.1 → 0.1.0-pilot.3

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 (51) hide show
  1. package/README.md +12 -5
  2. package/dist/apply/app-module.anchor.d.ts +5 -11
  3. package/dist/apply/app-module.anchor.js +0 -23
  4. package/dist/apply/conflict.confirmer.d.ts +0 -9
  5. package/dist/apply/conflict.confirmer.js +0 -15
  6. package/dist/apply/e2e-spec.anchor.js +0 -17
  7. package/dist/apply/main-cors.anchor.d.ts +50 -0
  8. package/dist/apply/main-cors.anchor.js +60 -0
  9. package/dist/apply/manifest.merger.d.ts +17 -13
  10. package/dist/apply/manifest.merger.js +8 -18
  11. package/dist/aventara.bin.js +0 -10
  12. package/dist/catalog/adapter.catalog.generated.d.ts +1 -0
  13. package/dist/catalog/adapter.catalog.generated.js +1 -4
  14. package/dist/catalog/catalog-entry.interface.d.ts +21 -13
  15. package/dist/catalog/catalog.matcher.d.ts +7 -13
  16. package/dist/catalog/catalog.matcher.js +0 -3
  17. package/dist/catalog/range.reader.d.ts +4 -10
  18. package/dist/catalog/range.reader.js +0 -14
  19. package/dist/cli.d.ts +2 -3
  20. package/dist/cli.js +5 -8
  21. package/dist/command/command.parser.d.ts +11 -9
  22. package/dist/command/command.parser.js +24 -18
  23. package/dist/node-version.guard.js +0 -12
  24. package/dist/plan/project.planner.d.ts +7 -13
  25. package/dist/plan/project.planner.js +44 -35
  26. package/dist/project/package-manager.detector.d.ts +0 -7
  27. package/dist/project/package-manager.detector.js +0 -7
  28. package/dist/project/project.inspector.d.ts +3 -9
  29. package/dist/project/project.inspector.js +0 -10
  30. package/dist/project/service.detector.d.ts +5 -10
  31. package/dist/project/service.detector.js +0 -2
  32. package/dist/project/source.scanner.js +0 -13
  33. package/dist/run/command.runner.d.ts +4 -4
  34. package/dist/run/init.orchestrator.d.ts +4 -6
  35. package/dist/run/init.orchestrator.js +3 -16
  36. package/dist/run/new.orchestrator.d.ts +8 -9
  37. package/dist/run/new.orchestrator.js +10 -18
  38. package/dist/run/scaffold.committer.d.ts +28 -0
  39. package/dist/run/scaffold.committer.js +53 -0
  40. package/dist/templates/docs.links.d.ts +8 -0
  41. package/dist/templates/docs.links.js +2 -0
  42. package/dist/templates/prisma7/prisma7.templates.js +0 -27
  43. package/dist/templates/template.registry.d.ts +13 -17
  44. package/dist/templates/template.registry.js +0 -5
  45. package/dist/wizard/answer.resolver.d.ts +5 -7
  46. package/dist/wizard/answer.resolver.js +0 -10
  47. package/dist/wizard/readline.prompter.d.ts +5 -7
  48. package/dist/wizard/readline.prompter.js +0 -10
  49. package/dist/wizard/wizard.questions.d.ts +28 -22
  50. package/dist/wizard/wizard.questions.js +21 -49
  51. package/package.json +2 -2
@@ -1,11 +1,4 @@
1
- /**
2
- * Reading a declared version range for the two facts the CLI needs from one:
3
- * its lowest version (D1, D9: what `init` pins when it installs the ORM — the
4
- * adapter's measured floor) and its lowest major (a project's declared Nest and
5
- * TypeScript ranges, checked before anything is written).
6
- */
7
1
  const VERSION = /(\d+)(?:\.(\d+))?(?:\.(\d+))?/;
8
- /** `^7.10.0` → `7.10.0`; `>=7.10.0 <8` → `7.10.0`. */
9
2
  export function lowestVersionOf(range) {
10
3
  const found = VERSION.exec(range);
11
4
  if (found === null) {
@@ -13,19 +6,12 @@ export function lowestVersionOf(range) {
13
6
  }
14
7
  return `${found[1]}.${found[2] ?? 0}.${found[3] ?? 0}`;
15
8
  }
16
- /** `^12.0.1` → 12; `~6.0.2` → 6; a range with no number (`latest`, `*`) → `undefined`. */
17
9
  export function lowestMajorOf(range) {
18
10
  const found = VERSION.exec(range);
19
11
  return found === null ? undefined : Number(found[1]);
20
12
  }
21
13
  const RELEASE = /^(\d+)\.(\d+)\.(\d+)$/;
22
14
  const COMPARATOR = /^(\^|>=|>|<=|<|=)?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/;
23
- /**
24
- * Whether an installed `version` is in a declared `range` (R7: the adapter's
25
- * declaration decides support). Reads the comparator forms an adapter's peer
26
- * range uses; a pre-release never satisfies (semver's rule for a range whose
27
- * comparators carry none). Any other form is refused rather than guessed.
28
- */
29
15
  export function satisfiesRange(version, range) {
30
16
  const release = RELEASE.exec(version);
31
17
  if (release === null) {
package/dist/cli.d.ts CHANGED
@@ -2,9 +2,8 @@ import type { AdapterCatalog } from "./catalog/catalog-entry.interface.js";
2
2
  import { type CommandRunner } from "./run/command.runner.js";
3
3
  import { type Prompter } from "./wizard/answer.resolver.js";
4
4
  /**
5
- * `aventara` — the bin (R1): `new` and `init`. A refusal is one sentence on
6
- * stderr and exit 1, never a stack; anything else is a defect and keeps its
7
- * stack.
5
+ * `aventara` — the bin: `new` and `init`. A refusal is one sentence on stderr and
6
+ * exit 1, never a stack; anything else is a defect and keeps its stack.
8
7
  *
9
8
  * `init` initializes the project in the working directory
10
9
  * (`run/init.orchestrator.ts`); `new` creates one with the pinned `nest new` and
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { ConflictsNotConfirmedError } from "./apply/conflict.confirmer.js";
3
3
  import { ADAPTER_CATALOG } from "./catalog/adapter.catalog.generated.js";
4
- import { CliCommandError, commandUsage, parseCliCommand, USAGE, } from "./command/command.parser.js";
4
+ import { CliCommandError, commandUsage, parseCliCommand, usage, } from "./command/command.parser.js";
5
5
  import { ProjectRefusedError } from "./project/project.inspector.js";
6
6
  import { runCommand } from "./run/command.runner.js";
7
7
  import { InstallFailedError, runInit } from "./run/init.orchestrator.js";
@@ -27,7 +27,10 @@ export async function runCli(argv, io) {
27
27
  try {
28
28
  const command = parseCliCommand(argv);
29
29
  if (command.command === "help") {
30
- io.stdout(command.topic === undefined ? USAGE : commandUsage(command.topic));
30
+ const catalog = io.catalog ?? ADAPTER_CATALOG;
31
+ io.stdout(command.topic === undefined
32
+ ? usage(catalog)
33
+ : commandUsage(command.topic, catalog));
31
34
  return 0;
32
35
  }
33
36
  if (command.command === "version") {
@@ -66,12 +69,6 @@ export async function runCli(argv, io) {
66
69
  return 1;
67
70
  }
68
71
  }
69
- /**
70
- * Runs `aventara` over this process — its arguments, its terminal, its exit
71
- * code. Called by the bin's entry (`aventara.bin.ts`) once the Node guard has
72
- * admitted this Node; importing this module runs nothing, which lets `runCli`
73
- * be tested in process.
74
- */
75
72
  export async function runFromProcess() {
76
73
  const prompter = createReadlinePrompter({
77
74
  input: process.stdin,
@@ -1,10 +1,11 @@
1
+ import type { AdapterCatalog } from "../catalog/catalog-entry.interface.js";
1
2
  import { type QuestionId } from "../wizard/wizard.questions.js";
2
3
  /**
3
- * The `aventara` command line (R1, R2, Q16): `new <name>` and `init`, each
4
- * question's flag from the wizard's table, and the options that are not
5
- * questions. Unknown, misplaced or repeated arguments are refused in one
6
- * sentence; a flag's VALUE is the question's to judge, because what is valid
7
- * (`--db`'s providers) depends on the catalog and on earlier answers.
4
+ * The `aventara` command line: `new <name>` and `init`, each question's flag from
5
+ * the wizard's table, and the options that are not questions. Unknown, misplaced
6
+ * or repeated arguments are refused in one sentence; a flag's VALUE is the
7
+ * question's to judge, because what is valid (`--db`'s providers) depends on the
8
+ * catalog and on earlier answers.
8
9
  */
9
10
  export type ScaffoldCommandName = "new" | "init";
10
11
  export type ScaffoldCommand = {
@@ -12,9 +13,9 @@ export type ScaffoldCommand = {
12
13
  /** Raw answers by question, from flags and `new`'s positional `<name>`. */
13
14
  readonly given: Readonly<Partial<Record<QuestionId, string>>>;
14
15
  readonly skipInstall: boolean;
15
- /** `new` only: passed through to `nest new` (Q16-7). */
16
+ /** `new` only: passed through to `nest new`. */
16
17
  readonly skipGit: boolean;
17
- /** Accepts each unanswered question's default and confirms overwrites (R5). */
18
+ /** Accepts each unanswered question's default and confirms overwrites. */
18
19
  readonly yes: boolean;
19
20
  };
20
21
  export type CliCommand =
@@ -30,7 +31,8 @@ export declare class CliCommandError extends Error {
30
31
  readonly name = "CliCommandError";
31
32
  }
32
33
  /** `aventara <command> --help` (pilot.1): that command's usage alone. */
33
- export declare function commandUsage(command: ScaffoldCommandName): string;
34
- export declare const USAGE: string;
34
+ export declare function commandUsage(command: ScaffoldCommandName, catalog: AdapterCatalog): string;
35
+ /** `aventara --help`: both commands, each flag with the values `catalog` offers. */
36
+ export declare function usage(catalog: AdapterCatalog): string;
35
37
  /** @throws CliCommandError when `argv` is not a command this CLI has. */
36
38
  export declare function parseCliCommand(argv: readonly string[]): CliCommand;
@@ -1,9 +1,7 @@
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. */
1
+ import { INIT_FLAG_QUESTIONS, NEW_QUESTIONS, ormAliasesSentence, } from "../wizard/wizard.questions.js";
3
2
  export class CliCommandError extends Error {
4
3
  name = "CliCommandError";
5
4
  }
6
- /** The options that are not wizard questions. */
7
5
  const OPTIONS = [
8
6
  {
9
7
  flag: "--skip-install",
@@ -30,12 +28,18 @@ const QUESTIONS = {
30
28
  init: INIT_FLAG_QUESTIONS,
31
29
  };
32
30
  const HELP = "run `aventara --help` for usage";
33
- function usageLines(command) {
31
+ function flagUsage(question, catalog) {
32
+ const accepted = question.accepts?.(catalog);
33
+ return `${question.flag} ${accepted === undefined
34
+ ? (question.placeholder ?? "<value>")
35
+ : `<${accepted.join("|")}>`}`;
36
+ }
37
+ function usageLines(command, catalog) {
34
38
  const rows = [
35
39
  ...QUESTIONS[command]
36
40
  .filter((question) => question.flag.startsWith("--"))
37
41
  .map((question) => [
38
- `${question.flag} ${question.placeholder ?? "<value>"}`,
42
+ flagUsage(question, catalog),
39
43
  `${question.label}; asked when omitted`,
40
44
  ]),
41
45
  ...OPTIONS.filter((option) => option.commands.includes(command)).map((option) => [
@@ -48,7 +52,6 @@ function usageLines(command) {
48
52
  .map(([left, right]) => ` ${left.padEnd(width)} ${right}`)
49
53
  .join("\n");
50
54
  }
51
- /** What each command does, in one line: the top-level usage and its own. */
52
55
  const SUMMARY = {
53
56
  new: "Create a NestJS project with Aventara (nest new, then init).",
54
57
  init: "Add Aventara to the NestJS 12 project in this directory.",
@@ -57,35 +60,38 @@ const SYNOPSIS = {
57
60
  new: "aventara new <name> [options]",
58
61
  init: "aventara init [options]",
59
62
  };
60
- const ASKED = `On a terminal every unanswered question is asked; anywhere else, pass its flag
63
+ function asked(catalog) {
64
+ return `${ormAliasesSentence(catalog)}.
65
+ On a terminal every unanswered question is asked; anywhere else, pass its flag
61
66
  or --yes, or the run stops before writing anything.
62
67
  `;
63
- /** `aventara <command> --help` (pilot.1): that command's usage alone. */
64
- export function commandUsage(command) {
68
+ }
69
+ export function commandUsage(command, catalog) {
65
70
  return `Usage: ${SYNOPSIS[command]}
66
71
 
67
72
  ${SUMMARY[command]}
68
73
 
69
74
  Options:
70
- ${usageLines(command)}
75
+ ${usageLines(command, catalog)}
71
76
 
72
- ${ASKED}`;
77
+ ${asked(catalog)}`;
73
78
  }
74
- export const USAGE = `aventara — scaffold an Aventara server on NestJS.
79
+ export function usage(catalog) {
80
+ return `aventara — scaffold an Aventara server on NestJS.
75
81
 
76
82
  Usage:
77
83
  ${SYNOPSIS.new} ${SUMMARY.new}
78
- ${usageLines("new")}
84
+ ${usageLines("new", catalog)}
79
85
 
80
86
  ${SYNOPSIS.init} ${SUMMARY.init}
81
- ${usageLines("init")}
87
+ ${usageLines("init", catalog)}
82
88
 
83
89
  aventara --help Print this and exit 0.
84
90
  aventara <command> --help Print that command's usage and exit 0.
85
- aventara --version Print this CLI's version and exit 0.
91
+ aventara --version, -v Print this CLI's version and exit 0.
86
92
 
87
- ${ASKED}`;
88
- /** @throws CliCommandError when `argv` is not a command this CLI has. */
93
+ ${asked(catalog)}`;
94
+ }
89
95
  export function parseCliCommand(argv) {
90
96
  const [command, ...rest] = argv;
91
97
  if (command === undefined) {
@@ -94,7 +100,7 @@ export function parseCliCommand(argv) {
94
100
  if (command === "--help" || command === "-h") {
95
101
  return { command: "help" };
96
102
  }
97
- if (command === "--version") {
103
+ if (command === "--version" || command === "-v") {
98
104
  return { command: "version" };
99
105
  }
100
106
  if (command !== "new" && command !== "init") {
@@ -1,4 +1,3 @@
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
1
  import { readFileSync } from "fs";
3
2
  function versionOf(text) {
4
3
  const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(text);
@@ -16,11 +15,6 @@ function compareVersions(left, right) {
16
15
  }
17
16
  return 0;
18
17
  }
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
18
  function admits(range, version) {
25
19
  return range.split("||").some((part) => {
26
20
  const comparator = /^\s*(\^|>=)(\d+\.\d+\.\d+)\s*$/.exec(part);
@@ -32,17 +26,11 @@ function admits(range, version) {
32
26
  (comparator[1] === ">=" || version[0] === floor[0]));
33
27
  });
34
28
  }
35
- /** The sentence a bin answers on `version`, or `undefined` when `range` admits it. */
36
29
  export function nodeVersionRefusal(bin, packageName, range, version) {
37
30
  return admits(range, versionOf(version))
38
31
  ? undefined
39
32
  : `${bin}: Node ${version} is not supported; ${packageName} needs Node ${range}.`;
40
33
  }
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
34
  export function refuseUnsupportedNode(bin, manifestUrl) {
47
35
  const manifest = JSON.parse(readFileSync(manifestUrl, "utf8"));
48
36
  const range = manifest.engines === undefined ? undefined : manifest.engines.node;
@@ -3,18 +3,12 @@ import { type ProjectInspection } from "../project/project.inspector.js";
3
3
  import { type OrmScaffold } from "../templates/template.registry.js";
4
4
  import type { PackageManager } from "../wizard/wizard.questions.js";
5
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).
6
+ * Pure apart from reading the files it would replace.
7
+ */
8
+ /**
9
+ * The name of the script that regenerates the ORM client and the discovery
10
+ * artifact.
16
11
  */
17
- /** The name of the script that regenerates the ORM client and the discovery artifact (Q16-22). */
18
12
  export declare const PREPARE_SCRIPT = "aventara:prepare";
19
13
  export type PlannedWrite = {
20
14
  readonly path: string;
@@ -39,12 +33,12 @@ export type PlanInput = {
39
33
  readonly entry: CatalogEntry;
40
34
  readonly scaffold: OrmScaffold;
41
35
  readonly packageManager: PackageManager;
42
- /** This CLI's version: every Aventara package is pinned to it (D1). */
36
+ /** This CLI's version: every Aventara package is pinned to it. */
43
37
  readonly aventaraVersion: string;
44
38
  readonly skipInstall: boolean;
45
39
  /** The current text of a project file, or `undefined` when absent. */
46
40
  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). */
41
+ /** The anchored edit is not attempted; the wiring is printed. */
48
42
  readonly wiringByHand?: string;
49
43
  /** Said before the write, beside the planner's own. */
50
44
  readonly warnings?: readonly string[];
@@ -1,23 +1,10 @@
1
1
  import { spliceAppModule, wiringInstructions, } from "../apply/app-module.anchor.js";
2
2
  import { E2E_SPEC, spliceContractTest } from "../apply/e2e-spec.anchor.js";
3
- import { mergeEnvFile, mergeGitignore, mergePackageManifest, mergePnpmWorkspace, } from "../apply/manifest.merger.js";
3
+ import { CORS_ORIGINS_VARIABLE, MAIN_FILE, mainCorsInstructions, SCAFFOLD_CORS_ORIGINS, spliceMainCors, } from "../apply/main-cors.anchor.js";
4
+ import { appendEnvEntryIfAbsent, mergeEnvFile, mergeGitignore, mergePackageManifest, mergePnpmWorkspace, } from "../apply/manifest.merger.js";
4
5
  import { APP_MODULE, } from "../project/project.inspector.js";
5
6
  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
7
  export const PREPARE_SCRIPT = "aventara:prepare";
20
- /** D3: `nest new`'s start scripts (B1), and what each becomes so `.env` reaches the server. */
21
8
  const START_SCRIPTS = {
22
9
  start: { from: "nest start", to: "nest start --env-file .env" },
23
10
  "start:dev": {
@@ -33,11 +20,6 @@ const START_SCRIPTS = {
33
20
  to: "node --env-file-if-exists=.env dist/main",
34
21
  },
35
22
  };
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
23
  const E2E_SCRIPTS = {
42
24
  esm: {
43
25
  from: "vitest run --config ./vitest.config.e2e.ts",
@@ -61,12 +43,6 @@ function writeOf(path, before, after) {
61
43
  },
62
44
  ];
63
45
  }
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
46
  export function frontendInitCommand(packageManager, cliVersion) {
71
47
  const tag = /^\d+\.\d+\.\d+-([0-9A-Za-z-]+)/.exec(cliVersion)?.[1];
72
48
  const runner = packageManager === "pnpm" ? "pnpm dlx" : "npx";
@@ -87,7 +63,6 @@ export function planInit(input) {
87
63
  keeping.push(...writeOf(path, before, kept.text));
88
64
  replacing.push(...writeOf(path, before, replaced.text));
89
65
  };
90
- // The ORM's new files: identical content is no change (P3), other content a conflict.
91
66
  for (const file of scaffold.files) {
92
67
  const before = input.read(file.path);
93
68
  if (before === undefined) {
@@ -111,7 +86,6 @@ export function planInit(input) {
111
86
  });
112
87
  }
113
88
  }
114
- // The one edit to developer code (R2).
115
89
  const wiring = {
116
90
  imports: [
117
91
  `import { AventaraModule } from '${NEST_HOST}';`,
@@ -132,8 +106,6 @@ export function planInit(input) {
132
106
  if (edit.kind === "edited") {
133
107
  keeping.push(...writeOf(APP_MODULE, appModule, edit.text));
134
108
  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
109
  const spec = input.read(E2E_SPEC);
138
110
  const tested = spec === undefined
139
111
  ? undefined
@@ -147,6 +119,31 @@ export function planInit(input) {
147
119
  instructions = wiringInstructions(wiring);
148
120
  warnings.push(`${APP_MODULE} was not edited: ${edit.reason}; add AventaraModule by hand, as printed below`);
149
121
  }
122
+ const main = input.read(MAIN_FILE);
123
+ const cors = main === undefined
124
+ ? { kind: "not-anchored", reason: `there is no ${MAIN_FILE}` }
125
+ : spliceMainCors(main);
126
+ if (cors.kind === "edited") {
127
+ keeping.push(...writeOf(MAIN_FILE, main, cors.text));
128
+ replacing.push(...writeOf(MAIN_FILE, main, cors.text));
129
+ }
130
+ else if (cors.kind === "configured") {
131
+ warnings.push(`${MAIN_FILE} already configures CORS, so it was left as it is; a browser frontend on another origin is answered only if that configuration allows it`);
132
+ }
133
+ else if (cors.kind === "not-anchored") {
134
+ instructions = [instructions, mainCorsInstructions()]
135
+ .filter((text) => text !== undefined)
136
+ .join("\n");
137
+ warnings.push(`${MAIN_FILE} was not edited: ${cors.reason}; add CORS by hand, as printed below`);
138
+ }
139
+ const corsWanted = cors.kind !== "configured";
140
+ const scaffoldWritesEnv = Object.keys(scaffold.env).length > 0;
141
+ const env = input.read(".env");
142
+ const corsStep = corsWanted && !scaffoldWritesEnv && env === undefined
143
+ ? [
144
+ `Let a browser frontend call the server: add ${CORS_ORIGINS_VARIABLE}="${SCAFFOLD_CORS_ORIGINS}" to .env (unset, CORS stays off)`,
145
+ ]
146
+ : [];
150
147
  const versions = {
151
148
  [CORE]: input.aventaraVersion,
152
149
  [NEST_HOST]: input.aventaraVersion,
@@ -157,7 +154,6 @@ export function planInit(input) {
157
154
  devDependencies: scaffold.devDependencies,
158
155
  scripts: {
159
156
  [PREPARE_SCRIPT]: scaffold.prepare,
160
- // D2: the generated trees are ignored and rebuilt on every install.
161
157
  postinstall: `${packageManager} run ${PREPARE_SCRIPT}`,
162
158
  },
163
159
  rewrites: inspection.usesNestConfig
@@ -167,12 +163,24 @@ export function planInit(input) {
167
163
  "test:e2e": E2E_SCRIPTS[inspection.moduleKind],
168
164
  },
169
165
  }, 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));
166
+ if (scaffoldWritesEnv || (corsWanted && env !== undefined)) {
167
+ both(".env", env, (replace) => {
168
+ const merged = scaffoldWritesEnv
169
+ ? mergeEnvFile(env, scaffold.env, replace)
170
+ : { text: env ?? "", conflicts: [] };
171
+ return corsWanted
172
+ ? {
173
+ text: appendEnvEntryIfAbsent(merged.text, CORS_ORIGINS_VARIABLE, SCAFFOLD_CORS_ORIGINS),
174
+ conflicts: merged.conflicts,
175
+ }
176
+ : merged;
177
+ });
173
178
  }
174
179
  const gitignore = input.read(".gitignore");
175
- both(".gitignore", gitignore, () => mergeGitignore(gitignore, scaffold.gitignore));
180
+ both(".gitignore", gitignore, () => mergeGitignore(gitignore, [
181
+ ...scaffold.gitignore,
182
+ ...(scaffoldWritesEnv ? [".env"] : []),
183
+ ]));
176
184
  if (packageManager === "pnpm" && scaffold.nativeBuilds.length > 0) {
177
185
  const workspace = input.read("pnpm-workspace.yaml");
178
186
  both("pnpm-workspace.yaml", workspace, () => mergePnpmWorkspace(workspace, scaffold.nativeBuilds));
@@ -194,6 +202,7 @@ export function planInit(input) {
194
202
  "The start scripts were left as they are: @nestjs/config loads .env, so make sure it does before Prisma connects.",
195
203
  ]
196
204
  : []),
205
+ ...corsStep,
197
206
  `Start the server: ${run} run start:dev`,
198
207
  `In your frontend: ${frontendInitCommand(packageManager, input.aventaraVersion)}`,
199
208
  ],
@@ -1,11 +1,4 @@
1
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
2
  export declare function detectPackageManager(signals: {
10
3
  readonly lockfile?: PackageManager | undefined;
11
4
  readonly userAgent?: string | undefined;
@@ -1,10 +1,3 @@
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
1
  export function detectPackageManager(signals) {
9
2
  if (signals.lockfile !== undefined) {
10
3
  return signals.lockfile;
@@ -2,17 +2,11 @@ import type { PackageManifest } from "../apply/manifest.merger.js";
2
2
  import type { AdapterCatalog, CatalogEntry } from "../catalog/catalog-entry.interface.js";
3
3
  import type { ModuleKind } from "../templates/template.registry.js";
4
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
5
  /** The project is not one `aventara init` serves. A refusal: one sentence, nothing written. */
12
6
  export declare class ProjectRefusedError extends Error {
13
7
  readonly name = "ProjectRefusedError";
14
8
  }
15
- /** The ORM a project already has, by the catalog's family packages (R14, D10). */
9
+ /** The ORM a project already has, by the catalog's family packages. */
16
10
  export type OrmPresence = {
17
11
  readonly kind: "absent";
18
12
  } | {
@@ -30,9 +24,9 @@ export type ProjectInspection = {
30
24
  /** The package manager the lockfile names, if exactly one does. */
31
25
  readonly lockfile: PackageManager | undefined;
32
26
  readonly orm: OrmPresence;
33
- /** `@nestjs/config` loads `.env` itself, so the start scripts are left alone (P5). */
27
+ /** `@nestjs/config` loads `.env` itself, so the start scripts are left alone. */
34
28
  readonly usesNestConfig: boolean;
35
29
  };
36
- /** The file the anchored edit opens (R2). */
30
+ /** The file the anchored edit opens. */
37
31
  export declare const APP_MODULE = "src/app.module.ts";
38
32
  export declare function inspectProject(directory: string, catalog: AdapterCatalog): ProjectInspection;
@@ -1,21 +1,11 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import path from "node:path";
3
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
4
  export class ProjectRefusedError extends Error {
12
5
  name = "ProjectRefusedError";
13
6
  }
14
- /** Nest 12 only (Phase 11 Q16). */
15
7
  const NEST_MAJOR = 12;
16
- /** D4: TypeScript 7 has no classic compiler API. */
17
8
  const FIRST_UNSUPPORTED_TYPESCRIPT = 7;
18
- /** The file the anchored edit opens (R2). */
19
9
  export const APP_MODULE = "src/app.module.ts";
20
10
  const LOCKFILES = [
21
11
  ["package-lock.json", "npm"],
@@ -1,15 +1,10 @@
1
1
  /**
2
- * R6 — finding the developer's own ORM service, lexically, over `src/**\/*.ts`:
3
- * no compiler (`aventara init` may run before anything is installed).
4
- *
5
- * 1. **Candidates**: exported classes that `extends` the ORM's client class,
6
- * where the file imports that class from a specifier the ORM's templates
7
- * recognise (the generated client, or the ORM package).
8
- * 2. **Compositions**: exported classes that hold a client instead of extending
9
- * one (`client = new PrismaClient(…)`, Phase 11 S7's shape) — reported with
10
- * the member, never wired.
2
+ * 1. **Candidates**: exported classes that `extends` the ORM's client class, where
3
+ * the file imports that class from a specifier the ORM's templates recognise
4
+ * (the generated client, or the ORM package).
11
5
  * 3. **Modules**: each `@Module({…})`-decorated exported class with the
12
- * identifiers its `providers` and `exports` list, and whether it is `@Global()`.
6
+ * identifiers its `providers` and `exports` list, and whether it is
7
+ * `@Global()`.
13
8
  *
14
9
  * Which of them is *the* service is the caller's question (the wizard's).
15
10
  */
@@ -18,7 +18,6 @@ function sourceFiles(directory, root, skip) {
18
18
  : [];
19
19
  });
20
20
  }
21
- /** The identifiers the array under `key`, at `object`'s own level, names (`providers: [A, B]`). */
22
21
  function arrayNames(text, code, object, key) {
23
22
  const end = closing(text, code, object);
24
23
  let depth = 0;
@@ -99,7 +98,6 @@ export function scanServices(directory, shape) {
99
98
  }
100
99
  return { candidates, compositions, modules };
101
100
  }
102
- /** The modules that make `service` injectable elsewhere: providing and exporting it, or global. */
103
101
  export function modulesProviding(scan, service) {
104
102
  return scan.modules.filter((module) => module.providers.includes(service.exportName) &&
105
103
  (module.exports.includes(service.exportName) || module.global));
@@ -1,15 +1,5 @@
1
- /**
2
- * A lexical view of TypeScript source — enough to find decorators, classes,
3
- * imports and brackets **in code**, never inside a comment, a string or a
4
- * template literal's text — with no compiler: `aventara new` runs before
5
- * anything (TypeScript included) is installed in the project, and the CLI
6
- * carries no TypeScript of its own. Shared by the anchored `app.module.ts` edit
7
- * and the detection of a developer's ORM service.
8
- */
9
- /** For each character: is it code (not inside a comment, string or template text)? */
10
1
  export function codeMask(text) {
11
2
  const code = new Array(text.length).fill(true);
12
- /** Open template literals' brace depths: `${` re-enters code until its `}`. */
13
3
  const templates = [];
14
4
  let braces = 0;
15
5
  let index = 0;
@@ -19,7 +9,6 @@ export function codeMask(text) {
19
9
  }
20
10
  };
21
11
  const templateText = (from) => {
22
- // Scans template text from `from` until "`" (closes) or "${" (re-enters code).
23
12
  let at = from;
24
13
  while (at < text.length) {
25
14
  if (text[at] === "\\") {
@@ -88,7 +77,6 @@ export function codeMask(text) {
88
77
  }
89
78
  return code;
90
79
  }
91
- /** Every index where `token` starts in code. */
92
80
  export function codeIndexes(text, code, token) {
93
81
  const found = [];
94
82
  for (const match of text.matchAll(token)) {
@@ -103,7 +91,6 @@ export const OPENERS = {
103
91
  "[": "]",
104
92
  "{": "}",
105
93
  };
106
- /** The index of the bracket closing the one at `open`, counting code brackets only. */
107
94
  export function closing(text, code, open) {
108
95
  const stack = [];
109
96
  for (let at = open; at < text.length; at += 1) {
@@ -1,8 +1,8 @@
1
1
  /**
2
- * The one external process seam (§4.2): a command, its arguments, a working
3
- * directory. Its stdout is always **piped**, never inherited — `nest new` asks a
4
- * question it has no flag for when its stdout is a terminal (B25) — and both
5
- * streams are collected, so a failure can be reported with what it said.
2
+ * The one external process seam: a command, its arguments, a working directory.
3
+ * Its stdout is always **piped**, never inherited — `nest new` asks a question it
4
+ * has no flag for when its stdout is a terminal — and both streams are collected,
5
+ * so a failure can be reported with what it said.
6
6
  */
7
7
  export type CommandResult = {
8
8
  readonly code: number;
@@ -3,11 +3,9 @@ import type { ScaffoldCommand } from "../command/command.parser.js";
3
3
  import { type Prompter } from "../wizard/answer.resolver.js";
4
4
  import type { CommandRunner } from "./command.runner.js";
5
5
  /**
6
- * `aventara init` in §4.1's order — inspect, ask, plan, confirm, and only then
7
- * write and install. Every step before the write reads; so a refusal anywhere
8
- * before it leaves the project exactly as it was. A failure after it (the
9
- * install) names the command, its exit code and what to re-run; nothing is
10
- * rolled back.
6
+ * Every step before the write reads; so a refusal anywhere before it leaves the
7
+ * project exactly as it was. A failure after it (the install) names the command,
8
+ * its exit code and what to re-run; nothing is rolled back.
11
9
  */
12
10
  export type InitIo = {
13
11
  readonly directory: string;
@@ -17,7 +15,7 @@ export type InitIo = {
17
15
  readonly prompter: Prompter;
18
16
  readonly runner: CommandRunner;
19
17
  readonly catalog: AdapterCatalog;
20
- /** This CLI's own version (D1). */
18
+ /** This CLI's own version. */
21
19
  readonly aventaraVersion: string;
22
20
  /** Next steps that come before init's own (`new`'s `cd <name>`). */
23
21
  readonly firstSteps?: readonly string[];