@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.
- package/README.md +12 -5
- package/dist/apply/app-module.anchor.d.ts +5 -11
- package/dist/apply/app-module.anchor.js +0 -23
- package/dist/apply/conflict.confirmer.d.ts +0 -9
- package/dist/apply/conflict.confirmer.js +0 -15
- package/dist/apply/e2e-spec.anchor.js +0 -17
- package/dist/apply/main-cors.anchor.d.ts +50 -0
- package/dist/apply/main-cors.anchor.js +60 -0
- package/dist/apply/manifest.merger.d.ts +17 -13
- package/dist/apply/manifest.merger.js +8 -18
- package/dist/aventara.bin.js +0 -10
- package/dist/catalog/adapter.catalog.generated.d.ts +1 -0
- package/dist/catalog/adapter.catalog.generated.js +1 -4
- package/dist/catalog/catalog-entry.interface.d.ts +21 -13
- package/dist/catalog/catalog.matcher.d.ts +7 -13
- package/dist/catalog/catalog.matcher.js +0 -3
- package/dist/catalog/range.reader.d.ts +4 -10
- package/dist/catalog/range.reader.js +0 -14
- package/dist/cli.d.ts +2 -3
- package/dist/cli.js +5 -8
- package/dist/command/command.parser.d.ts +11 -9
- package/dist/command/command.parser.js +24 -18
- package/dist/node-version.guard.js +0 -12
- package/dist/plan/project.planner.d.ts +7 -13
- package/dist/plan/project.planner.js +44 -35
- package/dist/project/package-manager.detector.d.ts +0 -7
- package/dist/project/package-manager.detector.js +0 -7
- package/dist/project/project.inspector.d.ts +3 -9
- package/dist/project/project.inspector.js +0 -10
- package/dist/project/service.detector.d.ts +5 -10
- package/dist/project/service.detector.js +0 -2
- package/dist/project/source.scanner.js +0 -13
- package/dist/run/command.runner.d.ts +4 -4
- package/dist/run/init.orchestrator.d.ts +4 -6
- package/dist/run/init.orchestrator.js +3 -16
- package/dist/run/new.orchestrator.d.ts +8 -9
- package/dist/run/new.orchestrator.js +10 -18
- package/dist/run/scaffold.committer.d.ts +28 -0
- package/dist/run/scaffold.committer.js +53 -0
- package/dist/templates/docs.links.d.ts +8 -0
- package/dist/templates/docs.links.js +2 -0
- package/dist/templates/prisma7/prisma7.templates.js +0 -27
- package/dist/templates/template.registry.d.ts +13 -17
- package/dist/templates/template.registry.js +0 -5
- package/dist/wizard/answer.resolver.d.ts +5 -7
- package/dist/wizard/answer.resolver.js +0 -10
- package/dist/wizard/readline.prompter.d.ts +5 -7
- package/dist/wizard/readline.prompter.js +0 -10
- package/dist/wizard/wizard.questions.d.ts +28 -22
- package/dist/wizard/wizard.questions.js +21 -49
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ aventara init # Aventara added to the NestJS 12 project in this
|
|
|
9
9
|
```
|
|
10
10
|
|
|
11
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.
|
|
12
|
+
`framework.client.ts` and the generated client — is `@aventara/client`'s (`npx @aventara/client@pilot init`).
|
|
13
13
|
|
|
14
14
|
## `aventara new <name>`
|
|
15
15
|
|
|
@@ -17,6 +17,11 @@ Runs the pinned `nest new` (`npx -y @nestjs/cli@12.0.8 new …`, or `pnpm dlx
|
|
|
17
17
|
`src/app.module.ts` is what that version writes, then runs exactly `aventara init`'s pipeline over the new project and
|
|
18
18
|
installs it. A directory that exists and is not empty is refused before anything runs.
|
|
19
19
|
|
|
20
|
+
Once installed, the project gets its first git commit, `Initial Aventara Scaffold` — the lockfile in it, `.env` not
|
|
21
|
+
(it is in `.gitignore`). No commit is made, and one line says why, when git is not installed, when git has no
|
|
22
|
+
`user.name`/`user.email`, or when the directory is already inside a git repository (a monorepo: commit it there).
|
|
23
|
+
`--skip-git` makes no repository and no commit.
|
|
24
|
+
|
|
20
25
|
## `aventara init`
|
|
21
26
|
|
|
22
27
|
Runs inside an existing **NestJS 12** project (refused: no `package.json`, another Nest major, TypeScript 7, a yarn-only
|
|
@@ -25,8 +30,9 @@ lockfile, a project already initialized).
|
|
|
25
30
|
- **No ORM yet:** the ORM and the database are asked (or taken from the flags), and everything below is written.
|
|
26
31
|
- **Prisma already there:** the installed Prisma must be one an Aventara adapter supports — today Prisma 7, `^7.10.0`,
|
|
27
32
|
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
|
|
29
|
-
extending `PrismaClient` and the module
|
|
33
|
+
nothing written. Your schema, Prisma config and service are **reused and never written**, and your `.env` only gains
|
|
34
|
+
a `CORS_ORIGINS` line at its end when it has none; init finds the class extending `PrismaClient` and the module
|
|
35
|
+
exporting it, or asks.
|
|
30
36
|
|
|
31
37
|
### Questions and flags
|
|
32
38
|
|
|
@@ -37,7 +43,7 @@ possible answer is shown, not asked.
|
|
|
37
43
|
| Flag | Question | Default |
|
|
38
44
|
|---|---|---|
|
|
39
45
|
| `<name>` (`new` only) | The project directory. | — (required) |
|
|
40
|
-
| `--orm <
|
|
46
|
+
| `--orm <alias>` | The ORM, as an adapter's alias: `prisma7` (Prisma 7, `@aventara/prisma7-adapter`). `--help` lists the aliases that exist. | the only one there is |
|
|
41
47
|
| `--db <provider>` | The database: the adapter's providers (`sqlite`, `postgresql`). | `sqlite` |
|
|
42
48
|
| `--package-manager <npm\|pnpm>` | — | the lockfile's, else the one that launched the CLI, else npm |
|
|
43
49
|
| `--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` |
|
|
@@ -59,7 +65,8 @@ when you confirm, or with `--yes`; where nobody can be asked, the run stops and
|
|
|
59
65
|
| `src/aventara.config.ts` — `aventaraConfig(prisma)`: the entrypoint (`/api`), and the restrictions and pipelines you add | you |
|
|
60
66
|
| `src/app.module.ts` — one edit: the imports, `PrismaModule` and `AventaraModule.forRootAsync({ … })`; printed instead when the file is not the shape expected | you |
|
|
61
67
|
| `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
|
-
|
|
|
68
|
+
| `src/main.ts` — one edit after `NestFactory.create`: `app.enableCors(…)` for the comma-separated origins in `CORS_ORIGINS`, and CORS off when it is unset or empty, then a commented-out platform-level rate limiter (`express-rate-limit`; Nest's `@nestjs/throttler` is a guard, which does not run on Aventara's routes); left alone when the file already configures CORS, printed when it has no such line | you |
|
|
69
|
+
| `.env` (`DATABASE_URL`, and `CORS_ORIGINS="http://localhost:5173,http://localhost:3001"` appended unless a line sets it), `.gitignore` (`/src/generated/`, `/dev.db*`, `.env`) | you |
|
|
63
70
|
| `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
71
|
| `pnpm-workspace.yaml` (pnpm only) — `allowBuilds` for Prisma's and SQLite's install scripts | you |
|
|
65
72
|
| `src/generated/prisma/**`, `src/generated/aventara/discovery.artifact.ts` | **generated** by `aventara:prepare` (run on every install); never edit |
|
|
@@ -1,15 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
2
|
+
* The scan is string- and comment-aware (a `@Module(` or an `imports: [` inside a
|
|
3
|
+
* comment, a string or a template literal is not code), and it never reformats:
|
|
4
|
+
* everything outside the two insertion points keeps its bytes. When the anchor is
|
|
5
|
+
* not found exactly once — two `@Module`s, a computed `imports`, no `imports` at
|
|
6
|
+
* all — nothing is edited and the caller prints the lines to add instead.
|
|
13
7
|
*/
|
|
14
8
|
export type AppModuleWiring = {
|
|
15
9
|
/** Complete import declarations, one per line. */
|
|
@@ -1,16 +1,3 @@
|
|
|
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
1
|
import { closing, codeIndexes, codeMask, nextCode, OPENERS, } from "../project/source.scanner.js";
|
|
15
2
|
function indentOf(text, at) {
|
|
16
3
|
const lineStart = text.lastIndexOf("\n", at - 1) + 1;
|
|
@@ -22,7 +9,6 @@ function indented(block, indent) {
|
|
|
22
9
|
.map((line) => (line === "" ? line : `${indent}${line}`))
|
|
23
10
|
.join("\n");
|
|
24
11
|
}
|
|
25
|
-
/** The local names a file's import declarations bind. */
|
|
26
12
|
function importedNames(text, code) {
|
|
27
13
|
const names = new Set();
|
|
28
14
|
for (const at of codeIndexes(text, code, /^import\b/gm)) {
|
|
@@ -46,7 +32,6 @@ function importedNames(text, code) {
|
|
|
46
32
|
}
|
|
47
33
|
return names;
|
|
48
34
|
}
|
|
49
|
-
/** Splices `wiring` into `text` at the anchor, or says why there is no anchor. */
|
|
50
35
|
export function spliceAppModule(text, wiring) {
|
|
51
36
|
const code = codeMask(text);
|
|
52
37
|
const decorators = codeIndexes(text, code, /@Module\s*\(/g);
|
|
@@ -74,7 +59,6 @@ export function spliceAppModule(text, wiring) {
|
|
|
74
59
|
};
|
|
75
60
|
}
|
|
76
61
|
const objectEnd = closing(text, code, object);
|
|
77
|
-
// `imports` at the object's own level: not inside a nested bracket.
|
|
78
62
|
let depth = 0;
|
|
79
63
|
let key = -1;
|
|
80
64
|
for (let at = object + 1; at < objectEnd; at += 1) {
|
|
@@ -116,7 +100,6 @@ export function spliceAppModule(text, wiring) {
|
|
|
116
100
|
}
|
|
117
101
|
const base = indentOf(text, key);
|
|
118
102
|
const element = `${base} `;
|
|
119
|
-
// An entry the developer already lists (their own `PrismaModule`) is not added twice.
|
|
120
103
|
const listed = text.slice(array + 1, arrayEnd);
|
|
121
104
|
const entries = wiring.entries
|
|
122
105
|
.filter((entry) => {
|
|
@@ -125,8 +108,6 @@ export function spliceAppModule(text, wiring) {
|
|
|
125
108
|
!new RegExp(`(^|[\\s,\\[])${name}\\s*,?\\s*($|\\])`, "m").test(listed));
|
|
126
109
|
})
|
|
127
110
|
.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
111
|
let lastCode = arrayEnd - 1;
|
|
131
112
|
while (lastCode > array &&
|
|
132
113
|
(!code[lastCode] || /\s/.test(text[lastCode]))) {
|
|
@@ -141,7 +122,6 @@ export function spliceAppModule(text, wiring) {
|
|
|
141
122
|
...entries,
|
|
142
123
|
];
|
|
143
124
|
const spliced = `[\n${lines.join("\n")}\n${base}]`;
|
|
144
|
-
// The import lines go after the last import declaration.
|
|
145
125
|
const declarations = codeIndexes(text, code, /^import\b/gm);
|
|
146
126
|
let insertAt = 0;
|
|
147
127
|
for (const declaration of declarations) {
|
|
@@ -149,8 +129,6 @@ export function spliceAppModule(text, wiring) {
|
|
|
149
129
|
const lineEnd = text.indexOf("\n", end === -1 ? declaration : end);
|
|
150
130
|
insertAt = lineEnd === -1 ? text.length : lineEnd + 1;
|
|
151
131
|
}
|
|
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
132
|
const imported = importedNames(text, code);
|
|
155
133
|
const missing = wiring.imports.flatMap((line) => {
|
|
156
134
|
const named = /^import\s+(type\s+)?\{([^}]*)\}(\s*from\s*.*)$/.exec(line);
|
|
@@ -174,7 +152,6 @@ export function spliceAppModule(text, wiring) {
|
|
|
174
152
|
text: edited.slice(0, insertAt) + imports + edited.slice(insertAt),
|
|
175
153
|
};
|
|
176
154
|
}
|
|
177
|
-
/** What to add by hand when there is no anchor. */
|
|
178
155
|
export function wiringInstructions(wiring) {
|
|
179
156
|
return [
|
|
180
157
|
"Add to src/app.module.ts:",
|
|
@@ -1,13 +1,4 @@
|
|
|
1
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
2
|
/** Nobody confirmed replacing existing content. A refusal: nothing was touched. */
|
|
12
3
|
export declare class ConflictsNotConfirmedError extends Error {
|
|
13
4
|
readonly name = "ConflictsNotConfirmedError";
|
|
@@ -1,24 +1,9 @@
|
|
|
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
1
|
export class ConflictsNotConfirmedError extends Error {
|
|
12
2
|
name = "ConflictsNotConfirmedError";
|
|
13
3
|
}
|
|
14
4
|
export function conflictWarning(conflicts) {
|
|
15
5
|
return `${conflicts.join(", ")} already ${conflicts.length === 1 ? "has" : "have"} other content, and initializing will replace ${conflicts.length === 1 ? "it" : "them"}`;
|
|
16
6
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Resolves when `conflicts` may be replaced; throws when they may not.
|
|
19
|
-
*
|
|
20
|
-
* @throws ConflictsNotConfirmedError
|
|
21
|
-
*/
|
|
22
7
|
export async function confirmConflicts(conflicts, consent) {
|
|
23
8
|
if (conflicts.length === 0 || consent.yes) {
|
|
24
9
|
return;
|
|
@@ -1,17 +1,4 @@
|
|
|
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
1
|
export const E2E_SPEC = "test/app.e2e-spec.ts";
|
|
14
|
-
/** `nest new`'s `GET /` test, byte for byte: the anchor. */
|
|
15
2
|
const ROOT_TEST = ` it('/ (GET)', () => {
|
|
16
3
|
return request(app.getHttpServer())
|
|
17
4
|
.get('/')
|
|
@@ -19,10 +6,6 @@ const ROOT_TEST = ` it('/ (GET)', () => {
|
|
|
19
6
|
.expect('Hello World!');
|
|
20
7
|
});
|
|
21
8
|
`;
|
|
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
9
|
export function spliceContractTest(spec, entrypoint) {
|
|
27
10
|
const path = `${entrypoint}/_contract`;
|
|
28
11
|
const at = spec.indexOf(ROOT_TEST);
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pilot.3 — the scaffold turns CORS on in `src/main.ts` for the origins listed in
|
|
3
|
+
* `CORS_ORIGINS`, so a browser frontend on another port (Vite's 5173, a Next.js
|
|
4
|
+
* dev server on 3001) can call the protocol:
|
|
5
|
+
*
|
|
6
|
+
* const corsOrigins = (process.env.CORS_ORIGINS ?? '')…filter(Boolean);
|
|
7
|
+
* app.enableCors({ origin: corsOrigins.length > 0 ? corsOrigins : false });
|
|
8
|
+
*
|
|
9
|
+
* followed by a commented-out platform-level rate limiter.
|
|
10
|
+
*
|
|
11
|
+
* **Unset or empty is OFF, never open.** The `cors` package Nest's Express
|
|
12
|
+
* platform uses — and `@fastify/cors` — answer `Access-Control-Allow-Origin: *`
|
|
13
|
+
* when given no origin, so the list is never passed through as it is: no origin is
|
|
14
|
+
* `false`, CORS disabled.
|
|
15
|
+
*
|
|
16
|
+
* Like the `app.module.ts` edit, this one is anchored and never reformats: the
|
|
17
|
+
* lines are inserted after the one statement that creates the application — `const
|
|
18
|
+
* app = await NestFactory.create(…);`, found in code exactly once — through its
|
|
19
|
+
* own variable. A `main.ts` that already configures CORS (`enableCors`, or a
|
|
20
|
+
* `cors` option given to `NestFactory.create`) is the developer's decision and is
|
|
21
|
+
* left as it is; one with no such statement is not edited, and the lines are
|
|
22
|
+
* printed instead.
|
|
23
|
+
*/
|
|
24
|
+
/** The variable the inserted lines read. */
|
|
25
|
+
export declare const CORS_ORIGINS_VARIABLE = "CORS_ORIGINS";
|
|
26
|
+
/** What the scaffold's `.env` lists: Vite's dev server, and a Next.js one beside Nest's 3000. */
|
|
27
|
+
export declare const SCAFFOLD_CORS_ORIGINS = "http://localhost:5173,http://localhost:3001";
|
|
28
|
+
/** The main file `nest new` writes. */
|
|
29
|
+
export declare const MAIN_FILE = "src/main.ts";
|
|
30
|
+
export type MainCorsEdit =
|
|
31
|
+
/** The lines were inserted. */
|
|
32
|
+
{
|
|
33
|
+
readonly kind: "edited";
|
|
34
|
+
readonly text: string;
|
|
35
|
+
}
|
|
36
|
+
/** The lines are already there: a previous run's edit. */
|
|
37
|
+
| {
|
|
38
|
+
readonly kind: "present";
|
|
39
|
+
}
|
|
40
|
+
/** The developer configures CORS: nothing is changed. */
|
|
41
|
+
| {
|
|
42
|
+
readonly kind: "configured";
|
|
43
|
+
} | {
|
|
44
|
+
readonly kind: "not-anchored";
|
|
45
|
+
readonly reason: string;
|
|
46
|
+
};
|
|
47
|
+
/** Splices the CORS lines into `text`, or says why they are not. */
|
|
48
|
+
export declare function spliceMainCors(text: string): MainCorsEdit;
|
|
49
|
+
/** What to add by hand when there is no anchor. */
|
|
50
|
+
export declare function mainCorsInstructions(): string;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { codeIndexes, codeMask } from "../project/source.scanner.js";
|
|
2
|
+
import { RATE_LIMITING_DOCS } from "../templates/docs.links.js";
|
|
3
|
+
export const CORS_ORIGINS_VARIABLE = "CORS_ORIGINS";
|
|
4
|
+
export const SCAFFOLD_CORS_ORIGINS = "http://localhost:5173,http://localhost:3001";
|
|
5
|
+
export const MAIN_FILE = "src/main.ts";
|
|
6
|
+
function corsLines(app, indent) {
|
|
7
|
+
return [
|
|
8
|
+
`// The browser origins allowed to call this server: ${CORS_ORIGINS_VARIABLE} in .env,`,
|
|
9
|
+
"// comma-separated. Unset or empty, CORS stays off.",
|
|
10
|
+
`const corsOrigins = (process.env.${CORS_ORIGINS_VARIABLE} ?? '')`,
|
|
11
|
+
" .split(',')",
|
|
12
|
+
" .map((origin) => origin.trim())",
|
|
13
|
+
" .filter(Boolean);",
|
|
14
|
+
`${app}.enableCors({ origin: corsOrigins.length > 0 ? corsOrigins : false });`,
|
|
15
|
+
...RATE_LIMIT_LINES(app),
|
|
16
|
+
]
|
|
17
|
+
.map((line) => `${indent}${line}`)
|
|
18
|
+
.join("\n");
|
|
19
|
+
}
|
|
20
|
+
function RATE_LIMIT_LINES(app) {
|
|
21
|
+
return [
|
|
22
|
+
"// Rate limiting: @nestjs/throttler is a Nest guard and does not run on Aventara's",
|
|
23
|
+
"// routes; a platform-level limiter does. `npm i express-rate-limit`, import",
|
|
24
|
+
"// { rateLimit } from 'express-rate-limit', pick your limits and uncomment below.",
|
|
25
|
+
`// See: ${RATE_LIMITING_DOCS}`,
|
|
26
|
+
`// ${app}.use(rateLimit({ windowMs: 60_000, limit: 100 }));`,
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
const CREATE = /^([ \t]*)(?:const|let)\s+([A-Za-z_$][\w$]*)\s*=\s*await\s+NestFactory\.create\b[^;]*;[ \t]*$/gm;
|
|
30
|
+
export function spliceMainCors(text) {
|
|
31
|
+
const code = codeMask(text);
|
|
32
|
+
const creates = [...text.matchAll(CREATE)].filter((match) => code[match.index + match[1].length]);
|
|
33
|
+
const create = creates.length === 1 ? creates[0] : undefined;
|
|
34
|
+
if (create !== undefined) {
|
|
35
|
+
const lines = corsLines(create[2], create[1]);
|
|
36
|
+
if (text.includes(lines)) {
|
|
37
|
+
return { kind: "present" };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
if (codeIndexes(text, code, /\benableCors\b/g).length > 0 ||
|
|
41
|
+
creates.some((match) => /\bcors\s*:/.test(match[0]))) {
|
|
42
|
+
return { kind: "configured" };
|
|
43
|
+
}
|
|
44
|
+
if (create === undefined) {
|
|
45
|
+
return {
|
|
46
|
+
kind: "not-anchored",
|
|
47
|
+
reason: creates.length === 0
|
|
48
|
+
? "it has no `const app = await NestFactory.create(…);` line"
|
|
49
|
+
: `it has ${creates.length} \`NestFactory.create\` lines, not one`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
const end = create.index + create[0].length;
|
|
53
|
+
return {
|
|
54
|
+
kind: "edited",
|
|
55
|
+
text: `${text.slice(0, end)}\n${corsLines(create[2], create[1])}${text.slice(end)}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function mainCorsInstructions() {
|
|
59
|
+
return `Add to ${MAIN_FILE}, after the application is created:\n${corsLines("app", " ")}`;
|
|
60
|
+
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The merges `aventara init` makes into files a project already has —
|
|
3
|
-
* `package.json`, `.env`, `.gitignore`, `pnpm-workspace.yaml` — each answering
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* `package.json`, `.env`, `.gitignore`, `pnpm-workspace.yaml` — each answering the
|
|
4
|
+
* merged text and the **conflicts**: values already there that differ from what
|
|
5
|
+
* would be written (the `generateAt` rule's "content the tool did not produce"). A
|
|
6
|
+
* value already equal to what would be written is no change; a conflict is
|
|
7
|
+
* replaced only once confirmed (`conflict.confirmer.ts`), so each merge is
|
|
8
|
+
* computed with conflicts both kept and replaced.
|
|
9
9
|
*/
|
|
10
10
|
export type Merge = {
|
|
11
11
|
readonly text: string;
|
|
@@ -24,8 +24,8 @@ export type ManifestChanges = {
|
|
|
24
24
|
/** Scripts to set; a different existing value is a conflict. */
|
|
25
25
|
readonly scripts: Readonly<Record<string, string>>;
|
|
26
26
|
/**
|
|
27
|
-
* Scripts to rewrite only from a known value
|
|
28
|
-
*
|
|
27
|
+
* Scripts to rewrite only from a known value: `from` → `to`. Any other existing
|
|
28
|
+
* value is a conflict; an absent script is left absent.
|
|
29
29
|
*/
|
|
30
30
|
readonly rewrites: Readonly<Record<string, {
|
|
31
31
|
readonly from: string;
|
|
@@ -34,14 +34,18 @@ export type ManifestChanges = {
|
|
|
34
34
|
};
|
|
35
35
|
/** `package.json`, in npm's own layout: two-space JSON, dependencies sorted. */
|
|
36
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
|
|
37
|
+
/** `.env`: a missing key is appended as `KEY="value"`, after a final newline. */
|
|
38
38
|
export declare function mergeEnvFile(text: string | undefined, entries: Readonly<Record<string, string>>, replaceConflicts: boolean): Merge;
|
|
39
|
+
/**
|
|
40
|
+
* pilot.3 — `.env`: `KEY="value"` appended after a final newline when no line sets
|
|
41
|
+
* `key`; a line that does is the developer's, never a conflict, and nothing else
|
|
42
|
+
* is reordered or rewritten.
|
|
43
|
+
*/
|
|
44
|
+
export declare function appendEnvEntryIfAbsent(text: string | undefined, key: string, value: string): string;
|
|
39
45
|
/** `.gitignore`: missing lines appended, nothing else touched. */
|
|
40
46
|
export declare function mergeGitignore(text: string | undefined, additions: readonly string[]): Merge;
|
|
41
47
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* under an existing `allowBuilds:` are added there; otherwise the block is
|
|
45
|
-
* appended.
|
|
48
|
+
* Keys missing under an existing `allowBuilds:` are added there; otherwise the
|
|
49
|
+
* block is appended.
|
|
46
50
|
*/
|
|
47
51
|
export declare function mergePnpmWorkspace(text: string | undefined, packages: readonly string[]): Merge;
|
|
@@ -1,16 +1,6 @@
|
|
|
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
1
|
function sorted(record) {
|
|
11
2
|
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
|
|
12
3
|
}
|
|
13
|
-
/** `package.json`, in npm's own layout: two-space JSON, dependencies sorted. */
|
|
14
4
|
export function mergePackageManifest(text, changes, replaceConflicts) {
|
|
15
5
|
const manifest = JSON.parse(text);
|
|
16
6
|
const conflicts = [];
|
|
@@ -66,7 +56,6 @@ function unquoted(value) {
|
|
|
66
56
|
? value.slice(1, -1)
|
|
67
57
|
: value;
|
|
68
58
|
}
|
|
69
|
-
/** `.env`: a missing key is appended as `KEY="value"`, after a final newline (B5). */
|
|
70
59
|
export function mergeEnvFile(text, entries, replaceConflicts) {
|
|
71
60
|
const lines = text === undefined || text === ""
|
|
72
61
|
? []
|
|
@@ -90,7 +79,14 @@ export function mergeEnvFile(text, entries, replaceConflicts) {
|
|
|
90
79
|
}
|
|
91
80
|
return { text: `${lines.join("\n")}\n`, conflicts };
|
|
92
81
|
}
|
|
93
|
-
|
|
82
|
+
export function appendEnvEntryIfAbsent(text, key, value) {
|
|
83
|
+
const current = text ?? "";
|
|
84
|
+
if (current.split("\n").some((line) => ENV_LINE.exec(line)?.[1] === key)) {
|
|
85
|
+
return current;
|
|
86
|
+
}
|
|
87
|
+
const separator = current === "" || current.endsWith("\n") ? "" : "\n";
|
|
88
|
+
return `${current}${separator}${key}="${value}"\n`;
|
|
89
|
+
}
|
|
94
90
|
export function mergeGitignore(text, additions) {
|
|
95
91
|
const lines = text === undefined || text === ""
|
|
96
92
|
? []
|
|
@@ -103,12 +99,6 @@ export function mergeGitignore(text, additions) {
|
|
|
103
99
|
conflicts: [],
|
|
104
100
|
};
|
|
105
101
|
}
|
|
106
|
-
/**
|
|
107
|
-
* `pnpm-workspace.yaml`'s `allowBuilds:` (B9: pnpm 12 refuses install scripts
|
|
108
|
-
* until they are allowed — what `pnpm approve-builds` writes). Keys missing
|
|
109
|
-
* under an existing `allowBuilds:` are added there; otherwise the block is
|
|
110
|
-
* appended.
|
|
111
|
-
*/
|
|
112
102
|
export function mergePnpmWorkspace(text, packages) {
|
|
113
103
|
const quoted = (name) => /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(name) ? name : `"${name}"`;
|
|
114
104
|
const lines = text === undefined || text === ""
|
package/dist/aventara.bin.js
CHANGED
|
@@ -1,15 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { refuseUnsupportedNode } from "./node-version.guard.js";
|
|
3
|
-
/**
|
|
4
|
-
* `aventara`'s entry, what the manifest's `bin` names (F-855). It always runs — no
|
|
5
|
-
* `import.meta.main` — and checks this Node against the package's
|
|
6
|
-
* `engines.node` before it loads anything else: the program is imported only
|
|
7
|
-
* once the guard admits this Node, so an older Node meets one sentence and exit
|
|
8
|
-
* 1, never a silent exit 0 or a parse error from a module it cannot run.
|
|
9
|
-
*
|
|
10
|
-
* A promise chain rather than a top-level `await`, which Node 12 cannot parse:
|
|
11
|
-
* this file is read by the Nodes the package does not support.
|
|
12
|
-
*/
|
|
13
3
|
if (!refuseUnsupportedNode("aventara", new URL("../package.json", import.meta.url))) {
|
|
14
4
|
void import("./cli.js").then((program) => program.runFromProcess());
|
|
15
5
|
}
|
|
@@ -1,10 +1,7 @@
|
|
|
1
|
-
// biome-ignore-all format: generated output; these bytes are the catalog
|
|
2
|
-
// biome-ignore-all lint: generated output
|
|
3
|
-
/* !!! Generated by @aventara/cli's adapter-catalog generator from each adapter's manifest. Do not edit. !!! */
|
|
4
|
-
/* Regenerate with `pnpm --filter @aventara/cli build` (its `prebuild`). */
|
|
5
1
|
export const ADAPTER_CATALOG = [
|
|
6
2
|
{
|
|
7
3
|
"id": "prisma7",
|
|
4
|
+
"alias": "prisma7",
|
|
8
5
|
"adapterPackage": "@aventara/prisma7-adapter",
|
|
9
6
|
"orm": {
|
|
10
7
|
"family": "prisma",
|
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import type { ADAPTER_CATALOG } from "./adapter.catalog.generated.js";
|
|
2
2
|
/**
|
|
3
|
-
* One entry per Aventara adapter package that exists
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `scripts/adapter-catalog.generator.ts` into `adapter.catalog.generated.ts`
|
|
8
|
-
*
|
|
9
|
-
*
|
|
3
|
+
* One entry per Aventara adapter package that exists: what the CLI may offer and
|
|
4
|
+
* what it must refuse, every value traced to that adapter's own manifest — its
|
|
5
|
+
* `aventara.adapter` declaration, its `peerDependencies` and its `bin`. The
|
|
6
|
+
* entries are **generated** at this package's build by
|
|
7
|
+
* `scripts/adapter-catalog.generator.ts` into `adapter.catalog.generated.ts` and
|
|
8
|
+
* never edited. The CLI holds no list of ORMs, majors, providers or drivers of its
|
|
9
|
+
* own.
|
|
10
10
|
*/
|
|
11
11
|
export type CatalogEntry = {
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The adapter package's ORM stem, e.g. `prisma7`: the wizard's value and the
|
|
14
|
+
* templates' key.
|
|
15
|
+
*/
|
|
13
16
|
readonly id: string;
|
|
17
|
+
/**
|
|
18
|
+
* `aventara.adapter.alias`, e.g. `prisma7`: what `--orm` takes. Always
|
|
19
|
+
* `<orm><major>`, the generator refuses any other.
|
|
20
|
+
*/
|
|
21
|
+
readonly alias: string;
|
|
14
22
|
/** The adapter package a project installs, e.g. `@aventara/prisma7-adapter`. */
|
|
15
23
|
readonly adapterPackage: string;
|
|
16
24
|
readonly orm: {
|
|
@@ -23,17 +31,17 @@ export type CatalogEntry = {
|
|
|
23
31
|
/** The one major `range` admits. */
|
|
24
32
|
readonly major: number;
|
|
25
33
|
};
|
|
26
|
-
/** `peerDependencies[orm.package]`: the ORM versions the adapter supports
|
|
34
|
+
/** `peerDependencies[orm.package]`: the ORM versions the adapter supports. */
|
|
27
35
|
readonly range: string;
|
|
28
|
-
/** Packages that identify the ORM family in a project
|
|
36
|
+
/** Packages that identify the ORM family in a project. */
|
|
29
37
|
readonly familyPackages: readonly string[];
|
|
30
|
-
/** Database providers, in the adapter's order; the first is the default
|
|
38
|
+
/** Database providers, in the adapter's order; the first is the default. */
|
|
31
39
|
readonly providers: readonly string[];
|
|
32
40
|
/** Provider → the driver adapter package for it. */
|
|
33
41
|
readonly drivers: Readonly<Record<string, string>>;
|
|
34
|
-
/** The ORM generators whose output the adapter reads
|
|
42
|
+
/** The ORM generators whose output the adapter reads. */
|
|
35
43
|
readonly generators: readonly string[];
|
|
36
|
-
/** The adapter's generate step: its one `bin
|
|
44
|
+
/** The adapter's generate step: its one `bin`. */
|
|
37
45
|
readonly bin: string;
|
|
38
46
|
};
|
|
39
47
|
export type AdapterCatalog = readonly CatalogEntry[];
|
|
@@ -1,23 +1,17 @@
|
|
|
1
1
|
import type { AdapterCatalog, CatalogEntry } from "./catalog-entry.interface.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* depends on (a pattern ending in `*` is a prefix).
|
|
9
|
-
* 2. Its major: the installed version of the family's ORM package, never the
|
|
10
|
-
* range in `package.json` (P10: `^7` can resolve to anything in 7, and a
|
|
11
|
-
* stale lockfile to 6.x). Every other family package installed must be of
|
|
12
|
-
* that same major — a Prisma 8 CLI or an `@prisma/orm-*` beside a Prisma 7
|
|
13
|
-
* client is a project mid-migration, refused (D10).
|
|
3
|
+
* 1. The ORM family: the catalog entries whose family packages the project depends
|
|
4
|
+
* on (a pattern ending in `*` is a prefix).
|
|
5
|
+
* 2. Every other family package installed must be of that same major — a Prisma 8
|
|
6
|
+
* CLI or an `@prisma/orm-*` beside a Prisma 7 client is a project
|
|
7
|
+
* mid-migration, refused.
|
|
14
8
|
* 3. The entry for that major, or a refusal naming what exists.
|
|
15
9
|
* 4. The installed version inside the entry's declared range.
|
|
16
10
|
* 5. Then what the project's ORM setup declares — provider, driver, generator —
|
|
17
11
|
* against the entry's providers, drivers and generators.
|
|
18
12
|
*
|
|
19
|
-
* Every refusal is one sentence naming what was found and what the catalog
|
|
20
|
-
*
|
|
13
|
+
* Every refusal is one sentence naming what was found and what the catalog offers.
|
|
14
|
+
* Nothing has been written when one is raised.
|
|
21
15
|
*/
|
|
22
16
|
export type MatchVerdict = {
|
|
23
17
|
readonly kind: "matched";
|
|
@@ -35,8 +35,6 @@ export function matchInstalledOrm(catalog, project) {
|
|
|
35
35
|
}
|
|
36
36
|
const client = installed.find((member) => member.name === ormPackage);
|
|
37
37
|
const clientMajor = client?.version === undefined ? undefined : majorOf(client.version);
|
|
38
|
-
// D10: one major across the family. The first package off the client's major
|
|
39
|
-
// (or any, when there is no client) names the project's other major.
|
|
40
38
|
const off = installed.find((member) => member.name !== ormPackage &&
|
|
41
39
|
majorOf(member.version) !== clientMajor);
|
|
42
40
|
const decided = off ?? client;
|
|
@@ -62,7 +60,6 @@ export function matchInstalledOrm(catalog, project) {
|
|
|
62
60
|
}
|
|
63
61
|
return { kind: "matched", entry };
|
|
64
62
|
}
|
|
65
|
-
/** Step 5: the project's provider, driver and generator against what `entry` declares. */
|
|
66
63
|
export function matchOrmSetup(entry, setup) {
|
|
67
64
|
const name = `the ${entry.orm.label} ${entry.orm.major} adapter`;
|
|
68
65
|
if (setup.provider === undefined ||
|
|
@@ -1,17 +1,11 @@
|
|
|
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
|
/** `^7.10.0` → `7.10.0`; `>=7.10.0 <8` → `7.10.0`. */
|
|
8
2
|
export declare function lowestVersionOf(range: string): string;
|
|
9
3
|
/** `^12.0.1` → 12; `~6.0.2` → 6; a range with no number (`latest`, `*`) → `undefined`. */
|
|
10
4
|
export declare function lowestMajorOf(range: string): number | undefined;
|
|
11
5
|
/**
|
|
12
|
-
* Whether an installed `version` is in a declared `range
|
|
13
|
-
*
|
|
14
|
-
* range
|
|
15
|
-
*
|
|
6
|
+
* Whether an installed `version` is in a declared `range`. Reads the comparator
|
|
7
|
+
* forms an adapter's peer range uses; a pre-release never satisfies (semver's rule
|
|
8
|
+
* for a range whose comparators carry none). Any other form is refused rather than
|
|
9
|
+
* guessed.
|
|
16
10
|
*/
|
|
17
11
|
export declare function satisfiesRange(version: string, range: string): boolean;
|