@aventara/cli 1.0.0-rc.0 → 1.0.0-rc.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/CHANGELOG.md +260 -0
- package/README.md +61 -42
- package/dist/apply/conflict.confirmer.js +4 -4
- package/dist/cli.js +24 -14
- package/dist/command/command.parser.js +44 -10
- package/dist/plan/project.planner.js +11 -4
- package/dist/project/project.inspector.js +8 -8
- package/dist/run/init.orchestrator.js +8 -8
- package/dist/run/new.orchestrator.js +5 -5
- package/dist/run/skill.orchestrator.js +54 -0
- package/dist/skill/agent-skill.generated.js +7 -0
- package/dist/skill/agent-skill.planner.js +116 -0
- package/dist/templates/docs.links.js +2 -1
- package/dist/templates/prisma7/prisma7.templates.js +31 -8
- package/dist/templates/template.registry.js +1 -1
- package/dist/wizard/answer.resolver.js +5 -5
- package/dist/wizard/readline.prompter.js +3 -3
- package/dist/wizard/wizard.questions.js +9 -9
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,265 @@
|
|
|
1
1
|
# @aventara/cli
|
|
2
2
|
|
|
3
|
+
## 1.0.0-rc.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- `aventara init`'s refusal of a TypeScript 7 project names the supported range: "use TypeScript 5.7 to 6" (it said
|
|
8
|
+
"use TypeScript 6 or 5"). The Prisma adapter's `typescript` peer range starts at 5.7.0 now.
|
|
9
|
+
- An agent skill for your AI coding agent. `aventara new` and `aventara init` write `.claude/skills/aventara/`:
|
|
10
|
+
`SKILL.md` (the Agent Skills format Claude Code reads: when to use it, and one rule per task) and
|
|
11
|
+
`reference/config.md`, `reference/stages.md`, `reference/recipes.md` and `reference/errors.md` (`@aventara/core`'s
|
|
12
|
+
`ERRORS.md`), for the Aventara version they install. They also add a short Aventara section to `AGENTS.md`, between
|
|
13
|
+
`<!-- aventara:start -->` and `<!-- aventara:end -->`, for agents that read `AGENTS.md`: an existing `AGENTS.md`
|
|
14
|
+
keeps everything else, and `CLAUDE.md` is never written.
|
|
15
|
+
- `aventara skill` refreshes those files for the `@aventara/core` installed in the project (its version and its
|
|
16
|
+
`ERRORS.md`; it runs none of the project's packages). It rewrites the skill files it wrote, keeps one you changed
|
|
17
|
+
and says so (`--force` replaces it), and replaces only the marked section of `AGENTS.md`.
|
|
18
|
+
- `aventara new` and `aventara init` write an `aventara:prepare` script that also passes
|
|
19
|
+
`--config src/aventara.config.ts`, so the generated types know which fields your configuration computes: a
|
|
20
|
+
variant's guards, pipes and filters may leave exactly those out of a create. A project set up before keeps working;
|
|
21
|
+
add the flag to its script to get the precise types.
|
|
22
|
+
- The agent skill says that a `Date` or bytes value a stage is told is that stage's own copy, so changing it in place
|
|
23
|
+
changes nothing another stage sees, what is written or what the caller receives; and that a `compute.read` stage with
|
|
24
|
+
no `dependsOn` computes from the same inputs for every row, so its value usually belongs in response metadata or
|
|
25
|
+
`ctx.$state`. In rc.1 it said neither.
|
|
26
|
+
- The agent skill says that a `compute.read` stage is told the row frozen: it returns its field's value, and writing
|
|
27
|
+
into `record` answers `A3000`. Its error reference lists a nested write that writes its relation back (`A2004`,
|
|
28
|
+
`V1012`).
|
|
29
|
+
- The agent skill says that a computed value its field's `format` refuses (a relative path for `format: "url"`)
|
|
30
|
+
answers `A3000`, as a value of the wrong type does.
|
|
31
|
+
- Released with the other Aventara packages, and `aventara new` and `aventara init` install them at 1.0.0-rc.2.
|
|
32
|
+
|
|
33
|
+
## 1.0.0-rc.1
|
|
34
|
+
|
|
35
|
+
### Patch Changes
|
|
36
|
+
|
|
37
|
+
- 7e3d06f: Breaking: the configuration has a new shape. Every layer (the root, `application` and `client`) now holds the
|
|
38
|
+
same six keys: `exposed`, `transactionsStyle`, `limits`, `pipelines`, `resources` and `exemptions`. Everything about
|
|
39
|
+
one Resource lives under `resources.<Resource>`: its fields, and each field's `definition` and `pipelines`. The old
|
|
40
|
+
keys are refused when the framework starts, with a message that names where each one moved:
|
|
41
|
+
|
|
42
|
+
| Before (rc.0) | Now (rc.1) |
|
|
43
|
+
| ------------------------------------------------------- | ----------------------------------------------------------------------------------- |
|
|
44
|
+
| `fields: { Post: { url: { type, nullable, … } } }` | `resources: { Post: { fields: { url: { definition: { type, nullable, … } } } } }` |
|
|
45
|
+
| `behaviors: { Post: { fields: { slug: { create } } } }` | `resources: { Post: { fields: { slug: { pipelines: { compute: { create } } } } } }` |
|
|
46
|
+
| `transactions: "none"` | `transactionsStyle: "none"` |
|
|
47
|
+
| `restrictions` | `exposed` + `exemptions` (see below) |
|
|
48
|
+
| `@aventara/core/json` | `@aventara/core/avz` |
|
|
49
|
+
|
|
50
|
+
Before:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
createFramework({
|
|
54
|
+
api: "/api",
|
|
55
|
+
adapter,
|
|
56
|
+
transactions: "interactive",
|
|
57
|
+
fields: {
|
|
58
|
+
Post: {
|
|
59
|
+
url: {
|
|
60
|
+
type: { scalar: "string" },
|
|
61
|
+
nullable: false,
|
|
62
|
+
list: false,
|
|
63
|
+
lifecycle: ["VIRTUAL"],
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
behaviors: {
|
|
68
|
+
Post: {
|
|
69
|
+
fields: {
|
|
70
|
+
url: {
|
|
71
|
+
read: {
|
|
72
|
+
dependsOn: ["slug"],
|
|
73
|
+
compute: ({ record }) => `/p/${record.slug}`,
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
After:
|
|
83
|
+
|
|
84
|
+
```ts
|
|
85
|
+
createFramework({
|
|
86
|
+
api: "/api",
|
|
87
|
+
adapter,
|
|
88
|
+
transactionsStyle: "interactive",
|
|
89
|
+
resources: {
|
|
90
|
+
Post: {
|
|
91
|
+
fields: {
|
|
92
|
+
url: {
|
|
93
|
+
definition: {
|
|
94
|
+
type: { scalar: "string" },
|
|
95
|
+
nullable: false,
|
|
96
|
+
list: false,
|
|
97
|
+
lifecycle: ["VIRTUAL"],
|
|
98
|
+
},
|
|
99
|
+
pipelines: {
|
|
100
|
+
compute: {
|
|
101
|
+
read: {
|
|
102
|
+
dependsOn: ["slug"],
|
|
103
|
+
compute: ({ record }) => `/p/${record.slug}`,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
What else changed:
|
|
115
|
+
|
|
116
|
+
- A field is defined at the root only. A `definition` in `application` or `client` is refused
|
|
117
|
+
(`CONFIG_FORBIDDEN_IN_OVERLAY`). Define the field
|
|
118
|
+
at the root; `application` and `client` can still add compute stages to it.
|
|
119
|
+
- Computed values chain. When the root and an overlay both compute a field, the root's stage runs first and the
|
|
120
|
+
overlay's receives its result as `value`. A stage can no longer be turned off with `false`. Returning `undefined`
|
|
121
|
+
keeps the current value. An upsert computes its create branch with `compute.create` and its update branch with
|
|
122
|
+
`compute.update`.
|
|
123
|
+
- A discovered field keeps the adapter's facts. On a field the adapter discovered you may restate its own `type`,
|
|
124
|
+
`nullable` and `list`, and give a Json field its shape (`definition.json`). Setting a different type, any `format`
|
|
125
|
+
or any `capabilities` on it is refused. `format` is for fields you add, and it must be one of zod's named string
|
|
126
|
+
formats (`"email"`, `"url"`, `"uuid"`, …); any other name is a type error and refused at startup.
|
|
127
|
+
- A misplaced key is a type error. In a configuration written inline in `createFramework({ ... })` or
|
|
128
|
+
`AventaraModule.forRoot`, a key that has no place there (a typo, an old key, `compute` on a relation field, a field
|
|
129
|
+
name neither your schema nor the root has) is now a type error at that key, and still refused at startup.
|
|
130
|
+
- A Resource whose name starts with `$` is refused at startup (`$` names are the framework's).
|
|
131
|
+
- The Json shape builder moved from `@aventara/core/json` to `@aventara/core/avz`. Change the import; `AvZ` is
|
|
132
|
+
the same.
|
|
133
|
+
|
|
134
|
+
`exposed` and `exemptions`, which replace `restrictions`, have their own entry.
|
|
135
|
+
|
|
136
|
+
- 6317811: Breaking: `exposed` and `exemptions` replace `restrictions`. Every layer (the root, `application` and `client`)
|
|
137
|
+
says what it exposes with two keys: `exposed`, the layer's default, and `exemptions`, the entries that are the
|
|
138
|
+
opposite of it.
|
|
139
|
+
|
|
140
|
+
- `exposed: true` (the root's default) exposes everything the layer above offers, and `exemptions` lists what to
|
|
141
|
+
hide. `exposed: false` exposes nothing, and `exemptions` lists the only things to expose.
|
|
142
|
+
- In `exemptions`, `true` exempts an entry whole, `false` and `[]` exempt nothing, a list exempts the listed
|
|
143
|
+
entries one level down, and an object goes one level deeper. A Resource takes `true`, `false` or
|
|
144
|
+
`{ operations, fields }`. Operations are nested: `operations: { find: ["many"], update: { unique: true } }`.
|
|
145
|
+
A field takes a list of capability names, or an object per capability whose token lists name filter operators,
|
|
146
|
+
directives, reducers or pagination kinds.
|
|
147
|
+
- A list now always means "exempt these". rc.0's `filter: ["equals"]` meant "keep only `equals`"; under
|
|
148
|
+
`exposed: true` the same intent is the list of the operators to remove.
|
|
149
|
+
- Overlays only narrow. `application` and `client` start from the root's view. An overlay can no longer bring
|
|
150
|
+
back what the root hid (rc.0's `hidden: false`); that is now refused at startup, and the message names the other
|
|
151
|
+
overlay to hide it in instead. Expose an entry at the root and hide it in the overlay that must not see it.
|
|
152
|
+
- An overlay that sets `exemptions` must also set its own `exposed`.
|
|
153
|
+
- Relation positions are the adapter's: an exemption can no longer narrow where a relation is projected or written.
|
|
154
|
+
- A unique operation (`find.unique`, `update.unique`, `delete.unique`, `upsert.unique`) needs an identifier. Picking
|
|
155
|
+
one without the identifier's fields, or hiding every identifier while one stays exposed, is refused at startup
|
|
156
|
+
with `COMPILER_MISSING_IDENTIFIER` instead of the operation silently disappearing.
|
|
157
|
+
|
|
158
|
+
Every name in `exemptions` must exist, in both styles: a Resource, field, family, variant, capability or token the
|
|
159
|
+
adapter does not offer is refused at startup with its path (`COMPILER_UNKNOWN_RESOURCE`, `COMPILER_UNKNOWN_FIELD`,
|
|
160
|
+
`COMPILER_UNAVAILABLE_OPERATION`, `COMPILER_UNAVAILABLE_CAPABILITY`), and in a configuration written inline in
|
|
161
|
+
`createFramework({ ... })` it is a type error first. Under `exposed: true`, hiding in an overlay what the root
|
|
162
|
+
already hid does nothing.
|
|
163
|
+
|
|
164
|
+
Before (rc.0):
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
createFramework({
|
|
168
|
+
api: "/api",
|
|
169
|
+
adapter,
|
|
170
|
+
restrictions: {
|
|
171
|
+
User: {
|
|
172
|
+
fields: {
|
|
173
|
+
passwordHash: { hidden: true },
|
|
174
|
+
email: { capabilities: { filter: ["equals"] } },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
client: {
|
|
179
|
+
restrictions: {
|
|
180
|
+
AuditLog: { hidden: true },
|
|
181
|
+
Post: { operations: { delete: { unique: false } } },
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
After (rc.1):
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
createFramework({
|
|
191
|
+
api: "/api",
|
|
192
|
+
adapter,
|
|
193
|
+
// The root: `exposed` defaults to true, so these are hidden from every origin.
|
|
194
|
+
exemptions: {
|
|
195
|
+
resources: {
|
|
196
|
+
User: {
|
|
197
|
+
fields: {
|
|
198
|
+
passwordHash: true,
|
|
199
|
+
email: { filter: ["contains", "startsWith"] }, // remove every operator but `equals`
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
client: {
|
|
205
|
+
exposed: true,
|
|
206
|
+
exemptions: {
|
|
207
|
+
resources: {
|
|
208
|
+
AuditLog: true,
|
|
209
|
+
Post: { operations: { delete: ["unique"] } },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Or expose only what a frontend calls:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
client: {
|
|
220
|
+
exposed: false,
|
|
221
|
+
exemptions: {
|
|
222
|
+
resources: {
|
|
223
|
+
Post: {
|
|
224
|
+
operations: { find: ["many", "unique"], create: ["one"] },
|
|
225
|
+
fields: { id: ["select"], title: ["select", "create"], body: ["select", "create"] },
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
`aventara init` and `aventara new` now write `exposed: true` into `src/aventara.config.ts`, with a comment that
|
|
233
|
+
links the exposure documentation.
|
|
234
|
+
|
|
235
|
+
- 9c52482: A `$connect` to a row that does not exist now answers `404 A2003` (not found), the same as reading a missing row.
|
|
236
|
+
Before, it answered `422 A2004`. This covers a to-one `$connect` in `create`, `update` and `upsert`, a to-many
|
|
237
|
+
`$connect`, and the same writes inside a transaction plan, where `cause.operation` names the step. The message is
|
|
238
|
+
unchanged: "The operation's arguments referenced a record that does not exist." On `update.first`, the same mistake
|
|
239
|
+
answered `A2014` before, as if the row had changed underneath; it now answers `A2003` too, and a duplicate unique value
|
|
240
|
+
written through `update.first` answers `409 A2008` instead of `A3001`.
|
|
241
|
+
|
|
242
|
+
Clearer top-level messages for an unknown Resource or operation. `A2001` now says "The requested Resource is not
|
|
243
|
+
available." and `A2002` says "The requested operation is not available on this Resource.", instead of "Operation
|
|
244
|
+
arguments failed framework validation.", which stays the message of `A2004`, `A2007` and `A2009`. A transaction step
|
|
245
|
+
whose operation is outside the standard grammar now carries the same issue message as a single call, naming the
|
|
246
|
+
operation and the Resource (for example `Operation "dance.one" is not available on Resource "posts".`), instead of
|
|
247
|
+
"Operation is not part of the standard operation grammar."
|
|
248
|
+
|
|
249
|
+
A `$ref` with no `fingerprint` is `V1000` (required value missing) and answers `422 A2004`. Before, it was `V1017`
|
|
250
|
+
(fingerprint mismatch) under `A2007`. A fingerprint that is present but wrong is still `V1017`.
|
|
251
|
+
|
|
252
|
+
`aventara init` and `aventara new` link to the documentation at https://aventara-framework.vercel.app. The old
|
|
253
|
+
address redirects, so links already written into projects keep working.
|
|
254
|
+
|
|
255
|
+
- 8db76e7: `aventara new` and `aventara init` write a `diagnostics` sink into `src/aventara.config.ts`. When something fails
|
|
256
|
+
inside the server, the caller gets a plain 500 and the Nest log said only which request failed; the raw error went to
|
|
257
|
+
a sink the scaffold did not set. Now the sink logs the raw error with its stack through Nest's `Logger` (context
|
|
258
|
+
`Aventara`), at error level, right after that line. Replace it with your own reporter when you have one.
|
|
259
|
+
|
|
260
|
+
- `aventara new` and `aventara init` write `api: '/api'` into `src/aventara.config.ts` (the root key was renamed from
|
|
261
|
+
`entrypoint`).
|
|
262
|
+
|
|
3
263
|
## 1.0.0-rc.0
|
|
4
264
|
|
|
5
265
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -6,10 +6,11 @@ The `aventara` command: it scaffolds an Aventara server on NestJS 12.
|
|
|
6
6
|
npm i -g @aventara/cli@rc # or run it once: npx @aventara/cli@rc <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
|
+
aventara skill # refresh the agent skill for the installed @aventara/core
|
|
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
|
|
12
|
+
It is run once, globally or through `npx`; it is not a dependency of the projects it writes. The frontend's side
|
|
13
|
+
(`framework.client.ts` and the generated client) belongs to `@aventara/client` (`npx @aventara/client@rc init`).
|
|
13
14
|
|
|
14
15
|
## `aventara new <name>`
|
|
15
16
|
|
|
@@ -17,22 +18,24 @@ Runs the pinned `nest new` (`npx -y @nestjs/cli@12.0.8 new …`, or `pnpm dlx
|
|
|
17
18
|
`src/app.module.ts` is what that version writes, then runs exactly `aventara init`'s pipeline over the new project and
|
|
18
19
|
installs it. A directory that exists and is not empty is refused before anything runs.
|
|
19
20
|
|
|
20
|
-
Once installed, the project gets its first git commit, `Initial Aventara Scaffold
|
|
21
|
-
(it is in `.gitignore`). No commit is made, and one line says why, when git is not installed, when git has no
|
|
21
|
+
Once installed, the project gets its first git commit, `Initial Aventara Scaffold`, with the lockfile in it and
|
|
22
|
+
without `.env` (it is in `.gitignore`). No commit is made, and one line says why, when git is not installed, when git has no
|
|
22
23
|
`user.name`/`user.email`, or when the directory is already inside a git repository (a monorepo: commit it there).
|
|
23
24
|
`--skip-git` makes no repository and no commit.
|
|
24
25
|
|
|
25
26
|
## `aventara init`
|
|
26
27
|
|
|
27
|
-
Runs inside an existing
|
|
28
|
-
lockfile, a project already initialized
|
|
28
|
+
Runs inside an existing NestJS 12 project. It refuses a directory with no `package.json`, another Nest major,
|
|
29
|
+
TypeScript 7, a yarn-only lockfile, or a project already initialized.
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
In a project with no ORM yet, the ORM and the database are asked (or taken from the flags), and everything below is
|
|
32
|
+
written.
|
|
33
|
+
|
|
34
|
+
In a project that already uses Prisma, the installed Prisma must be one an Aventara adapter supports, or the run is
|
|
35
|
+
refused in one sentence with nothing written. Today that is Prisma 7 (`^7.10.0`) on SQLite or PostgreSQL, with a driver
|
|
36
|
+
adapter and the `prisma-client` generator. Your schema, Prisma config and service are reused and never written, and
|
|
37
|
+
your `.env` only gains a `CORS_ORIGINS` line at its end when it has none. Init finds the class extending `PrismaClient`
|
|
38
|
+
and the module exporting it, or asks.
|
|
36
39
|
|
|
37
40
|
### Questions and flags
|
|
38
41
|
|
|
@@ -42,20 +45,20 @@ possible answer is shown, not asked.
|
|
|
42
45
|
|
|
43
46
|
| Flag | Question | Default |
|
|
44
47
|
|---|---|---|
|
|
45
|
-
| `<name>` (`new` only) | The project directory. |
|
|
48
|
+
| `<name>` (`new` only) | The project directory. | required |
|
|
46
49
|
| `--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 |
|
|
47
50
|
| `--db <provider>` | The database: the adapter's providers (`sqlite`, `postgresql`). | `sqlite` |
|
|
48
|
-
| `--package-manager <npm\|pnpm>` |
|
|
51
|
+
| `--package-manager <npm\|pnpm>` | The package manager. | the lockfile's, else the one that launched the CLI, else npm |
|
|
49
52
|
| `--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` |
|
|
50
53
|
| `--prisma-module <path#Export>` (`init`) | The Nest module that provides and exports it. | the one found |
|
|
51
54
|
| `--skip-install` | Write the files; print the install instead of running it. | |
|
|
52
55
|
| `--skip-git` (`new`) | Passed to `nest new`. | |
|
|
53
|
-
| `-y`, `--yes` | Accept every unanswered default
|
|
56
|
+
| `-y`, `--yes` | Accept every unanswered default, and replace existing content that differs. | |
|
|
54
57
|
| `--verbose` | Show the output of `nest new` and the install as they run. | |
|
|
55
58
|
|
|
56
|
-
While it works, each step is shown on stderr
|
|
57
|
-
|
|
58
|
-
`
|
|
59
|
+
While it works, each step is shown on stderr, such as `[3/4] Installing dependencies (npm install)`. On a terminal it
|
|
60
|
+
has a spinner and the seconds so far; in a pipe, in CI (`CI`) or with `NO_COLOR` it is one plain line per step
|
|
61
|
+
(`… done (12.4s)`). The output of `nest new` and the install is shown only if it fails, or with `--verbose`.
|
|
59
62
|
|
|
60
63
|
Existing content that differs from what would be written (a `.env` key, a script, a file) is listed, and replaced only
|
|
61
64
|
when you confirm, or with `--yes`; where nobody can be asked, the run stops and touches nothing.
|
|
@@ -64,17 +67,19 @@ when you confirm, or with `--yes`; where nobody can be asked, the run stops and
|
|
|
64
67
|
|
|
65
68
|
| Path | Owner afterwards |
|
|
66
69
|
|---|---|
|
|
67
|
-
| `prisma/schema.prisma
|
|
68
|
-
| `prisma.config.ts
|
|
69
|
-
| `src/prisma.service.ts
|
|
70
|
-
| `src/aventara.config.ts
|
|
71
|
-
| `src/app.module.ts
|
|
72
|
-
| `test/app.e2e-spec.ts
|
|
73
|
-
| `src/main.ts
|
|
70
|
+
| `prisma/schema.prisma`: `prisma-client` generating into `src/generated/prisma`, a `User` model | you |
|
|
71
|
+
| `prisma.config.ts`: loads `.env` itself (Prisma does not) | you |
|
|
72
|
+
| `src/prisma.service.ts`: `PrismaService` extending the client with the driver adapter, refusing a missing `DATABASE_URL` before the server listens; `PrismaModule` | you |
|
|
73
|
+
| `src/aventara.config.ts`: `aventaraConfig(prisma)`: the `api` mount path (`/api`), a `diagnostics` sink that logs the raw error of a server failure, with its stack, through Nest's `Logger`, and the `exposed`/`exemptions`, `pipelines` and `resources` you add | you |
|
|
74
|
+
| `src/app.module.ts`: one edit that adds the imports, `PrismaModule` and `AventaraModule.forRootAsync({ … })`; printed instead when the file is not the shape expected | you |
|
|
75
|
+
| `test/app.e2e-spec.ts`: one edit that adds a `GET /api/_contract` → 200 test after Nest's `GET /` test; left alone when the file is not Nest's | you |
|
|
76
|
+
| `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 |
|
|
74
77
|
| `.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 |
|
|
75
|
-
| `package.json
|
|
76
|
-
| `pnpm-workspace.yaml` (pnpm only)
|
|
77
|
-
| `src/generated/prisma/**`, `src/generated/aventara/discovery.artifact.ts` |
|
|
78
|
+
| `package.json`: `@aventara/*` at this CLI's version, `prisma`, `@prisma/client` and the driver at `7.10.0`; `aventara:prepare`, run by `postinstall` and by `prebuild`, `prestart`, `prestart:dev` and `prestart:debug`, and the `.env` on the start scripts and `test:e2e` (`--env-file`, `--env-file-if-exists`) | you |
|
|
79
|
+
| `pnpm-workspace.yaml` (pnpm only): `allowBuilds` for Prisma's and SQLite's install scripts | you |
|
|
80
|
+
| `src/generated/prisma/**`, `src/generated/aventara/discovery.artifact.ts` | generated by `aventara:prepare` (run on every install, build and start, so a schema change needs only a restart); never edit |
|
|
81
|
+
| `.claude/skills/aventara/` (`SKILL.md`, `reference/*.md`, `.aventara-skill.json`): the agent skill your AI coding agent reads | Aventara's: `aventara skill` refreshes it, and keeps a file you changed unless `--force` |
|
|
82
|
+
| `AGENTS.md`: an Aventara section between `<!-- aventara:start -->` and `<!-- aventara:end -->`, appended to an existing file | you, except the marked section |
|
|
78
83
|
|
|
79
84
|
### Next steps it prints
|
|
80
85
|
|
|
@@ -111,23 +116,37 @@ curl -s -X POST http://localhost:3000/api/_resources/User/find/many \
|
|
|
111
116
|
Leave out an identity header and the answer is `400 A2000`, naming the header that is missing. The hash changes
|
|
112
117
|
whenever the schema or the configuration does: fetch it again, and regenerate the client.
|
|
113
118
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
Resource keys are the model names, as written. `model User` is `User` everywhere: on the wire
|
|
120
|
+
(`/_resources/User/find/many`) and in the generated client (`avClient.User.find.many({})`). Nothing is renamed,
|
|
121
|
+
lower-cased or pluralized.
|
|
122
|
+
|
|
123
|
+
Results are read-only. A list result is a `readonly` array, so type it `readonly User[]`:
|
|
124
|
+
`const users: readonly User[] = await avClient.User.find.many({});`.
|
|
125
|
+
|
|
126
|
+
## `aventara skill`
|
|
127
|
+
|
|
128
|
+
Writes the agent skill for the `@aventara/core` installed in the project in this directory: its version, and its
|
|
129
|
+
`ERRORS.md` as `reference/errors.md`. It reads those files and runs none of the project's packages. A skill file it
|
|
130
|
+
wrote before is replaced; one you changed is kept, with a warning, unless you pass `--force`. In `AGENTS.md` only the
|
|
131
|
+
text between the Aventara markers is replaced (or the section appended), and `CLAUDE.md` is never written. Run it with
|
|
132
|
+
the CLI at your `@aventara/core` version: `npx @aventara/cli@<version> skill`.
|
|
119
133
|
|
|
120
134
|
## Supported
|
|
121
135
|
|
|
122
|
-
NestJS 12
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
136
|
+
- NestJS 12.
|
|
137
|
+
- Node `^22.18.0 || >=24.2.0`. On any other Node the bin refuses in one sentence naming that range. On Node 22 use
|
|
138
|
+
npm ≥ 11, because Node 22's bundled npm 10 cannot install Nest 12's own scaffold. A CommonJS project's jest e2e needs
|
|
139
|
+
Node ≥ 24.9, which is Nest's and Jest's limit.
|
|
140
|
+
- npm and pnpm. Yarn is not supported.
|
|
141
|
+
- ESM and CommonJS projects.
|
|
142
|
+
- Prisma 7 (`^7.10.0`) on SQLite and PostgreSQL.
|
|
143
|
+
|
|
144
|
+
The ORMs, majors, databases and drivers on offer are generated from the Aventara adapter packages that exist
|
|
145
|
+
(`src/catalog/adapter.catalog.generated.ts`), never listed by hand, so a future `@aventara/prisma8-adapter` appears
|
|
146
|
+
once it exists.
|
|
129
147
|
|
|
130
148
|
## License
|
|
131
149
|
|
|
132
|
-
PolyForm Shield 1.0.0 with an additional permission
|
|
133
|
-
use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
150
|
+
PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
|
|
151
|
+
may not use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
152
|
+
`LICENSE-ADDITIONAL-PERMISSION.md`.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export class
|
|
2
|
-
name = "
|
|
1
|
+
export class AvConflictsNotConfirmedError extends Error {
|
|
2
|
+
name = "AvConflictsNotConfirmedError";
|
|
3
3
|
}
|
|
4
4
|
export function conflictWarning(conflicts) {
|
|
5
5
|
return `${conflicts.join(", ")} already ${conflicts.length === 1 ? "has" : "have"} other content, and initializing will replace ${conflicts.length === 1 ? "it" : "them"}`;
|
|
@@ -10,7 +10,7 @@ export async function confirmConflicts(conflicts, consent) {
|
|
|
10
10
|
}
|
|
11
11
|
const them = conflicts.length === 1 ? "it" : "them";
|
|
12
12
|
if (!consent.prompter.interactive) {
|
|
13
|
-
throw new
|
|
13
|
+
throw new AvConflictsNotConfirmedError(`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}`);
|
|
14
14
|
}
|
|
15
15
|
consent.prompter.say(`aventara: warning: ${conflictWarning(conflicts)}.`);
|
|
16
16
|
const answer = await consent.prompter.ask({
|
|
@@ -19,6 +19,6 @@ export async function confirmConflicts(conflicts, consent) {
|
|
|
19
19
|
defaultValue: "N",
|
|
20
20
|
});
|
|
21
21
|
if (!/^y(?:es)?$/i.test(answer.trim())) {
|
|
22
|
-
throw new
|
|
22
|
+
throw new AvConflictsNotConfirmedError(`cancelled, and nothing was touched; run again and answer yes to replace ${them}`);
|
|
23
23
|
}
|
|
24
24
|
}
|
package/dist/cli.js
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import { AvConflictsNotConfirmedError } from "./apply/conflict.confirmer.js";
|
|
3
3
|
import { ADAPTER_CATALOG } from "./catalog/adapter.catalog.generated.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { AvCliCommandError, commandUsage, parseCliCommand, usage, } from "./command/command.parser.js";
|
|
5
|
+
import { AvProjectRefusedError } from "./project/project.inspector.js";
|
|
6
6
|
import { runCommand } from "./run/command.runner.js";
|
|
7
|
-
import {
|
|
7
|
+
import { AvInstallFailedError, runInit } from "./run/init.orchestrator.js";
|
|
8
8
|
import { runNew } from "./run/new.orchestrator.js";
|
|
9
|
+
import { AvSkillRefusedError, runSkill } from "./run/skill.orchestrator.js";
|
|
9
10
|
import { progressModeOf } from "./run/step.reporter.js";
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
11
|
+
import { AvUnansweredQuestionsError, } from "./wizard/answer.resolver.js";
|
|
12
|
+
import { AvWizardCancelledError, createReadlinePrompter, } from "./wizard/readline.prompter.js";
|
|
13
|
+
import { AvWizardAnswerError } from "./wizard/wizard.questions.js";
|
|
13
14
|
const REFUSALS = [
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
AvCliCommandError,
|
|
16
|
+
AvWizardAnswerError,
|
|
17
|
+
AvUnansweredQuestionsError,
|
|
18
|
+
AvWizardCancelledError,
|
|
19
|
+
AvProjectRefusedError,
|
|
20
|
+
AvConflictsNotConfirmedError,
|
|
21
|
+
AvInstallFailedError,
|
|
22
|
+
AvSkillRefusedError,
|
|
21
23
|
];
|
|
22
24
|
const MANIFEST = new URL("../package.json", import.meta.url);
|
|
23
25
|
function ownVersion() {
|
|
@@ -38,6 +40,14 @@ export async function runCli(argv, io) {
|
|
|
38
40
|
io.stdout(`${ownVersion()}\n`);
|
|
39
41
|
return 0;
|
|
40
42
|
}
|
|
43
|
+
if (command.command === "skill") {
|
|
44
|
+
return runSkill(command, {
|
|
45
|
+
directory: io.cwd,
|
|
46
|
+
stdout: io.stdout,
|
|
47
|
+
stderr: io.stderr,
|
|
48
|
+
cliVersion: ownVersion(),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
41
51
|
if (command.command === "init") {
|
|
42
52
|
return await runInit(command, {
|
|
43
53
|
directory: io.cwd,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { INIT_FLAG_QUESTIONS, NEW_QUESTIONS, ormAliasesSentence, } from "../wizard/wizard.questions.js";
|
|
2
|
-
export class
|
|
3
|
-
name = "
|
|
2
|
+
export class AvCliCommandError extends Error {
|
|
3
|
+
name = "AvCliCommandError";
|
|
4
4
|
}
|
|
5
5
|
const OPTIONS = [
|
|
6
6
|
{
|
|
@@ -72,7 +72,18 @@ On a terminal every unanswered question is asked; anywhere else, pass its flag
|
|
|
72
72
|
or --yes, or the run stops before writing anything.
|
|
73
73
|
`;
|
|
74
74
|
}
|
|
75
|
+
const SKILL_SUMMARY = "Write or refresh the agent skill (.claude/skills/aventara/) and the Aventara section of AGENTS.md, for the installed @aventara/core.";
|
|
76
|
+
const SKILL_USAGE = `Usage: aventara skill [options]
|
|
77
|
+
|
|
78
|
+
${SKILL_SUMMARY}
|
|
79
|
+
|
|
80
|
+
Options:
|
|
81
|
+
--force also replace skill files you changed (AGENTS.md outside the Aventara section is never touched)
|
|
82
|
+
`;
|
|
75
83
|
export function commandUsage(command, catalog) {
|
|
84
|
+
if (command === "skill") {
|
|
85
|
+
return SKILL_USAGE;
|
|
86
|
+
}
|
|
76
87
|
return `Usage: ${SYNOPSIS[command]}
|
|
77
88
|
|
|
78
89
|
${SUMMARY[command]}
|
|
@@ -92,6 +103,8 @@ ${usageLines("new", catalog)}
|
|
|
92
103
|
${SYNOPSIS.init} ${SUMMARY.init}
|
|
93
104
|
${usageLines("init", catalog)}
|
|
94
105
|
|
|
106
|
+
aventara skill [--force] ${SKILL_SUMMARY}
|
|
107
|
+
|
|
95
108
|
aventara --help Print this and exit 0.
|
|
96
109
|
aventara <command> --help Print that command's usage and exit 0.
|
|
97
110
|
aventara --version, -v Print this CLI's version and exit 0.
|
|
@@ -101,7 +114,7 @@ ${asked(catalog)}`;
|
|
|
101
114
|
export function parseCliCommand(argv) {
|
|
102
115
|
const [command, ...rest] = argv;
|
|
103
116
|
if (command === undefined) {
|
|
104
|
-
throw new
|
|
117
|
+
throw new AvCliCommandError(`a command is required: new, init or skill; ${HELP}`);
|
|
105
118
|
}
|
|
106
119
|
if (command === "--help" || command === "-h") {
|
|
107
120
|
return { command: "help" };
|
|
@@ -109,8 +122,11 @@ export function parseCliCommand(argv) {
|
|
|
109
122
|
if (command === "--version" || command === "-v") {
|
|
110
123
|
return { command: "version" };
|
|
111
124
|
}
|
|
125
|
+
if (command === "skill") {
|
|
126
|
+
return parseSkillCommand(rest);
|
|
127
|
+
}
|
|
112
128
|
if (command !== "new" && command !== "init") {
|
|
113
|
-
throw new
|
|
129
|
+
throw new AvCliCommandError(`unknown command ${JSON.stringify(command)}: the commands are new, init and skill; ${HELP}`);
|
|
114
130
|
}
|
|
115
131
|
if (rest.includes("--help") || rest.includes("-h")) {
|
|
116
132
|
return { command: "help", topic: command };
|
|
@@ -124,7 +140,7 @@ export function parseCliCommand(argv) {
|
|
|
124
140
|
const argument = rest[index];
|
|
125
141
|
if (!argument.startsWith("-")) {
|
|
126
142
|
if (positional === undefined || given[positional.id] !== undefined) {
|
|
127
|
-
throw new
|
|
143
|
+
throw new AvCliCommandError(`unexpected argument ${JSON.stringify(argument)}: aventara ${command} takes ${positional === undefined ? "no" : "one"} positional argument; ${HELP}`);
|
|
128
144
|
}
|
|
129
145
|
given[positional.id] = argument;
|
|
130
146
|
continue;
|
|
@@ -137,20 +153,20 @@ export function parseCliCommand(argv) {
|
|
|
137
153
|
const canonical = question?.flag ?? option?.flag ?? flag;
|
|
138
154
|
if (question === undefined && option === undefined) {
|
|
139
155
|
const elsewhere = ["new", "init"].filter((other) => QUESTIONS[other].some((candidate) => candidate.flag === flag));
|
|
140
|
-
throw new
|
|
156
|
+
throw new AvCliCommandError(elsewhere.length === 0
|
|
141
157
|
? `unknown flag ${flag}; ${HELP}`
|
|
142
158
|
: `${flag} is a flag of aventara ${elsewhere.join(" and ")}, not aventara ${command}; ${HELP}`);
|
|
143
159
|
}
|
|
144
160
|
if (option !== undefined && !option.commands.includes(command)) {
|
|
145
|
-
throw new
|
|
161
|
+
throw new AvCliCommandError(`${option.flag} is a flag of aventara ${option.commands.join(" and ")}, not aventara ${command}; ${HELP}`);
|
|
146
162
|
}
|
|
147
163
|
if (seen.has(canonical)) {
|
|
148
|
-
throw new
|
|
164
|
+
throw new AvCliCommandError(`${canonical} was given twice; ${HELP}`);
|
|
149
165
|
}
|
|
150
166
|
seen.add(canonical);
|
|
151
167
|
if (question === undefined) {
|
|
152
168
|
if (inline !== undefined) {
|
|
153
|
-
throw new
|
|
169
|
+
throw new AvCliCommandError(`${canonical} takes no value; ${HELP}`);
|
|
154
170
|
}
|
|
155
171
|
options[option.key] = true;
|
|
156
172
|
continue;
|
|
@@ -158,7 +174,7 @@ export function parseCliCommand(argv) {
|
|
|
158
174
|
const value = inline ?? rest[index + 1];
|
|
159
175
|
if (value === undefined ||
|
|
160
176
|
(inline === undefined && value.startsWith("-"))) {
|
|
161
|
-
throw new
|
|
177
|
+
throw new AvCliCommandError(`${canonical} takes a value; ${HELP}`);
|
|
162
178
|
}
|
|
163
179
|
if (inline === undefined) {
|
|
164
180
|
index += 1;
|
|
@@ -174,3 +190,21 @@ export function parseCliCommand(argv) {
|
|
|
174
190
|
...(options.verbose === true ? { verbose: true } : {}),
|
|
175
191
|
};
|
|
176
192
|
}
|
|
193
|
+
function parseSkillCommand(rest) {
|
|
194
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
195
|
+
return { command: "help", topic: "skill" };
|
|
196
|
+
}
|
|
197
|
+
let force = false;
|
|
198
|
+
for (const argument of rest) {
|
|
199
|
+
if (argument !== "--force") {
|
|
200
|
+
throw new AvCliCommandError(argument.startsWith("-")
|
|
201
|
+
? `unknown flag ${argument.split("=")[0]}; ${HELP}`
|
|
202
|
+
: `unexpected argument ${JSON.stringify(argument)}: aventara skill takes no positional argument; ${HELP}`);
|
|
203
|
+
}
|
|
204
|
+
if (force) {
|
|
205
|
+
throw new AvCliCommandError(`--force was given twice; ${HELP}`);
|
|
206
|
+
}
|
|
207
|
+
force = true;
|
|
208
|
+
}
|
|
209
|
+
return { command: "skill", force };
|
|
210
|
+
}
|