@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
package/LICENSE ADDED
@@ -0,0 +1,91 @@
1
+ Required Notice: Copyright 2026 Mohamed Ragheb
2
+
3
+ # PolyForm Shield License 1.0.0
4
+
5
+ <https://polyformproject.org/licenses/shield/1.0.0>
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
+
15
+ ## Distribution License
16
+
17
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
+
19
+ ## Notices
20
+
21
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
+
23
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
+
25
+ ## Changes and New Works License
26
+
27
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
+
29
+ ## Patent License
30
+
31
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
+
33
+ ## Noncompete
34
+
35
+ Any purpose is a permitted purpose, except for providing any product that competes with the software or any product the licensor or any of its affiliates provides using the software.
36
+
37
+ ## Competition
38
+
39
+ Goods and services compete even when they provide functionality through different kinds of interfaces or for different technical platforms. Applications can compete with services, libraries with plugins, frameworks with development tools, and so on, even if they're written in different programming languages or for different computer architectures. Goods and services compete even when provided free of charge. If you market a product as a practical substitute for the software or another product, it definitely competes.
40
+
41
+ ## New Products
42
+
43
+ If you are using the software to provide a product that does not compete, but the licensor or any of its affiliates brings your product into competition by providing a new version of the software or another product using the software, you may continue using versions of the software available under these terms beforehand to provide your competing product, but not any later versions.
44
+
45
+ ## Discontinued Products
46
+
47
+ You may begin using the software to compete with a product or service that the licensor or any of its affiliates has stopped providing, unless the licensor includes a plain-text line beginning with `Licensor Line of Business:` with the software that mentions that line of business. For example:
48
+
49
+ > Licensor Line of Business: YoyodyneCMS Content Management System (http://example.com/cms)
50
+
51
+ ## Sales of Business
52
+
53
+ If the licensor or any of its affiliates sells a line of business developing the software or using the software to provide a product, the buyer can also enforce [Noncompete](#noncompete) for that product.
54
+
55
+ ## Fair Use
56
+
57
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
58
+
59
+ ## No Other Rights
60
+
61
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
62
+
63
+ ## Patent Defense
64
+
65
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
66
+
67
+ ## Violations
68
+
69
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
70
+
71
+ ## No Liability
72
+
73
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
74
+
75
+ ## Definitions
76
+
77
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
78
+
79
+ A **product** can be a good or service, or a combination of them.
80
+
81
+ **You** refers to the individual or entity agreeing to these terms.
82
+
83
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all its affiliates.
84
+
85
+ **Affiliates** means the other organizations than an organization has control over, is under the control of, or is under common control with.
86
+
87
+ **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
88
+
89
+ **Your licenses** are all the licenses granted to you for the software under these terms.
90
+
91
+ **Use** means anything you do with the software requiring one of your licenses.
@@ -0,0 +1,9 @@
1
+ # Additional Permission — Aventara
2
+
3
+ Copyright 2026 Mohamed Ragheb
4
+
5
+ In addition to the permissions of the PolyForm Shield License 1.0.0 in `LICENSE`, the licensor grants the following additional permission:
6
+
7
+ The Noncompete section applies only to providing a product that competes with the software itself — the Aventara framework, its packages and its tooling. Providing a product that competes with any other product the licensor or its affiliates provide using the software is a permitted purpose.
8
+
9
+ This additional permission only adds to your permissions; it does not restrict or replace any term of `LICENSE`.
package/README.md CHANGED
@@ -1,3 +1,121 @@
1
- # Temporary Holding Version
1
+ # `@aventara/cli`
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The `aventara` command: it scaffolds an Aventara server on NestJS 12.
4
+
5
+ ```bash
6
+ npm i -g @aventara/cli@pilot # or run it once: npx @aventara/cli@pilot <command>
7
+ aventara new my-api # a new NestJS project with Aventara
8
+ aventara init # Aventara added to the NestJS 12 project in this directory
9
+ ```
10
+
11
+ It is run once, globally or through `npx`; it is not a dependency of the projects it writes. The frontend's side —
12
+ `framework.client.mts` and the generated client — is `@aventara/client`'s (`npx @aventara/client@pilot init`).
13
+
14
+ ## `aventara new <name>`
15
+
16
+ Runs the pinned `nest new` (`npx -y @nestjs/cli@12.0.8 new …`, or `pnpm dlx …` under pnpm), checks that its
17
+ `src/app.module.ts` is what that version writes, then runs exactly `aventara init`'s pipeline over the new project and
18
+ installs it. A directory that exists and is not empty is refused before anything runs.
19
+
20
+ ## `aventara init`
21
+
22
+ Runs inside an existing **NestJS 12** project (refused: no `package.json`, another Nest major, TypeScript 7, a yarn-only
23
+ lockfile, a project already initialized).
24
+
25
+ - **No ORM yet:** the ORM and the database are asked (or taken from the flags), and everything below is written.
26
+ - **Prisma already there:** the installed Prisma must be one an Aventara adapter supports — today Prisma 7, `^7.10.0`,
27
+ SQLite or PostgreSQL, a driver adapter, the `prisma-client` generator — or the run is refused in one sentence, with
28
+ nothing written. Your schema, Prisma config, `.env` and service are **reused and never written**; init finds the class
29
+ extending `PrismaClient` and the module exporting it, or asks.
30
+
31
+ ### Questions and flags
32
+
33
+ Every question has a flag. On a terminal, unanswered questions are asked; anywhere else (CI, a script) pass the flags
34
+ or `--yes`, or the run stops in one sentence naming what is missing, before writing anything. A question with one
35
+ possible answer is shown, not asked.
36
+
37
+ | Flag | Question | Default |
38
+ |---|---|---|
39
+ | `<name>` (`new` only) | The project directory. | — (required) |
40
+ | `--orm <family>@<major>` | The ORM, from the adapters that exist (`prisma@7`; `prisma` = its newest). | the only one there is |
41
+ | `--db <provider>` | The database: the adapter's providers (`sqlite`, `postgresql`). | `sqlite` |
42
+ | `--package-manager <npm\|pnpm>` | — | the lockfile's, else the one that launched the CLI, else npm |
43
+ | `--prisma-service <path#Export>` (`init`) | Your Prisma service, when more than one, or none, is found. | the one found; none → write `src/prisma.service.ts` |
44
+ | `--prisma-module <path#Export>` (`init`) | The Nest module that provides and exports it. | the one found |
45
+ | `--skip-install` | Write the files; print the install instead of running it. | |
46
+ | `--skip-git` (`new`) | Passed to `nest new`. | |
47
+ | `-y`, `--yes` | Accept every unanswered default **and** replace existing content that differs. | |
48
+
49
+ Existing content that differs from what would be written (a `.env` key, a script, a file) is listed, and replaced only
50
+ when you confirm, or with `--yes`; where nobody can be asked, the run stops and touches nothing.
51
+
52
+ ### What it writes (no ORM yet, SQLite, npm)
53
+
54
+ | Path | Owner afterwards |
55
+ |---|---|
56
+ | `prisma/schema.prisma` — `prisma-client` generating into `src/generated/prisma`, a `User` model | you |
57
+ | `prisma.config.ts` — loads `.env` itself (Prisma does not) | you |
58
+ | `src/prisma.service.ts` — `PrismaService` extending the client with the driver adapter, refusing a missing `DATABASE_URL` before the server listens; `PrismaModule` | you |
59
+ | `src/aventara.config.ts` — `aventaraConfig(prisma)`: the entrypoint (`/api`), and the restrictions and pipelines you add | you |
60
+ | `src/app.module.ts` — one edit: the imports, `PrismaModule` and `AventaraModule.forRootAsync({ … })`; printed instead when the file is not the shape expected | you |
61
+ | `test/app.e2e-spec.ts` — one edit: a `GET /api/_contract` → 200 test after Nest's `GET /` test; left alone when the file is not Nest's | you |
62
+ | `.env` (`DATABASE_URL`), `.gitignore` (`/src/generated/`, `/dev.db*`) | you |
63
+ | `package.json` — `@aventara/*` at this CLI's version, `prisma`, `@prisma/client` and the driver at `7.10.0`; `aventara:prepare`, `postinstall`, and the `.env` on the start scripts and `test:e2e` (`--env-file`, `--env-file-if-exists`) | you |
64
+ | `pnpm-workspace.yaml` (pnpm only) — `allowBuilds` for Prisma's and SQLite's install scripts | you |
65
+ | `src/generated/prisma/**`, `src/generated/aventara/discovery.artifact.ts` | **generated** by `aventara:prepare` (run on every install); never edit |
66
+
67
+ ### Next steps it prints
68
+
69
+ ```bash
70
+ npx prisma db push # create the tables (init never touches a database); pnpm: pnpm exec prisma db push
71
+ npm run start:dev # GET http://localhost:3000/api/_contract
72
+ npx @aventara/client@pilot init # in your frontend; pnpm: pnpm dlx @aventara/client@pilot init
73
+ ```
74
+
75
+ The frontend command is the package manager's own runner (`npx`, or `pnpm dlx` under pnpm) and, while the CLI is a
76
+ prerelease, carries its dist-tag (`@pilot`), so the client comes from the same release.
77
+
78
+ With PostgreSQL, set `DATABASE_URL` in `.env` first (init writes Prisma's placeholder, and never asks for a secret).
79
+
80
+ ### Calling it by hand
81
+
82
+ With the server running, the starter `User` model answers at `/api`:
83
+
84
+ <!-- pilot-gate:curl -->
85
+ ```bash
86
+ # 1. The contract. Its protocol.hash is what every resource request sends as Aventara-Contract-Hash.
87
+ HASH=$(curl -s http://localhost:3000/api/_contract | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).protocol.hash')
88
+
89
+ # 2. find.many on User, with both identity headers.
90
+ curl -s -X POST http://localhost:3000/api/_resources/User/find/many \
91
+ -H 'Content-Type: application/json' \
92
+ -H 'Aventara-Protocol-Version: 1' \
93
+ -H "Aventara-Contract-Hash: $HASH" \
94
+ -d '{}'
95
+ # {"data":[],"code":"A1000","cause":null}
96
+ ```
97
+ <!-- /pilot-gate:curl -->
98
+
99
+ Leave out an identity header and the answer is `400 A2000`, naming the header that is missing. The hash changes
100
+ whenever the schema or the configuration does: fetch it again, and regenerate the client.
101
+
102
+ - **Resource keys are the model names, as written.** `model User` is `User` everywhere: on the wire
103
+ (`/_resources/User/find/many`) and in the generated client (`avClient.User.find.many({})`). Nothing is renamed,
104
+ lower-cased or pluralized.
105
+ - **Results are read-only.** A list result is a `readonly` array: type it `readonly User[]`, not `User[]` —
106
+ `const users: readonly User[] = await avClient.User.find.many({});`.
107
+
108
+ ## Supported
109
+
110
+ NestJS 12; Node `^22.18.0 || >=24.2.0` (measured; on any other Node the bin refuses in one sentence naming that range;
111
+ on Node 22 use npm ≥ 11 — Node 22's bundled npm 10 cannot install Nest 12's own scaffold; a CommonJS project's jest
112
+ e2e needs Node ≥ 24.9, Nest's and Jest's limit); npm and pnpm (yarn is not supported); ESM and
113
+ CommonJS projects; Prisma 7 (`^7.10.0`) on SQLite and PostgreSQL. The ORMs, majors, databases and drivers on offer are
114
+ **generated** from the Aventara adapter packages that exist (`src/catalog/adapter.catalog.generated.ts`), never listed
115
+ by hand: a future
116
+ `@aventara/prisma8-adapter` appears by existing.
117
+
118
+ ## License
119
+
120
+ PolyForm Shield 1.0.0 with an additional permission — free to use, including in commercial applications; you may not
121
+ use it to build a product that competes with Aventara itself. See `LICENSE` and `LICENSE-ADDITIONAL-PERMISSION.md`.
@@ -0,0 +1,30 @@
1
+ /**
2
+ * R2 — the one edit `aventara init` makes to code the developer owns:
3
+ * `src/app.module.ts` gains import lines and `imports:` entries for
4
+ * `AventaraModule`, spliced at an **anchor** — the `imports: [ … ]` array of the
5
+ * one `@Module({ … })` that decorates `AppModule`.
6
+ *
7
+ * The scan is string- and comment-aware (a `@Module(` or an `imports: [` inside
8
+ * a comment, a string or a template literal is not code), and it never
9
+ * reformats: everything outside the two insertion points keeps its bytes. When
10
+ * the anchor is not found exactly once — two `@Module`s, a computed `imports`,
11
+ * no `imports` at all — nothing is edited and the caller prints the lines to
12
+ * add instead.
13
+ */
14
+ export type AppModuleWiring = {
15
+ /** Complete import declarations, one per line. */
16
+ readonly imports: readonly string[];
17
+ /** `imports:` entries, each possibly multi-line, each ending with a comma. */
18
+ readonly entries: readonly string[];
19
+ };
20
+ export type AppModuleEdit = {
21
+ readonly kind: "edited";
22
+ readonly text: string;
23
+ } | {
24
+ readonly kind: "not-anchored";
25
+ readonly reason: string;
26
+ };
27
+ /** Splices `wiring` into `text` at the anchor, or says why there is no anchor. */
28
+ export declare function spliceAppModule(text: string, wiring: AppModuleWiring): AppModuleEdit;
29
+ /** What to add by hand when there is no anchor. */
30
+ export declare function wiringInstructions(wiring: AppModuleWiring): string;
@@ -0,0 +1,185 @@
1
+ /**
2
+ * R2 — the one edit `aventara init` makes to code the developer owns:
3
+ * `src/app.module.ts` gains import lines and `imports:` entries for
4
+ * `AventaraModule`, spliced at an **anchor** — the `imports: [ … ]` array of the
5
+ * one `@Module({ … })` that decorates `AppModule`.
6
+ *
7
+ * The scan is string- and comment-aware (a `@Module(` or an `imports: [` inside
8
+ * a comment, a string or a template literal is not code), and it never
9
+ * reformats: everything outside the two insertion points keeps its bytes. When
10
+ * the anchor is not found exactly once — two `@Module`s, a computed `imports`,
11
+ * no `imports` at all — nothing is edited and the caller prints the lines to
12
+ * add instead.
13
+ */
14
+ import { closing, codeIndexes, codeMask, nextCode, OPENERS, } from "../project/source.scanner.js";
15
+ function indentOf(text, at) {
16
+ const lineStart = text.lastIndexOf("\n", at - 1) + 1;
17
+ return /^[ \t]*/.exec(text.slice(lineStart))?.[0] ?? "";
18
+ }
19
+ function indented(block, indent) {
20
+ return block
21
+ .split("\n")
22
+ .map((line) => (line === "" ? line : `${indent}${line}`))
23
+ .join("\n");
24
+ }
25
+ /** The local names a file's import declarations bind. */
26
+ function importedNames(text, code) {
27
+ const names = new Set();
28
+ for (const at of codeIndexes(text, code, /^import\b/gm)) {
29
+ const end = text.indexOf(" from ", at);
30
+ if (end === -1) {
31
+ continue;
32
+ }
33
+ const clause = text
34
+ .slice(at + "import".length, end)
35
+ .replace(/\btype\b/g, "");
36
+ for (const part of clause.replace(/[{}]/g, ",").split(",")) {
37
+ const local = part
38
+ .trim()
39
+ .split(/\s+as\s+/)
40
+ .pop()
41
+ ?.trim();
42
+ if (local !== undefined && /^[A-Za-z_$][\w$]*$/.test(local)) {
43
+ names.add(local);
44
+ }
45
+ }
46
+ }
47
+ return names;
48
+ }
49
+ /** Splices `wiring` into `text` at the anchor, or says why there is no anchor. */
50
+ export function spliceAppModule(text, wiring) {
51
+ const code = codeMask(text);
52
+ const decorators = codeIndexes(text, code, /@Module\s*\(/g);
53
+ if (decorators.length !== 1) {
54
+ return {
55
+ kind: "not-anchored",
56
+ reason: `it has ${decorators.length} @Module decorators, not one`,
57
+ };
58
+ }
59
+ const open = text.indexOf("(", decorators[0]);
60
+ const close = closing(text, code, open);
61
+ const after = nextCode(text, code, close + 1);
62
+ if (close === -1 ||
63
+ !/^(?:export\s+)?class\s+AppModule\b/.test(text.slice(after))) {
64
+ return {
65
+ kind: "not-anchored",
66
+ reason: "its @Module decorator does not decorate class AppModule",
67
+ };
68
+ }
69
+ const object = nextCode(text, code, open + 1);
70
+ if (text[object] !== "{") {
71
+ return {
72
+ kind: "not-anchored",
73
+ reason: "@Module is not given an object literal",
74
+ };
75
+ }
76
+ const objectEnd = closing(text, code, object);
77
+ // `imports` at the object's own level: not inside a nested bracket.
78
+ let depth = 0;
79
+ let key = -1;
80
+ for (let at = object + 1; at < objectEnd; at += 1) {
81
+ if (!code[at]) {
82
+ continue;
83
+ }
84
+ const char = text[at];
85
+ if (OPENERS[char] !== undefined) {
86
+ depth += 1;
87
+ }
88
+ else if (char === ")" || char === "]" || char === "}") {
89
+ depth -= 1;
90
+ }
91
+ else if (depth === 0 &&
92
+ /^imports\s*:/.test(text.slice(at)) &&
93
+ !/[\w$]/.test(text[at - 1])) {
94
+ if (key !== -1) {
95
+ return { kind: "not-anchored", reason: "it has two imports keys" };
96
+ }
97
+ key = at;
98
+ }
99
+ }
100
+ if (key === -1) {
101
+ return {
102
+ kind: "not-anchored",
103
+ reason: "AppModule's @Module has no imports array",
104
+ };
105
+ }
106
+ const array = nextCode(text, code, text.indexOf(":", key) + 1);
107
+ if (text[array] !== "[") {
108
+ return {
109
+ kind: "not-anchored",
110
+ reason: "AppModule's imports is computed, not an array literal",
111
+ };
112
+ }
113
+ const arrayEnd = closing(text, code, array);
114
+ if (arrayEnd === -1) {
115
+ return { kind: "not-anchored", reason: "its imports array does not close" };
116
+ }
117
+ const base = indentOf(text, key);
118
+ const element = `${base} `;
119
+ // An entry the developer already lists (their own `PrismaModule`) is not added twice.
120
+ const listed = text.slice(array + 1, arrayEnd);
121
+ const entries = wiring.entries
122
+ .filter((entry) => {
123
+ const name = /^[A-Za-z_$][\w$]*/.exec(entry)?.[0];
124
+ return (name === undefined ||
125
+ !new RegExp(`(^|[\\s,\\[])${name}\\s*,?\\s*($|\\])`, "m").test(listed));
126
+ })
127
+ .map((entry) => indented(entry, element));
128
+ // The last element needs a trailing comma: placed after its last code
129
+ // character, so a comment after it stays a comment.
130
+ let lastCode = arrayEnd - 1;
131
+ while (lastCode > array &&
132
+ (!code[lastCode] || /\s/.test(text[lastCode]))) {
133
+ lastCode -= 1;
134
+ }
135
+ const content = lastCode === array || text[lastCode] === ","
136
+ ? text.slice(array + 1, arrayEnd)
137
+ : `${text.slice(array + 1, lastCode + 1)},${text.slice(lastCode + 1, arrayEnd)}`;
138
+ const existing = content.trim();
139
+ const lines = [
140
+ ...(existing === "" ? [] : [`${element}${existing}`]),
141
+ ...entries,
142
+ ];
143
+ const spliced = `[\n${lines.join("\n")}\n${base}]`;
144
+ // The import lines go after the last import declaration.
145
+ const declarations = codeIndexes(text, code, /^import\b/gm);
146
+ let insertAt = 0;
147
+ for (const declaration of declarations) {
148
+ const end = text.indexOf(";", declaration);
149
+ const lineEnd = text.indexOf("\n", end === -1 ? declaration : end);
150
+ insertAt = lineEnd === -1 ? text.length : lineEnd + 1;
151
+ }
152
+ // A name the file already imports is not imported twice: a wiring line keeps
153
+ // only the names still missing, and is dropped when none are.
154
+ const imported = importedNames(text, code);
155
+ const missing = wiring.imports.flatMap((line) => {
156
+ const named = /^import\s+(type\s+)?\{([^}]*)\}(\s*from\s*.*)$/.exec(line);
157
+ if (named === null) {
158
+ return [...importedNames(line, codeMask(line))].some((name) => !imported.has(name))
159
+ ? [line]
160
+ : [];
161
+ }
162
+ const names = named[2]
163
+ .split(",")
164
+ .map((name) => name.trim())
165
+ .filter((name) => name !== "" && !imported.has(name.split(/\s+as\s+/).pop()));
166
+ return names.length === 0
167
+ ? []
168
+ : [`import ${named[1] ?? ""}{ ${names.join(", ")} }${named[3]}`];
169
+ });
170
+ const imports = missing.length === 0 ? "" : `${missing.join("\n")}\n`;
171
+ const edited = text.slice(0, array) + spliced + text.slice(arrayEnd + 1);
172
+ return {
173
+ kind: "edited",
174
+ text: edited.slice(0, insertAt) + imports + edited.slice(insertAt),
175
+ };
176
+ }
177
+ /** What to add by hand when there is no anchor. */
178
+ export function wiringInstructions(wiring) {
179
+ return [
180
+ "Add to src/app.module.ts:",
181
+ ...wiring.imports.map((line) => ` ${line}`),
182
+ "and to AppModule's @Module({ imports: [ … ] }):",
183
+ ...wiring.entries.map((entry) => indented(entry, " ")),
184
+ ].join("\n");
185
+ }
@@ -0,0 +1,24 @@
1
+ import type { Prompter } from "../wizard/answer.resolver.js";
2
+ /**
3
+ * The `generateAt` rule (architect, 2026-10-04; R5 makes it every wizard's
4
+ * conflict rule; P7 — implemented here and in `@aventara/client`, which this
5
+ * package does not import, in the same sentence shape): content the tool did not
6
+ * produce is listed with a warning and replaced only once someone confirms;
7
+ * `--yes` confirms in advance; where nobody can be asked (stdin is not a
8
+ * terminal) the run cancels in one sentence naming `--yes`, exit 1, with
9
+ * nothing touched.
10
+ */
11
+ /** Nobody confirmed replacing existing content. A refusal: nothing was touched. */
12
+ export declare class ConflictsNotConfirmedError extends Error {
13
+ readonly name = "ConflictsNotConfirmedError";
14
+ }
15
+ export declare function conflictWarning(conflicts: readonly string[]): string;
16
+ /**
17
+ * Resolves when `conflicts` may be replaced; throws when they may not.
18
+ *
19
+ * @throws ConflictsNotConfirmedError
20
+ */
21
+ export declare function confirmConflicts(conflicts: readonly string[], consent: {
22
+ readonly yes: boolean;
23
+ readonly prompter: Prompter;
24
+ }): Promise<void>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The `generateAt` rule (architect, 2026-10-04; R5 makes it every wizard's
3
+ * conflict rule; P7 — implemented here and in `@aventara/client`, which this
4
+ * package does not import, in the same sentence shape): content the tool did not
5
+ * produce is listed with a warning and replaced only once someone confirms;
6
+ * `--yes` confirms in advance; where nobody can be asked (stdin is not a
7
+ * terminal) the run cancels in one sentence naming `--yes`, exit 1, with
8
+ * nothing touched.
9
+ */
10
+ /** Nobody confirmed replacing existing content. A refusal: nothing was touched. */
11
+ export class ConflictsNotConfirmedError extends Error {
12
+ name = "ConflictsNotConfirmedError";
13
+ }
14
+ export function conflictWarning(conflicts) {
15
+ return `${conflicts.join(", ")} already ${conflicts.length === 1 ? "has" : "have"} other content, and initializing will replace ${conflicts.length === 1 ? "it" : "them"}`;
16
+ }
17
+ /**
18
+ * Resolves when `conflicts` may be replaced; throws when they may not.
19
+ *
20
+ * @throws ConflictsNotConfirmedError
21
+ */
22
+ export async function confirmConflicts(conflicts, consent) {
23
+ if (conflicts.length === 0 || consent.yes) {
24
+ return;
25
+ }
26
+ const them = conflicts.length === 1 ? "it" : "them";
27
+ if (!consent.prompter.interactive) {
28
+ throw new ConflictsNotConfirmedError(`not initializing, and nothing was touched: ${conflicts.join(", ")} already ${conflicts.length === 1 ? "has" : "have"} other content and nobody can be asked (stdin is not a terminal); run \`aventara init --yes\` to replace ${them}`);
29
+ }
30
+ consent.prompter.say(`aventara: warning: ${conflictWarning(conflicts)}.`);
31
+ const answer = await consent.prompter.ask({
32
+ label: "Replace them and initialize",
33
+ choices: ["y", "N"],
34
+ defaultValue: "N",
35
+ });
36
+ if (!/^y(?:es)?$/i.test(answer.trim())) {
37
+ throw new ConflictsNotConfirmedError(`cancelled, and nothing was touched; run again and answer yes to replace ${them}`);
38
+ }
39
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * pilot.1 — the scaffold's own e2e spec also proves the protocol is mounted.
3
+ *
4
+ * `nest new`'s `test/app.e2e-spec.ts` (`@nestjs/schematics` 12's template, ESM
5
+ * and CommonJS alike) tests `GET /` and nothing else. Like the `app.module.ts`
6
+ * edit, this one is anchored on Nest's exact text: the `/ (GET)` test is found
7
+ * exactly once, and a `GET <entrypoint>/_contract` test is added right after
8
+ * it, through the spec's own `app` and `request`. A spec that is not Nest's —
9
+ * the developer's own — is left as it is, and nothing is said: the test is a
10
+ * convenience, and the developer's tests are theirs.
11
+ */
12
+ /** The e2e spec `nest new` writes. */
13
+ export declare const E2E_SPEC = "test/app.e2e-spec.ts";
14
+ /**
15
+ * `spec` with the contract test after Nest's `GET /` test, or `undefined` when
16
+ * the anchor is not there exactly once, or the contract test already is.
17
+ */
18
+ export declare function spliceContractTest(spec: string, entrypoint: string): string | undefined;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * pilot.1 — the scaffold's own e2e spec also proves the protocol is mounted.
3
+ *
4
+ * `nest new`'s `test/app.e2e-spec.ts` (`@nestjs/schematics` 12's template, ESM
5
+ * and CommonJS alike) tests `GET /` and nothing else. Like the `app.module.ts`
6
+ * edit, this one is anchored on Nest's exact text: the `/ (GET)` test is found
7
+ * exactly once, and a `GET <entrypoint>/_contract` test is added right after
8
+ * it, through the spec's own `app` and `request`. A spec that is not Nest's —
9
+ * the developer's own — is left as it is, and nothing is said: the test is a
10
+ * convenience, and the developer's tests are theirs.
11
+ */
12
+ /** The e2e spec `nest new` writes. */
13
+ export const E2E_SPEC = "test/app.e2e-spec.ts";
14
+ /** `nest new`'s `GET /` test, byte for byte: the anchor. */
15
+ const ROOT_TEST = ` it('/ (GET)', () => {
16
+ return request(app.getHttpServer())
17
+ .get('/')
18
+ .expect(200)
19
+ .expect('Hello World!');
20
+ });
21
+ `;
22
+ /**
23
+ * `spec` with the contract test after Nest's `GET /` test, or `undefined` when
24
+ * the anchor is not there exactly once, or the contract test already is.
25
+ */
26
+ export function spliceContractTest(spec, entrypoint) {
27
+ const path = `${entrypoint}/_contract`;
28
+ const at = spec.indexOf(ROOT_TEST);
29
+ if (at === -1 ||
30
+ spec.indexOf(ROOT_TEST, at + 1) !== -1 ||
31
+ spec.includes(`'${path}'`)) {
32
+ return undefined;
33
+ }
34
+ const end = at + ROOT_TEST.length;
35
+ return `${spec.slice(0, end)}
36
+ it('${path} (GET)', () => {
37
+ return request(app.getHttpServer()).get('${path}').expect(200);
38
+ });
39
+ ${spec.slice(end)}`;
40
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The merges `aventara init` makes into files a project already has —
3
+ * `package.json`, `.env`, `.gitignore`, `pnpm-workspace.yaml` — each answering
4
+ * the merged text and the **conflicts**: values already there that differ from
5
+ * what would be written (the `generateAt` rule's "content the tool did not
6
+ * produce"). A value already equal to what would be written is no change (P3);
7
+ * a conflict is replaced only once confirmed (`conflict.confirmer.ts`), so each
8
+ * merge is computed with conflicts both kept and replaced.
9
+ */
10
+ export type Merge = {
11
+ readonly text: string;
12
+ /** What differs, named for the person asked: `package.json's scripts["postinstall"]`. */
13
+ readonly conflicts: readonly string[];
14
+ };
15
+ export type PackageManifest = {
16
+ readonly [key: string]: unknown;
17
+ readonly dependencies?: Readonly<Record<string, string>>;
18
+ readonly devDependencies?: Readonly<Record<string, string>>;
19
+ readonly scripts?: Readonly<Record<string, string>>;
20
+ };
21
+ export type ManifestChanges = {
22
+ readonly dependencies: Readonly<Record<string, string>>;
23
+ readonly devDependencies: Readonly<Record<string, string>>;
24
+ /** Scripts to set; a different existing value is a conflict. */
25
+ readonly scripts: Readonly<Record<string, string>>;
26
+ /**
27
+ * Scripts to rewrite only from a known value (D3): `from` → `to`. Any other
28
+ * existing value is a conflict; an absent script is left absent.
29
+ */
30
+ readonly rewrites: Readonly<Record<string, {
31
+ readonly from: string;
32
+ readonly to: string;
33
+ }>>;
34
+ };
35
+ /** `package.json`, in npm's own layout: two-space JSON, dependencies sorted. */
36
+ export declare function mergePackageManifest(text: string, changes: ManifestChanges, replaceConflicts: boolean): Merge;
37
+ /** `.env`: a missing key is appended as `KEY="value"`, after a final newline (B5). */
38
+ export declare function mergeEnvFile(text: string | undefined, entries: Readonly<Record<string, string>>, replaceConflicts: boolean): Merge;
39
+ /** `.gitignore`: missing lines appended, nothing else touched. */
40
+ export declare function mergeGitignore(text: string | undefined, additions: readonly string[]): Merge;
41
+ /**
42
+ * `pnpm-workspace.yaml`'s `allowBuilds:` (B9: pnpm 12 refuses install scripts
43
+ * until they are allowed — what `pnpm approve-builds` writes). Keys missing
44
+ * under an existing `allowBuilds:` are added there; otherwise the block is
45
+ * appended.
46
+ */
47
+ export declare function mergePnpmWorkspace(text: string | undefined, packages: readonly string[]): Merge;