@aventara/cli 0.1.0-pilot.0 → 0.1.0-pilot.2
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 +39 -7
- 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.d.ts +18 -0
- package/dist/apply/e2e-spec.anchor.js +23 -0
- package/dist/apply/manifest.merger.d.ts +11 -13
- package/dist/apply/manifest.merger.js +0 -18
- package/dist/aventara.bin.js +0 -10
- package/dist/catalog/adapter.catalog.generated.js +0 -4
- package/dist/catalog/catalog-entry.interface.d.ts +16 -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 +2 -8
- package/dist/command/command.parser.d.ts +13 -8
- package/dist/command/command.parser.js +30 -10
- package/dist/node-version.guard.js +0 -12
- package/dist/plan/project.planner.d.ts +15 -14
- package/dist/plan/project.planner.js +22 -25
- 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 +1 -12
- package/dist/run/new.orchestrator.d.ts +6 -9
- package/dist/run/new.orchestrator.js +0 -16
- package/dist/templates/prisma7/prisma7.templates.d.ts +1 -1
- package/dist/templates/prisma7/prisma7.templates.js +2 -28
- package/dist/templates/template.registry.d.ts +18 -17
- package/dist/templates/template.registry.js +1 -1
- 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 +16 -23
- package/dist/wizard/wizard.questions.js +0 -29
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
The `aventara` command: it scaffolds an Aventara server on NestJS 12.
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
npm i -g @aventara/cli
|
|
6
|
+
npm i -g @aventara/cli@pilot # or run it once: npx @aventara/cli@pilot <command>
|
|
7
7
|
aventara new my-api # a new NestJS project with Aventara
|
|
8
8
|
aventara init # Aventara added to the NestJS 12 project in this directory
|
|
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.ts` and the generated client — is `@aventara/client`'s (`npx @aventara/client init`).
|
|
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
|
|
|
@@ -58,7 +58,8 @@ when you confirm, or with `--yes`; where nobody can be asked, the run stops and
|
|
|
58
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
59
|
| `src/aventara.config.ts` — `aventaraConfig(prisma)`: the entrypoint (`/api`), and the restrictions and pipelines you add | you |
|
|
60
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
|
-
|
|
|
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*`, `.env`) | you |
|
|
62
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 |
|
|
63
64
|
| `pnpm-workspace.yaml` (pnpm only) — `allowBuilds` for Prisma's and SQLite's install scripts | you |
|
|
64
65
|
| `src/generated/prisma/**`, `src/generated/aventara/discovery.artifact.ts` | **generated** by `aventara:prepare` (run on every install); never edit |
|
|
@@ -68,16 +69,47 @@ when you confirm, or with `--yes`; where nobody can be asked, the run stops and
|
|
|
68
69
|
```bash
|
|
69
70
|
npx prisma db push # create the tables (init never touches a database); pnpm: pnpm exec prisma db push
|
|
70
71
|
npm run start:dev # GET http://localhost:3000/api/_contract
|
|
71
|
-
npx @aventara/client init # in your frontend
|
|
72
|
+
npx @aventara/client@pilot init # in your frontend; pnpm: pnpm dlx @aventara/client@pilot init
|
|
72
73
|
```
|
|
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
|
+
|
|
74
78
|
With PostgreSQL, set `DATABASE_URL` in `.env` first (init writes Prisma's placeholder, and never asks for a secret).
|
|
75
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
|
+
|
|
76
108
|
## Supported
|
|
77
109
|
|
|
78
|
-
NestJS 12; Node `^22.18.0 || >=24.2.0` (measured; on any other Node the bin refuses in one sentence naming that range
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
81
113
|
CommonJS projects; Prisma 7 (`^7.10.0`) on SQLite and PostgreSQL. The ORMs, majors, databases and drivers on offer are
|
|
82
114
|
**generated** from the Aventara adapter packages that exist (`src/catalog/adapter.catalog.generated.ts`), never listed
|
|
83
115
|
by hand: a future
|
|
@@ -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;
|
|
@@ -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,23 @@
|
|
|
1
|
+
export const E2E_SPEC = "test/app.e2e-spec.ts";
|
|
2
|
+
const ROOT_TEST = ` it('/ (GET)', () => {
|
|
3
|
+
return request(app.getHttpServer())
|
|
4
|
+
.get('/')
|
|
5
|
+
.expect(200)
|
|
6
|
+
.expect('Hello World!');
|
|
7
|
+
});
|
|
8
|
+
`;
|
|
9
|
+
export function spliceContractTest(spec, entrypoint) {
|
|
10
|
+
const path = `${entrypoint}/_contract`;
|
|
11
|
+
const at = spec.indexOf(ROOT_TEST);
|
|
12
|
+
if (at === -1 ||
|
|
13
|
+
spec.indexOf(ROOT_TEST, at + 1) !== -1 ||
|
|
14
|
+
spec.includes(`'${path}'`)) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const end = at + ROOT_TEST.length;
|
|
18
|
+
return `${spec.slice(0, end)}
|
|
19
|
+
it('${path} (GET)', () => {
|
|
20
|
+
return request(app.getHttpServer()).get('${path}').expect(200);
|
|
21
|
+
});
|
|
22
|
+
${spec.slice(end)}`;
|
|
23
|
+
}
|
|
@@ -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,12 @@ 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
39
|
/** `.gitignore`: missing lines appended, nothing else touched. */
|
|
40
40
|
export declare function mergeGitignore(text: string | undefined, additions: readonly string[]): Merge;
|
|
41
41
|
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* under an existing `allowBuilds:` are added there; otherwise the block is
|
|
45
|
-
* appended.
|
|
42
|
+
* Keys missing under an existing `allowBuilds:` are added there; otherwise the
|
|
43
|
+
* block is appended.
|
|
46
44
|
*/
|
|
47
45
|
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,6 @@ export function mergeEnvFile(text, entries, replaceConflicts) {
|
|
|
90
79
|
}
|
|
91
80
|
return { text: `${lines.join("\n")}\n`, conflicts };
|
|
92
81
|
}
|
|
93
|
-
/** `.gitignore`: missing lines appended, nothing else touched. */
|
|
94
82
|
export function mergeGitignore(text, additions) {
|
|
95
83
|
const lines = text === undefined || text === ""
|
|
96
84
|
? []
|
|
@@ -103,12 +91,6 @@ export function mergeGitignore(text, additions) {
|
|
|
103
91
|
conflicts: [],
|
|
104
92
|
};
|
|
105
93
|
}
|
|
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
94
|
export function mergePnpmWorkspace(text, packages) {
|
|
113
95
|
const quoted = (name) => /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(name) ? name : `"${name}"`;
|
|
114
96
|
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,7 +1,3 @@
|
|
|
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",
|
|
@@ -1,15 +1,18 @@
|
|
|
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;
|
|
14
17
|
/** The adapter package a project installs, e.g. `@aventara/prisma7-adapter`. */
|
|
15
18
|
readonly adapterPackage: string;
|
|
@@ -23,17 +26,17 @@ export type CatalogEntry = {
|
|
|
23
26
|
/** The one major `range` admits. */
|
|
24
27
|
readonly major: number;
|
|
25
28
|
};
|
|
26
|
-
/** `peerDependencies[orm.package]`: the ORM versions the adapter supports
|
|
29
|
+
/** `peerDependencies[orm.package]`: the ORM versions the adapter supports. */
|
|
27
30
|
readonly range: string;
|
|
28
|
-
/** Packages that identify the ORM family in a project
|
|
31
|
+
/** Packages that identify the ORM family in a project. */
|
|
29
32
|
readonly familyPackages: readonly string[];
|
|
30
|
-
/** Database providers, in the adapter's order; the first is the default
|
|
33
|
+
/** Database providers, in the adapter's order; the first is the default. */
|
|
31
34
|
readonly providers: readonly string[];
|
|
32
35
|
/** Provider → the driver adapter package for it. */
|
|
33
36
|
readonly drivers: Readonly<Record<string, string>>;
|
|
34
|
-
/** The ORM generators whose output the adapter reads
|
|
37
|
+
/** The ORM generators whose output the adapter reads. */
|
|
35
38
|
readonly generators: readonly string[];
|
|
36
|
-
/** The adapter's generate step: its one `bin
|
|
39
|
+
/** The adapter's generate step: its one `bin`. */
|
|
37
40
|
readonly bin: string;
|
|
38
41
|
};
|
|
39
42
|
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;
|
|
@@ -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
|
|
6
|
-
*
|
|
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, 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,7 @@ export async function runCli(argv, io) {
|
|
|
27
27
|
try {
|
|
28
28
|
const command = parseCliCommand(argv);
|
|
29
29
|
if (command.command === "help") {
|
|
30
|
-
io.stdout(USAGE);
|
|
30
|
+
io.stdout(command.topic === undefined ? USAGE : commandUsage(command.topic));
|
|
31
31
|
return 0;
|
|
32
32
|
}
|
|
33
33
|
if (command.command === "version") {
|
|
@@ -66,12 +66,6 @@ export async function runCli(argv, io) {
|
|
|
66
66
|
return 1;
|
|
67
67
|
}
|
|
68
68
|
}
|
|
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
69
|
export async function runFromProcess() {
|
|
76
70
|
const prompter = createReadlinePrompter({
|
|
77
71
|
input: process.stdin,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { type QuestionId } from "../wizard/wizard.questions.js";
|
|
2
2
|
/**
|
|
3
|
-
* The `aventara` command line
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
3
|
+
* The `aventara` command line: `new <name>` and `init`, each question's flag from
|
|
4
|
+
* the wizard's table, and the options that are not questions. Unknown, misplaced
|
|
5
|
+
* or repeated arguments are refused in one sentence; a flag's VALUE is the
|
|
6
|
+
* question's to judge, because what is valid (`--db`'s providers) depends on the
|
|
7
|
+
* catalog and on earlier answers.
|
|
8
8
|
*/
|
|
9
9
|
export type ScaffoldCommandName = "new" | "init";
|
|
10
10
|
export type ScaffoldCommand = {
|
|
@@ -12,13 +12,16 @@ export type ScaffoldCommand = {
|
|
|
12
12
|
/** Raw answers by question, from flags and `new`'s positional `<name>`. */
|
|
13
13
|
readonly given: Readonly<Partial<Record<QuestionId, string>>>;
|
|
14
14
|
readonly skipInstall: boolean;
|
|
15
|
-
/** `new` only: passed through to `nest new
|
|
15
|
+
/** `new` only: passed through to `nest new`. */
|
|
16
16
|
readonly skipGit: boolean;
|
|
17
|
-
/** Accepts each unanswered question's default and confirms overwrites
|
|
17
|
+
/** Accepts each unanswered question's default and confirms overwrites. */
|
|
18
18
|
readonly yes: boolean;
|
|
19
19
|
};
|
|
20
|
-
export type CliCommand =
|
|
20
|
+
export type CliCommand =
|
|
21
|
+
/** `--help`; with `topic`, `aventara <topic> --help` (pilot.1). */
|
|
22
|
+
{
|
|
21
23
|
readonly command: "help";
|
|
24
|
+
readonly topic?: ScaffoldCommandName;
|
|
22
25
|
} | {
|
|
23
26
|
readonly command: "version";
|
|
24
27
|
} | ScaffoldCommand;
|
|
@@ -26,6 +29,8 @@ export type CliCommand = {
|
|
|
26
29
|
export declare class CliCommandError extends Error {
|
|
27
30
|
readonly name = "CliCommandError";
|
|
28
31
|
}
|
|
32
|
+
/** `aventara <command> --help` (pilot.1): that command's usage alone. */
|
|
33
|
+
export declare function commandUsage(command: ScaffoldCommandName): string;
|
|
29
34
|
export declare const USAGE: string;
|
|
30
35
|
/** @throws CliCommandError when `argv` is not a command this CLI has. */
|
|
31
36
|
export declare function parseCliCommand(argv: readonly string[]): CliCommand;
|