@aventara/cli 1.0.0-rc.0 → 1.0.0-rc.10

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 CHANGED
@@ -1,15 +1,17 @@
1
1
  # `@aventara/cli`
2
2
 
3
- The `aventara` command: it scaffolds an Aventara server on NestJS 12.
3
+ The `aventara` command: it scaffolds an Aventara server on NestJS 12 or Next.js 16.
4
4
 
5
5
  ```bash
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
- aventara init # Aventara added to the NestJS 12 project in this directory
8
+ aventara new my-app --host next # a new Next.js app with Aventara (create-next-app, then init)
9
+ aventara init # Aventara added to the NestJS 12 or Next.js 16 project in this directory
10
+ aventara skill # refresh the agent skill for the installed @aventara/core
9
11
  ```
10
12
 
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`).
13
+ It is run once, globally or through `npx`; it is not a dependency of the projects it writes. The frontend's side
14
+ (`framework.client.ts` and the generated client) belongs to `@aventara/client` (`npx @aventara/client@rc init`).
13
15
 
14
16
  ## `aventara new <name>`
15
17
 
@@ -17,22 +19,37 @@ Runs the pinned `nest new` (`npx -y @nestjs/cli@12.0.8 new …`, or `pnpm dlx
17
19
  `src/app.module.ts` is what that version writes, then runs exactly `aventara init`'s pipeline over the new project and
18
20
  installs it. A directory that exists and is not empty is refused before anything runs.
19
21
 
20
- Once installed, the project gets its first git commit, `Initial Aventara Scaffold` — the lockfile in it, `.env` not
21
- (it is in `.gitignore`). No commit is made, and one line says why, when git is not installed, when git has no
22
+ Once installed, the project gets its first git commit, `Initial Aventara Scaffold`, with the lockfile in it and
23
+ 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
24
  `user.name`/`user.email`, or when the directory is already inside a git repository (a monorepo: commit it there).
23
25
  `--skip-git` makes no repository and no commit.
24
26
 
27
+ ### `aventara new <name> --host next`
28
+
29
+ Runs `create-next-app@latest <name> --ts --app --src-dir --import-alias "@/*" --use-<npm|pnpm> --skip-install`, then
30
+ `aventara init`'s pipeline over the new app, then the install. create-next-app asks its own questions on your terminal;
31
+ with `--yes` it takes its defaults instead (and is given `--yes` too), which is also what a run with no terminal needs.
32
+ Its own flags go after `--`, as they are: `aventara new my-app --host next --yes -- --tailwind --eslint`. `--skip-git`
33
+ becomes its `--disable-git`. `--host` defaults to `nest`.
34
+
25
35
  ## `aventara init`
26
36
 
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).
37
+ Runs inside an existing NestJS 12 or Next.js 16 project, telling them apart by `package.json`: `@nestjs/core` is
38
+ NestJS, `next` is Next.js, and a project depending on both or neither is refused in one sentence. A Next.js app must use
39
+ the App Router under `src/app/` and map `@/*` to `./src/*` in `tsconfig.json` (as create-next-app writes it), and have
40
+ no ORM yet. The rest of this section is NestJS's; a Next.js app gets [its own files](#what-it-writes-in-a-nextjs-app).
41
+
42
+ In a NestJS project, it refuses another Nest major,
43
+ TypeScript 7, a yarn-only lockfile, or a project already initialized.
44
+
45
+ In a project with no ORM yet, the ORM and the database are asked (or taken from the flags), and everything below is
46
+ written.
29
47
 
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.
48
+ In a project that already uses Prisma, the installed Prisma must be one an Aventara adapter supports, or the run is
49
+ refused in one sentence with nothing written. Today that is Prisma 7 (`^7.10.1`) on SQLite or PostgreSQL, with a driver
50
+ adapter and the `prisma-client` generator. Your schema, Prisma config and service are reused and never written, and
51
+ your `.env` only gains a `CORS_ORIGINS` line at its end when it has none. Init finds the class extending `PrismaClient`
52
+ and the module exporting it, or asks.
36
53
 
37
54
  ### Questions and flags
38
55
 
@@ -42,20 +59,22 @@ possible answer is shown, not asked.
42
59
 
43
60
  | Flag | Question | Default |
44
61
  |---|---|---|
45
- | `<name>` (`new` only) | The project directory. | — (required) |
62
+ | `<name>` (`new` only) | The project directory. | required |
63
+ | `--host <nest\|next>` (`new` only) | The framework to build on; `init` reads it from `package.json`. Not asked. | `nest` |
46
64
  | `--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
65
  | `--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 |
66
+ | `--package-manager <npm\|pnpm>` | The package manager. | the lockfile's, else the one that launched the CLI, else npm |
49
67
  | `--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
68
  | `--prisma-module <path#Export>` (`init`) | The Nest module that provides and exports it. | the one found |
51
69
  | `--skip-install` | Write the files; print the install instead of running it. | |
52
70
  | `--skip-git` (`new`) | Passed to `nest new`. | |
53
- | `-y`, `--yes` | Accept every unanswered default **and** replace existing content that differs. | |
71
+ | `-y`, `--yes` | Accept every unanswered default, and replace existing content that differs. | |
54
72
  | `--verbose` | Show the output of `nest new` and the install as they run. | |
73
+ | `-- <args>` (`new --host next`) | Passed to create-next-app as they are, after its own. | |
55
74
 
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`.
75
+ While it works, each step is shown on stderr, such as `[3/4] Installing dependencies (npm install)`. On a terminal it
76
+ 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
77
+ (`… done (12.4s)`). The output of `nest new` and the install is shown only if it fails, or with `--verbose`.
59
78
 
60
79
  Existing content that differs from what would be written (a `.env` key, a script, a file) is listed, and replaced only
61
80
  when you confirm, or with `--yes`; where nobody can be asked, the run stops and touches nothing.
@@ -64,17 +83,41 @@ when you confirm, or with `--yes`; where nobody can be asked, the run stops and
64
83
 
65
84
  | Path | Owner afterwards |
66
85
  |---|---|
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 |
86
+ | `prisma/schema.prisma`: `prisma-client` generating into `src/generated/prisma`, a `User` model | you |
87
+ | `prisma.config.ts`: loads `.env` itself (Prisma does not) | you |
88
+ | `src/prisma.service.ts`: `PrismaService` extending the client with the driver adapter, refusing a missing `DATABASE_URL` before the server listens; `PrismaModule` | you |
89
+ | `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 |
90
+ | `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 |
91
+ | `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 |
92
+ | `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
93
  | `.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 |
94
+ | `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 |
95
+ | `pnpm-workspace.yaml` (pnpm only): `allowBuilds` for Prisma's and SQLite's install scripts | you |
96
+ | `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 |
97
+ | `.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` |
98
+ | `AGENTS.md`: an Aventara section between `<!-- aventara:start -->` and `<!-- aventara:end -->`, appended to an existing file | you, except the marked section |
99
+
100
+ ### What it writes in a Next.js app
101
+
102
+ Everything Aventara is in one folder, `src/aventara/`, at the paths `@aventara/next` declares and `aventara-build`
103
+ reads.
104
+
105
+ | Path | Owner afterwards |
106
+ |---|---|
107
+ | `prisma/schema.prisma`, `prisma.config.ts` | you (as in a NestJS project) |
108
+ | `src/prisma.ts`: `prisma()`, the app's one Prisma client with the driver adapter, made on first use and kept across `next dev` reloads | you |
109
+ | `src/aventara/config.ts`: `aventaraConfig`, as in a NestJS project, logging through `console.error` | you |
110
+ | `src/aventara/adapter.ts`: the adapter's description | you |
111
+ | `src/aventara/server.ts`: `Aventara({ ...aventaraConfig, adapter, prebuilt, state })` exporting `handlers`, `avClient`, `avApp` and `ready`; `@/aventara/server` | you |
112
+ | `src/app/api/[...aventara]/route.ts`: every method of `handlers` | you |
113
+ | `src/instrumentation.ts`: awaits `ready()` on the Node.js runtime, so a bad configuration stops `next start`. An instrumentation file of yours is never replaced, even with `--yes`: the lines to add are printed | you |
114
+ | `next.config.ts`: `serverExternalPackages: ["@aventara/core", "@aventara/prisma7-adapter"]` added to its configuration object, so Node.js loads them once per server process (one core and one Prisma client for every route and page). Added to create-next-app's config, or to any whose `const nextConfig = {` object has no `serverExternalPackages`; written as a new `next.config.ts` when the app has none. A config that already names `serverExternalPackages`, or is written another way, is never changed, even with `--yes`: the line to add is printed | you |
115
+ | `package.json`: `@aventara/core`, `@aventara/next`, the adapter and `@aventara/build` at this CLI's version, `server-only`, Prisma at `7.10.0`; `aventara:prepare` (ending in `aventara-build --client`), run by `postinstall` and `prebuild` | you |
116
+ | `.env` (`DATABASE_URL`), `.gitignore` (`/src/generated/`, `/dev.db*`, `/src/aventara/generated/`, `/src/aventara/client.ts`, `/src/aventara/universal.ts`, `.env`) | you |
117
+ | `src/aventara/client.ts`, `src/aventara/universal.ts`, `src/aventara/generated/` | generated by `aventara:prepare`; never edit |
118
+
119
+ No CORS (the browser calls the same origin), no Pages Router, no proxy, and no MCP route: an `mcp` endpoint you add on
120
+ Next.js needs `authorization`. Next steps: `npx prisma db push`, then `npm run dev`.
78
121
 
79
122
  ### Next steps it prints
80
123
 
@@ -111,23 +154,37 @@ curl -s -X POST http://localhost:3000/api/_resources/User/find/many \
111
154
  Leave out an identity header and the answer is `400 A2000`, naming the header that is missing. The hash changes
112
155
  whenever the schema or the configuration does: fetch it again, and regenerate the client.
113
156
 
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({});`.
157
+ Resource keys are the model names, as written. `model User` is `User` everywhere: on the wire
158
+ (`/_resources/User/find/many`) and in the generated client (`avClient.User.find.many({})`). Nothing is renamed,
159
+ lower-cased or pluralized.
160
+
161
+ Results are read-only. A list result is a `readonly` array, so type it `readonly User[]`:
162
+ `const users: readonly User[] = await avClient.User.find.many({});`.
163
+
164
+ ## `aventara skill`
165
+
166
+ Writes the agent skill for the `@aventara/core` installed in the project in this directory: its version, and its
167
+ `ERRORS.md` as `reference/errors.md`. It reads those files and runs none of the project's packages. A skill file it
168
+ wrote before is replaced; one you changed is kept, with a warning, unless you pass `--force`. In `AGENTS.md` only the
169
+ text between the Aventara markers is replaced (or the section appended), and `CLAUDE.md` is never written. Run it with
170
+ the CLI at your `@aventara/core` version: `npx @aventara/cli@<version> skill`.
119
171
 
120
172
  ## Supported
121
173
 
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.
174
+ - NestJS 12.
175
+ - Node `^22.23.4 || >=24.22.1`. On any other Node the bin refuses in one sentence naming that range. On Node 22 use
176
+ npm ≥ 11, because Node 22's bundled npm 10 cannot install Nest 12's own scaffold. A CommonJS project's jest e2e needs
177
+ Node ≥ 24.9, which is Nest's and Jest's limit.
178
+ - npm and pnpm. Yarn is not supported.
179
+ - ESM and CommonJS projects.
180
+ - Prisma 7 (`^7.10.1`) on SQLite and PostgreSQL.
181
+
182
+ The ORMs, majors, databases and drivers on offer are generated from the Aventara adapter packages that exist
183
+ (`src/catalog/adapter.catalog.generated.ts`), never listed by hand, so a future `@aventara/prisma8-adapter` appears
184
+ once it exists.
129
185
 
130
186
  ## License
131
187
 
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`.
188
+ PolyForm Shield 1.0.0 with an additional permission. It is free to use, including in commercial applications, but you
189
+ may not use it to build a product that competes with Aventara itself. See `LICENSE` and
190
+ `LICENSE-ADDITIONAL-PERMISSION.md`.
@@ -1,7 +1,7 @@
1
- export class ConflictsNotConfirmedError extends Error {
2
- name = "ConflictsNotConfirmedError";
1
+ export class AvConflictsNotConfirmedError extends Error {
2
+ name = "AvConflictsNotConfirmedError";
3
3
  }
4
- export function conflictWarning(conflicts) {
4
+ 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"}`;
6
6
  }
7
7
  export async function confirmConflicts(conflicts, consent) {
@@ -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
  }
@@ -9,7 +9,7 @@ export const ADAPTER_CATALOG = [
9
9
  "package": "@prisma/client",
10
10
  "major": 7
11
11
  },
12
- "range": "^7.10.0",
12
+ "range": "^7.10.1",
13
13
  "familyPackages": [
14
14
  "prisma",
15
15
  "@prisma/client",
@@ -0,0 +1,10 @@
1
+ import { HOST_CATALOG } from "./host.catalog.generated.js";
2
+ import { lowestMajorOf } from "./range.reader.js";
3
+ export const HOSTS = HOST_CATALOG;
4
+ export const DEFAULT_HOST = "nest";
5
+ export function hostOf(alias) {
6
+ return HOST_CATALOG.find((entry) => entry.alias === alias);
7
+ }
8
+ export function frameworkMajorOf(host) {
9
+ return lowestMajorOf(host.framework.range);
10
+ }
@@ -0,0 +1,32 @@
1
+ export const HOST_CATALOG = [
2
+ {
3
+ "alias": "nest",
4
+ "label": "NestJS",
5
+ "package": "@aventara/nest",
6
+ "framework": {
7
+ "package": "@nestjs/core",
8
+ "range": "^12.0.0"
9
+ },
10
+ "layout": {
11
+ "config": "src/aventara.config",
12
+ "adapter": "src/aventara.adapter",
13
+ "generated": "src/generated/aventara"
14
+ }
15
+ },
16
+ {
17
+ "alias": "next",
18
+ "label": "Next.js",
19
+ "package": "@aventara/next",
20
+ "framework": {
21
+ "package": "next",
22
+ "range": ">=16.4 <17"
23
+ },
24
+ "layout": {
25
+ "config": "src/aventara/config",
26
+ "adapter": "src/aventara/adapter",
27
+ "generated": "src/aventara/generated",
28
+ "client": "src/aventara/client.ts",
29
+ "universal": "src/aventara/universal.ts"
30
+ }
31
+ }
32
+ ];
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,
@@ -65,10 +75,13 @@ export async function runCli(argv, io) {
65
75
  }
66
76
  catch (error) {
67
77
  if (REFUSALS.some((refusal) => error instanceof refusal)) {
68
- io.stderr(`aventara: ${error.message}.\n`);
78
+ const { message } = error;
79
+ io.stderr(message.includes("\n")
80
+ ? `aventara: ${message.trimEnd()}\n`
81
+ : `aventara: ${message}.\n`);
69
82
  return 1;
70
83
  }
71
- io.stderr(`${String(error instanceof Error ? error.stack : error)}\n`);
84
+ io.stderr(`${(error instanceof Error && error.stack) || String(error)}\n`);
72
85
  return 1;
73
86
  }
74
87
  }
@@ -1,6 +1,7 @@
1
+ import { DEFAULT_HOST, HOSTS } from "../catalog/host-entry.interface.js";
1
2
  import { INIT_FLAG_QUESTIONS, NEW_QUESTIONS, ormAliasesSentence, } from "../wizard/wizard.questions.js";
2
- export class CliCommandError extends Error {
3
- name = "CliCommandError";
3
+ export class AvCliCommandError extends Error {
4
+ name = "AvCliCommandError";
4
5
  }
5
6
  const OPTIONS = [
6
7
  {
@@ -34,6 +35,23 @@ const QUESTIONS = {
34
35
  init: INIT_FLAG_QUESTIONS,
35
36
  };
36
37
  const HELP = "run `aventara --help` for usage";
38
+ const HOST_FLAG = "--host";
39
+ function hostAliasesSentence() {
40
+ const hosts = HOSTS.map((host) => `${host.alias} (${host.label}, ${host.package})`);
41
+ return `--host takes ${hosts.length <= 1
42
+ ? hosts.join("")
43
+ : `${hosts.slice(0, -1).join(", ")} or ${hosts[hosts.length - 1]}`}`;
44
+ }
45
+ const NEW_ONLY_ROWS = [
46
+ [
47
+ `${HOST_FLAG} <${HOSTS.map((host) => host.alias).join("|")}>`,
48
+ `the framework to build on; ${DEFAULT_HOST} when omitted`,
49
+ ],
50
+ [
51
+ "-- <args>",
52
+ "passed as they are to the project generator (create-next-app, with --host next)",
53
+ ],
54
+ ];
37
55
  function flagUsage(question, catalog) {
38
56
  const accepted = question.accepts?.(catalog);
39
57
  return `${question.flag} ${accepted === undefined
@@ -52,6 +70,7 @@ function usageLines(command, catalog) {
52
70
  `${option.alias === undefined ? "" : `${option.alias}, `}${option.flag}`,
53
71
  option.summary,
54
72
  ]),
73
+ ...(command === "new" ? NEW_ONLY_ROWS : []),
55
74
  ];
56
75
  const width = Math.max(...rows.map(([left]) => left.length));
57
76
  return rows
@@ -59,8 +78,8 @@ function usageLines(command, catalog) {
59
78
  .join("\n");
60
79
  }
61
80
  const SUMMARY = {
62
- new: "Create a NestJS project with Aventara (nest new, then init).",
63
- init: "Add Aventara to the NestJS 12 project in this directory.",
81
+ new: "Create a project with Aventara (nest new or create-next-app, then init).",
82
+ init: `Add Aventara to the ${HOSTS.map((host) => host.label).join(" or ")} project in this directory.`,
64
83
  };
65
84
  const SYNOPSIS = {
66
85
  new: "aventara new <name> [options]",
@@ -72,7 +91,18 @@ On a terminal every unanswered question is asked; anywhere else, pass its flag
72
91
  or --yes, or the run stops before writing anything.
73
92
  `;
74
93
  }
94
+ const SKILL_SUMMARY = "Write or refresh the agent skill (.claude/skills/aventara/) and the Aventara section of AGENTS.md, for the installed @aventara/core.";
95
+ const SKILL_USAGE = `Usage: aventara skill [options]
96
+
97
+ ${SKILL_SUMMARY}
98
+
99
+ Options:
100
+ --force also replace skill files you changed (AGENTS.md outside the Aventara section is never touched)
101
+ `;
75
102
  export function commandUsage(command, catalog) {
103
+ if (command === "skill") {
104
+ return SKILL_USAGE;
105
+ }
76
106
  return `Usage: ${SYNOPSIS[command]}
77
107
 
78
108
  ${SUMMARY[command]}
@@ -83,7 +113,7 @@ ${usageLines(command, catalog)}
83
113
  ${asked(catalog)}`;
84
114
  }
85
115
  export function usage(catalog) {
86
- return `aventara — scaffold an Aventara server on NestJS.
116
+ return `aventara — scaffold an Aventara server on ${HOSTS.map((host) => host.label).join(" or ")}.
87
117
 
88
118
  Usage:
89
119
  ${SYNOPSIS.new} ${SUMMARY.new}
@@ -92,6 +122,8 @@ ${usageLines("new", catalog)}
92
122
  ${SYNOPSIS.init} ${SUMMARY.init}
93
123
  ${usageLines("init", catalog)}
94
124
 
125
+ aventara skill [--force] ${SKILL_SUMMARY}
126
+
95
127
  aventara --help Print this and exit 0.
96
128
  aventara <command> --help Print that command's usage and exit 0.
97
129
  aventara --version, -v Print this CLI's version and exit 0.
@@ -101,7 +133,7 @@ ${asked(catalog)}`;
101
133
  export function parseCliCommand(argv) {
102
134
  const [command, ...rest] = argv;
103
135
  if (command === undefined) {
104
- throw new CliCommandError(`a command is required: new or init; ${HELP}`);
136
+ throw new AvCliCommandError(`a command is required: new, init or skill; ${HELP}`);
105
137
  }
106
138
  if (command === "--help" || command === "-h") {
107
139
  return { command: "help" };
@@ -109,8 +141,11 @@ export function parseCliCommand(argv) {
109
141
  if (command === "--version" || command === "-v") {
110
142
  return { command: "version" };
111
143
  }
144
+ if (command === "skill") {
145
+ return parseSkillCommand(rest);
146
+ }
112
147
  if (command !== "new" && command !== "init") {
113
- throw new CliCommandError(`unknown command ${JSON.stringify(command)}: the commands are new and init; ${HELP}`);
148
+ throw new AvCliCommandError(`unknown command ${JSON.stringify(command)}: the commands are new, init and skill; ${HELP}`);
114
149
  }
115
150
  if (rest.includes("--help") || rest.includes("-h")) {
116
151
  return { command: "help", topic: command };
@@ -118,13 +153,22 @@ export function parseCliCommand(argv) {
118
153
  const questions = QUESTIONS[command];
119
154
  const given = {};
120
155
  const options = {};
156
+ let host;
157
+ let passthrough;
121
158
  const seen = new Set();
122
159
  const positional = questions.find((question) => !question.flag.startsWith("--"));
123
160
  for (let index = 0; index < rest.length; index += 1) {
124
161
  const argument = rest[index];
162
+ if (argument === "--") {
163
+ if (command !== "new") {
164
+ throw new AvCliCommandError(`aventara ${command} passes nothing on: arguments after -- go to aventara new's project generator; ${HELP}`);
165
+ }
166
+ passthrough = rest.slice(index + 1);
167
+ break;
168
+ }
125
169
  if (!argument.startsWith("-")) {
126
170
  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}`);
171
+ throw new AvCliCommandError(`unexpected argument ${JSON.stringify(argument)}: aventara ${command} takes ${positional === undefined ? "no" : "one"} positional argument; ${HELP}`);
128
172
  }
129
173
  given[positional.id] = argument;
130
174
  continue;
@@ -132,25 +176,47 @@ export function parseCliCommand(argv) {
132
176
  const equals = argument.indexOf("=");
133
177
  const flag = equals === -1 ? argument : argument.slice(0, equals);
134
178
  const inline = equals === -1 ? undefined : argument.slice(equals + 1);
179
+ if (flag === HOST_FLAG) {
180
+ if (command !== "new") {
181
+ throw new AvCliCommandError(`${HOST_FLAG} is a flag of aventara new, not aventara ${command}: init reads the host from package.json; ${HELP}`);
182
+ }
183
+ if (host !== undefined) {
184
+ throw new AvCliCommandError(`${HOST_FLAG} was given twice; ${HELP}`);
185
+ }
186
+ const value = inline ?? rest[index + 1];
187
+ if (value === undefined ||
188
+ (inline === undefined && value.startsWith("-"))) {
189
+ throw new AvCliCommandError(`${HOST_FLAG} takes a value; ${HELP}`);
190
+ }
191
+ if (inline === undefined) {
192
+ index += 1;
193
+ }
194
+ const alias = value.trim().toLowerCase();
195
+ if (!HOSTS.some((candidate) => candidate.alias === alias)) {
196
+ throw new AvCliCommandError(`there is no Aventara host for ${JSON.stringify(value)}; ${hostAliasesSentence()}; ${HELP}`);
197
+ }
198
+ host = alias;
199
+ continue;
200
+ }
135
201
  const question = questions.find((candidate) => candidate.flag === flag);
136
202
  const option = OPTIONS.find((candidate) => candidate.flag === flag || candidate.alias === flag);
137
203
  const canonical = question?.flag ?? option?.flag ?? flag;
138
204
  if (question === undefined && option === undefined) {
139
205
  const elsewhere = ["new", "init"].filter((other) => QUESTIONS[other].some((candidate) => candidate.flag === flag));
140
- throw new CliCommandError(elsewhere.length === 0
206
+ throw new AvCliCommandError(elsewhere.length === 0
141
207
  ? `unknown flag ${flag}; ${HELP}`
142
208
  : `${flag} is a flag of aventara ${elsewhere.join(" and ")}, not aventara ${command}; ${HELP}`);
143
209
  }
144
210
  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}`);
211
+ throw new AvCliCommandError(`${option.flag} is a flag of aventara ${option.commands.join(" and ")}, not aventara ${command}; ${HELP}`);
146
212
  }
147
213
  if (seen.has(canonical)) {
148
- throw new CliCommandError(`${canonical} was given twice; ${HELP}`);
214
+ throw new AvCliCommandError(`${canonical} was given twice; ${HELP}`);
149
215
  }
150
216
  seen.add(canonical);
151
217
  if (question === undefined) {
152
218
  if (inline !== undefined) {
153
- throw new CliCommandError(`${canonical} takes no value; ${HELP}`);
219
+ throw new AvCliCommandError(`${canonical} takes no value; ${HELP}`);
154
220
  }
155
221
  options[option.key] = true;
156
222
  continue;
@@ -158,7 +224,7 @@ export function parseCliCommand(argv) {
158
224
  const value = inline ?? rest[index + 1];
159
225
  if (value === undefined ||
160
226
  (inline === undefined && value.startsWith("-"))) {
161
- throw new CliCommandError(`${canonical} takes a value; ${HELP}`);
227
+ throw new AvCliCommandError(`${canonical} takes a value; ${HELP}`);
162
228
  }
163
229
  if (inline === undefined) {
164
230
  index += 1;
@@ -172,5 +238,25 @@ export function parseCliCommand(argv) {
172
238
  skipGit: options.skipGit === true,
173
239
  yes: options.yes === true,
174
240
  ...(options.verbose === true ? { verbose: true } : {}),
241
+ ...(host === undefined ? {} : { host }),
242
+ ...(passthrough === undefined ? {} : { passthrough }),
175
243
  };
176
244
  }
245
+ function parseSkillCommand(rest) {
246
+ if (rest.includes("--help") || rest.includes("-h")) {
247
+ return { command: "help", topic: "skill" };
248
+ }
249
+ let force = false;
250
+ for (const argument of rest) {
251
+ if (argument !== "--force") {
252
+ throw new AvCliCommandError(argument.startsWith("-")
253
+ ? `unknown flag ${argument.split("=")[0]}; ${HELP}`
254
+ : `unexpected argument ${JSON.stringify(argument)}: aventara skill takes no positional argument; ${HELP}`);
255
+ }
256
+ if (force) {
257
+ throw new AvCliCommandError(`--force was given twice; ${HELP}`);
258
+ }
259
+ force = true;
260
+ }
261
+ return { command: "skill", force };
262
+ }
@@ -0,0 +1 @@
1
+ export const PREPARE_SCRIPT = "aventara:prepare";