@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/CHANGELOG.md +461 -0
- package/README.md +101 -44
- package/dist/apply/conflict.confirmer.js +5 -5
- package/dist/catalog/adapter.catalog.generated.js +1 -1
- package/dist/catalog/host-entry.interface.js +10 -0
- package/dist/catalog/host.catalog.generated.js +32 -0
- package/dist/cli.js +29 -16
- package/dist/command/command.parser.js +99 -13
- package/dist/plan/host-plan.interface.js +1 -0
- package/dist/plan/nest-host.planner.js +131 -0
- package/dist/plan/next-host.planner.js +133 -0
- package/dist/plan/project.planner.js +94 -167
- package/dist/project/project.inspector.js +56 -17
- package/dist/run/command.runner.js +37 -19
- package/dist/run/init.orchestrator.js +55 -38
- package/dist/run/new.orchestrator.js +105 -30
- package/dist/run/scaffold.committer.js +1 -1
- 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/next-server.entry.js +5 -0
- package/dist/templates/prisma7/prisma7-nest.templates.js +242 -0
- package/dist/templates/prisma7/prisma7-next.templates.js +189 -0
- package/dist/templates/prisma7/prisma7-orm.templates.js +180 -0
- package/dist/templates/prisma7/prisma7.templates.js +7 -365
- package/dist/templates/template.registry.js +2 -2
- package/dist/wizard/answer.resolver.js +5 -5
- package/dist/wizard/readline.prompter.js +3 -3
- package/dist/wizard/wizard.questions.js +11 -11
- package/package.json +3 -3
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
|
|
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
|
|
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
|
|
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
|
|
28
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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. |
|
|
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>` |
|
|
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
|
|
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
|
|
57
|
-
|
|
58
|
-
`
|
|
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
|
|
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
|
|
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
|
|
76
|
-
| `pnpm-workspace.yaml` (pnpm only)
|
|
77
|
-
| `src/generated/prisma/**`, `src/generated/aventara/discovery.artifact.ts` |
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
133
|
-
use it to build a product that competes with Aventara itself. See `LICENSE` and
|
|
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
|
|
2
|
-
name = "
|
|
1
|
+
export class AvConflictsNotConfirmedError extends Error {
|
|
2
|
+
name = "AvConflictsNotConfirmedError";
|
|
3
3
|
}
|
|
4
|
-
|
|
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
|
|
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
|
}
|
|
@@ -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 {
|
|
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,
|
|
@@ -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
|
-
|
|
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(`${
|
|
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
|
|
3
|
-
name = "
|
|
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
|
|
63
|
-
init:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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";
|