@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 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 — is `@aventara/client`'s (`npx @aventara/client@rc init`).
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` — 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
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 **NestJS 12** project (refused: no `package.json`, another Nest major, TypeScript 7, a yarn-only
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
- - **No ORM yet:** the ORM and the database are asked (or taken from the flags), and everything below is written.
31
- - **Prisma already there:** the installed Prisma must be one an Aventara adapter supports — today Prisma 7, `^7.10.0`,
32
- SQLite or PostgreSQL, a driver adapter, the `prisma-client` generator — or the run is refused in one sentence, with
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.
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. | — (required) |
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>` | — | the lockfile's, else the one that launched the CLI, else npm |
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 **and** replace existing content that differs. | |
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 — `[3/4] Installing dependencies (npm install)` — with a spinner and the
57
- seconds so far on a terminal, or as one plain line per step (`… done (12.4s)`) in a pipe, in CI (`CI`) or with
58
- `NO_COLOR`; the output of `nest new` and the install is shown only if it fails, or with `--verbose`.
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` — `prisma-client` generating into `src/generated/prisma`, a `User` model | you |
68
- | `prisma.config.ts` — loads `.env` itself (Prisma does not) | you |
69
- | `src/prisma.service.ts` — `PrismaService` extending the client with the driver adapter, refusing a missing `DATABASE_URL` before the server listens; `PrismaModule` | you |
70
- | `src/aventara.config.ts` — `aventaraConfig(prisma)`: the entrypoint (`/api`), and the restrictions and pipelines you add | you |
71
- | `src/app.module.ts` — one edit: the imports, `PrismaModule` and `AventaraModule.forRootAsync({ … })`; printed instead when the file is not the shape expected | you |
72
- | `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 |
73
- | `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 |
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` — `@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 |
76
- | `pnpm-workspace.yaml` (pnpm only) — `allowBuilds` for Prisma's and SQLite's install scripts | you |
77
- | `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 |
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
- - **Resource keys are the model names, as written.** `model User` is `User` everywhere: on the wire
115
- (`/_resources/User/find/many`) and in the generated client (`avClient.User.find.many({})`). Nothing is renamed,
116
- lower-cased or pluralized.
117
- - **Results are read-only.** A list result is a `readonly` array: type it `readonly User[]`, not `User[]` —
118
- `const users: readonly User[] = await avClient.User.find.many({});`.
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; Node `^22.18.0 || >=24.2.0` (on any other Node the bin refuses in one sentence naming that range;
123
- on Node 22 use npm ≥ 11 — Node 22's bundled npm 10 cannot install Nest 12's own scaffold; a CommonJS project's jest
124
- e2e needs Node ≥ 24.9, Nest's and Jest's limit); npm and pnpm (yarn is not supported); ESM and
125
- CommonJS projects; Prisma 7 (`^7.10.0`) on SQLite and PostgreSQL. The ORMs, majors, databases and drivers on offer are
126
- **generated** from the Aventara adapter packages that exist (`src/catalog/adapter.catalog.generated.ts`), never listed
127
- by hand: a future
128
- `@aventara/prisma8-adapter` appears by existing.
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 — free to use, including in commercial applications; you may not
133
- use it to build a product that competes with Aventara itself. See `LICENSE` and `LICENSE-ADDITIONAL-PERMISSION.md`.
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 ConflictsNotConfirmedError extends Error {
2
- name = "ConflictsNotConfirmedError";
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 ConflictsNotConfirmedError(`not initializing, and nothing was touched: ${conflicts.join(", ")} already ${conflicts.length === 1 ? "has" : "have"} other content and nobody can be asked (stdin is not a terminal); run \`aventara init --yes\` to replace ${them}`);
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 ConflictsNotConfirmedError(`cancelled, and nothing was touched; run again and answer yes to replace ${them}`);
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 { ConflictsNotConfirmedError } from "./apply/conflict.confirmer.js";
2
+ import { AvConflictsNotConfirmedError } from "./apply/conflict.confirmer.js";
3
3
  import { ADAPTER_CATALOG } from "./catalog/adapter.catalog.generated.js";
4
- import { CliCommandError, commandUsage, parseCliCommand, usage, } from "./command/command.parser.js";
5
- import { ProjectRefusedError } from "./project/project.inspector.js";
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 { InstallFailedError, runInit } from "./run/init.orchestrator.js";
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 { UnansweredQuestionsError, } from "./wizard/answer.resolver.js";
11
- import { createReadlinePrompter, WizardCancelledError, } from "./wizard/readline.prompter.js";
12
- import { WizardAnswerError } from "./wizard/wizard.questions.js";
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
- CliCommandError,
15
- WizardAnswerError,
16
- UnansweredQuestionsError,
17
- WizardCancelledError,
18
- ProjectRefusedError,
19
- ConflictsNotConfirmedError,
20
- InstallFailedError,
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 CliCommandError extends Error {
3
- name = "CliCommandError";
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 CliCommandError(`a command is required: new or init; ${HELP}`);
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 CliCommandError(`unknown command ${JSON.stringify(command)}: the commands are new and init; ${HELP}`);
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 CliCommandError(`unexpected argument ${JSON.stringify(argument)}: aventara ${command} takes ${positional === undefined ? "no" : "one"} positional argument; ${HELP}`);
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 CliCommandError(elsewhere.length === 0
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 CliCommandError(`${option.flag} is a flag of aventara ${option.commands.join(" and ")}, not aventara ${command}; ${HELP}`);
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 CliCommandError(`${canonical} was given twice; ${HELP}`);
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 CliCommandError(`${canonical} takes no value; ${HELP}`);
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 CliCommandError(`${canonical} takes a value; ${HELP}`);
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
+ }