@aventara/cli 0.1.0-pilot.0 → 0.1.0-pilot.1

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
@@ -3,13 +3,13 @@
3
3
  The `aventara` command: it scaffolds an Aventara server on NestJS 12.
4
4
 
5
5
  ```bash
6
- npm i -g @aventara/cli # or run it once: npx @aventara/cli <command>
6
+ npm i -g @aventara/cli@pilot # or run it once: npx @aventara/cli@pilot <command>
7
7
  aventara new my-api # a new NestJS project with Aventara
8
8
  aventara init # Aventara added to the NestJS 12 project in this directory
9
9
  ```
10
10
 
11
11
  It is run once, globally or through `npx`; it is not a dependency of the projects it writes. The frontend's side —
12
- `framework.client.ts` and the generated client — is `@aventara/client`'s (`npx @aventara/client init`).
12
+ `framework.client.mts` and the generated client — is `@aventara/client`'s (`npx @aventara/client@pilot init`).
13
13
 
14
14
  ## `aventara new <name>`
15
15
 
@@ -58,6 +58,7 @@ when you confirm, or with `--yes`; where nobody can be asked, the run stops and
58
58
  | `src/prisma.service.ts` — `PrismaService` extending the client with the driver adapter, refusing a missing `DATABASE_URL` before the server listens; `PrismaModule` | you |
59
59
  | `src/aventara.config.ts` — `aventaraConfig(prisma)`: the entrypoint (`/api`), and the restrictions and pipelines you add | you |
60
60
  | `src/app.module.ts` — one edit: the imports, `PrismaModule` and `AventaraModule.forRootAsync({ … })`; printed instead when the file is not the shape expected | you |
61
+ | `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 |
61
62
  | `.env` (`DATABASE_URL`), `.gitignore` (`/src/generated/`, `/dev.db*`) | you |
62
63
  | `package.json` — `@aventara/*` at this CLI's version, `prisma`, `@prisma/client` and the driver at `7.10.0`; `aventara:prepare`, `postinstall`, and the `.env` on the start scripts and `test:e2e` (`--env-file`, `--env-file-if-exists`) | you |
63
64
  | `pnpm-workspace.yaml` (pnpm only) — `allowBuilds` for Prisma's and SQLite's install scripts | you |
@@ -68,16 +69,47 @@ when you confirm, or with `--yes`; where nobody can be asked, the run stops and
68
69
  ```bash
69
70
  npx prisma db push # create the tables (init never touches a database); pnpm: pnpm exec prisma db push
70
71
  npm run start:dev # GET http://localhost:3000/api/_contract
71
- npx @aventara/client init # in your frontend
72
+ npx @aventara/client@pilot init # in your frontend; pnpm: pnpm dlx @aventara/client@pilot init
72
73
  ```
73
74
 
75
+ The frontend command is the package manager's own runner (`npx`, or `pnpm dlx` under pnpm) and, while the CLI is a
76
+ prerelease, carries its dist-tag (`@pilot`), so the client comes from the same release.
77
+
74
78
  With PostgreSQL, set `DATABASE_URL` in `.env` first (init writes Prisma's placeholder, and never asks for a secret).
75
79
 
80
+ ### Calling it by hand
81
+
82
+ With the server running, the starter `User` model answers at `/api`:
83
+
84
+ <!-- pilot-gate:curl -->
85
+ ```bash
86
+ # 1. The contract. Its protocol.hash is what every resource request sends as Aventara-Contract-Hash.
87
+ HASH=$(curl -s http://localhost:3000/api/_contract | node -p 'JSON.parse(require("fs").readFileSync(0, "utf8")).protocol.hash')
88
+
89
+ # 2. find.many on User, with both identity headers.
90
+ curl -s -X POST http://localhost:3000/api/_resources/User/find/many \
91
+ -H 'Content-Type: application/json' \
92
+ -H 'Aventara-Protocol-Version: 1' \
93
+ -H "Aventara-Contract-Hash: $HASH" \
94
+ -d '{}'
95
+ # {"data":[],"code":"A1000","cause":null}
96
+ ```
97
+ <!-- /pilot-gate:curl -->
98
+
99
+ Leave out an identity header and the answer is `400 A2000`, naming the header that is missing. The hash changes
100
+ whenever the schema or the configuration does: fetch it again, and regenerate the client.
101
+
102
+ - **Resource keys are the model names, as written.** `model User` is `User` everywhere: on the wire
103
+ (`/_resources/User/find/many`) and in the generated client (`avClient.User.find.many({})`). Nothing is renamed,
104
+ lower-cased or pluralized.
105
+ - **Results are read-only.** A list result is a `readonly` array: type it `readonly User[]`, not `User[]` —
106
+ `const users: readonly User[] = await avClient.User.find.many({});`.
107
+
76
108
  ## Supported
77
109
 
78
- NestJS 12; Node `^22.18.0 || >=24.2.0` (measured; on any other Node the bin refuses in one sentence naming that range,
79
- F-855; on Node 22 use npm ≥ 11 — Node 22's bundled npm 10 cannot install Nest 12's own scaffold, F-854; a CommonJS
80
- project's jest e2e needs Node ≥ 24.9, Nest's and Jest's limit, F-856); npm and pnpm (yarn is not supported); ESM and
110
+ NestJS 12; Node `^22.18.0 || >=24.2.0` (measured; on any other Node the bin refuses in one sentence naming that range;
111
+ on Node 22 use npm ≥ 11 — Node 22's bundled npm 10 cannot install Nest 12's own scaffold; a CommonJS project's jest
112
+ e2e needs Node ≥ 24.9, Nest's and Jest's limit); npm and pnpm (yarn is not supported); ESM and
81
113
  CommonJS projects; Prisma 7 (`^7.10.0`) on SQLite and PostgreSQL. The ORMs, majors, databases and drivers on offer are
82
114
  **generated** from the Aventara adapter packages that exist (`src/catalog/adapter.catalog.generated.ts`), never listed
83
115
  by hand: a future
@@ -0,0 +1,18 @@
1
+ /**
2
+ * pilot.1 — the scaffold's own e2e spec also proves the protocol is mounted.
3
+ *
4
+ * `nest new`'s `test/app.e2e-spec.ts` (`@nestjs/schematics` 12's template, ESM
5
+ * and CommonJS alike) tests `GET /` and nothing else. Like the `app.module.ts`
6
+ * edit, this one is anchored on Nest's exact text: the `/ (GET)` test is found
7
+ * exactly once, and a `GET <entrypoint>/_contract` test is added right after
8
+ * it, through the spec's own `app` and `request`. A spec that is not Nest's —
9
+ * the developer's own — is left as it is, and nothing is said: the test is a
10
+ * convenience, and the developer's tests are theirs.
11
+ */
12
+ /** The e2e spec `nest new` writes. */
13
+ export declare const E2E_SPEC = "test/app.e2e-spec.ts";
14
+ /**
15
+ * `spec` with the contract test after Nest's `GET /` test, or `undefined` when
16
+ * the anchor is not there exactly once, or the contract test already is.
17
+ */
18
+ export declare function spliceContractTest(spec: string, entrypoint: string): string | undefined;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * pilot.1 — the scaffold's own e2e spec also proves the protocol is mounted.
3
+ *
4
+ * `nest new`'s `test/app.e2e-spec.ts` (`@nestjs/schematics` 12's template, ESM
5
+ * and CommonJS alike) tests `GET /` and nothing else. Like the `app.module.ts`
6
+ * edit, this one is anchored on Nest's exact text: the `/ (GET)` test is found
7
+ * exactly once, and a `GET <entrypoint>/_contract` test is added right after
8
+ * it, through the spec's own `app` and `request`. A spec that is not Nest's —
9
+ * the developer's own — is left as it is, and nothing is said: the test is a
10
+ * convenience, and the developer's tests are theirs.
11
+ */
12
+ /** The e2e spec `nest new` writes. */
13
+ export const E2E_SPEC = "test/app.e2e-spec.ts";
14
+ /** `nest new`'s `GET /` test, byte for byte: the anchor. */
15
+ const ROOT_TEST = ` it('/ (GET)', () => {
16
+ return request(app.getHttpServer())
17
+ .get('/')
18
+ .expect(200)
19
+ .expect('Hello World!');
20
+ });
21
+ `;
22
+ /**
23
+ * `spec` with the contract test after Nest's `GET /` test, or `undefined` when
24
+ * the anchor is not there exactly once, or the contract test already is.
25
+ */
26
+ export function spliceContractTest(spec, entrypoint) {
27
+ const path = `${entrypoint}/_contract`;
28
+ const at = spec.indexOf(ROOT_TEST);
29
+ if (at === -1 ||
30
+ spec.indexOf(ROOT_TEST, at + 1) !== -1 ||
31
+ spec.includes(`'${path}'`)) {
32
+ return undefined;
33
+ }
34
+ const end = at + ROOT_TEST.length;
35
+ return `${spec.slice(0, end)}
36
+ it('${path} (GET)', () => {
37
+ return request(app.getHttpServer()).get('${path}').expect(200);
38
+ });
39
+ ${spec.slice(end)}`;
40
+ }
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { ConflictsNotConfirmedError } from "./apply/conflict.confirmer.js";
3
3
  import { ADAPTER_CATALOG } from "./catalog/adapter.catalog.generated.js";
4
- import { CliCommandError, parseCliCommand, USAGE, } from "./command/command.parser.js";
4
+ import { CliCommandError, commandUsage, parseCliCommand, USAGE, } from "./command/command.parser.js";
5
5
  import { ProjectRefusedError } from "./project/project.inspector.js";
6
6
  import { runCommand } from "./run/command.runner.js";
7
7
  import { InstallFailedError, runInit } from "./run/init.orchestrator.js";
@@ -27,7 +27,7 @@ export async function runCli(argv, io) {
27
27
  try {
28
28
  const command = parseCliCommand(argv);
29
29
  if (command.command === "help") {
30
- io.stdout(USAGE);
30
+ io.stdout(command.topic === undefined ? USAGE : commandUsage(command.topic));
31
31
  return 0;
32
32
  }
33
33
  if (command.command === "version") {
@@ -17,8 +17,11 @@ export type ScaffoldCommand = {
17
17
  /** Accepts each unanswered question's default and confirms overwrites (R5). */
18
18
  readonly yes: boolean;
19
19
  };
20
- export type CliCommand = {
20
+ export type CliCommand =
21
+ /** `--help`; with `topic`, `aventara <topic> --help` (pilot.1). */
22
+ {
21
23
  readonly command: "help";
24
+ readonly topic?: ScaffoldCommandName;
22
25
  } | {
23
26
  readonly command: "version";
24
27
  } | ScaffoldCommand;
@@ -26,6 +29,8 @@ export type CliCommand = {
26
29
  export declare class CliCommandError extends Error {
27
30
  readonly name = "CliCommandError";
28
31
  }
32
+ /** `aventara <command> --help` (pilot.1): that command's usage alone. */
33
+ export declare function commandUsage(command: ScaffoldCommandName): string;
29
34
  export declare const USAGE: string;
30
35
  /** @throws CliCommandError when `argv` is not a command this CLI has. */
31
36
  export declare function parseCliCommand(argv: readonly string[]): CliCommand;
@@ -48,21 +48,43 @@ function usageLines(command) {
48
48
  .map(([left, right]) => ` ${left.padEnd(width)} ${right}`)
49
49
  .join("\n");
50
50
  }
51
+ /** What each command does, in one line: the top-level usage and its own. */
52
+ const SUMMARY = {
53
+ new: "Create a NestJS project with Aventara (nest new, then init).",
54
+ init: "Add Aventara to the NestJS 12 project in this directory.",
55
+ };
56
+ const SYNOPSIS = {
57
+ new: "aventara new <name> [options]",
58
+ init: "aventara init [options]",
59
+ };
60
+ const ASKED = `On a terminal every unanswered question is asked; anywhere else, pass its flag
61
+ or --yes, or the run stops before writing anything.
62
+ `;
63
+ /** `aventara <command> --help` (pilot.1): that command's usage alone. */
64
+ export function commandUsage(command) {
65
+ return `Usage: ${SYNOPSIS[command]}
66
+
67
+ ${SUMMARY[command]}
68
+
69
+ Options:
70
+ ${usageLines(command)}
71
+
72
+ ${ASKED}`;
73
+ }
51
74
  export const USAGE = `aventara — scaffold an Aventara server on NestJS.
52
75
 
53
76
  Usage:
54
- aventara new <name> [options] Create a NestJS project with Aventara (nest new, then init).
77
+ ${SYNOPSIS.new} ${SUMMARY.new}
55
78
  ${usageLines("new")}
56
79
 
57
- aventara init [options] Add Aventara to the NestJS 12 project in this directory.
80
+ ${SYNOPSIS.init} ${SUMMARY.init}
58
81
  ${usageLines("init")}
59
82
 
60
83
  aventara --help Print this and exit 0.
84
+ aventara <command> --help Print that command's usage and exit 0.
61
85
  aventara --version Print this CLI's version and exit 0.
62
86
 
63
- On a terminal every unanswered question is asked; anywhere else, pass its flag
64
- or --yes, or the run stops before writing anything.
65
- `;
87
+ ${ASKED}`;
66
88
  /** @throws CliCommandError when `argv` is not a command this CLI has. */
67
89
  export function parseCliCommand(argv) {
68
90
  const [command, ...rest] = argv;
@@ -78,6 +100,9 @@ export function parseCliCommand(argv) {
78
100
  if (command !== "new" && command !== "init") {
79
101
  throw new CliCommandError(`unknown command ${JSON.stringify(command)}: the commands are new and init; ${HELP}`);
80
102
  }
103
+ if (rest.includes("--help") || rest.includes("-h")) {
104
+ return { command: "help", topic: command };
105
+ }
81
106
  const questions = QUESTIONS[command];
82
107
  const given = {};
83
108
  const options = {};
@@ -1,6 +1,6 @@
1
1
  import type { CatalogEntry } from "../catalog/catalog-entry.interface.js";
2
2
  import { type ProjectInspection } from "../project/project.inspector.js";
3
- import type { OrmScaffold } from "../templates/template.registry.js";
3
+ import { type OrmScaffold } from "../templates/template.registry.js";
4
4
  import type { PackageManager } from "../wizard/wizard.questions.js";
5
5
  /**
6
6
  * §4.1's `plan`: everything `aventara init` will write, decided before anything
@@ -49,4 +49,11 @@ export type PlanInput = {
49
49
  /** Said before the write, beside the planner's own. */
50
50
  readonly warnings?: readonly string[];
51
51
  };
52
+ /**
53
+ * pilot.1 — the frontend's first command, as the developer can run it: the
54
+ * package manager's own one-off runner (`npx` or `pnpm dlx`), and — while this
55
+ * CLI is a prerelease — its dist-tag, so the client comes from the same release
56
+ * (`0.1.0-pilot.1` → `@aventara/client@pilot`). A release has no tag to name.
57
+ */
58
+ export declare function frontendInitCommand(packageManager: PackageManager, cliVersion: string): string;
52
59
  export declare function planInit(input: PlanInput): ProjectPlan;
@@ -1,6 +1,8 @@
1
1
  import { spliceAppModule, wiringInstructions, } from "../apply/app-module.anchor.js";
2
+ import { E2E_SPEC, spliceContractTest } from "../apply/e2e-spec.anchor.js";
2
3
  import { mergeEnvFile, mergeGitignore, mergePackageManifest, mergePnpmWorkspace, } from "../apply/manifest.merger.js";
3
4
  import { APP_MODULE, } from "../project/project.inspector.js";
5
+ import { SCAFFOLD_ENTRYPOINT, } from "../templates/template.registry.js";
4
6
  /**
5
7
  * §4.1's `plan`: everything `aventara init` will write, decided before anything
6
8
  * is written — the files, the anchored `app.module.ts` edit (or the printed
@@ -59,6 +61,17 @@ function writeOf(path, before, after) {
59
61
  },
60
62
  ];
61
63
  }
64
+ /**
65
+ * pilot.1 — the frontend's first command, as the developer can run it: the
66
+ * package manager's own one-off runner (`npx` or `pnpm dlx`), and — while this
67
+ * CLI is a prerelease — its dist-tag, so the client comes from the same release
68
+ * (`0.1.0-pilot.1` → `@aventara/client@pilot`). A release has no tag to name.
69
+ */
70
+ export function frontendInitCommand(packageManager, cliVersion) {
71
+ const tag = /^\d+\.\d+\.\d+-([0-9A-Za-z-]+)/.exec(cliVersion)?.[1];
72
+ const runner = packageManager === "pnpm" ? "pnpm dlx" : "npx";
73
+ return `${runner} @aventara/client${tag === undefined ? "" : `@${tag}`} init`;
74
+ }
62
75
  export function planInit(input) {
63
76
  const { inspection, scaffold, packageManager } = input;
64
77
  const conflicts = new Set();
@@ -119,6 +132,16 @@ export function planInit(input) {
119
132
  if (edit.kind === "edited") {
120
133
  keeping.push(...writeOf(APP_MODULE, appModule, edit.text));
121
134
  replacing.push(...writeOf(APP_MODULE, appModule, edit.text));
135
+ // pilot.1: Nest's own e2e spec also asks for the contract — only when
136
+ // AventaraModule was wired in, so the test proves what it claims.
137
+ const spec = input.read(E2E_SPEC);
138
+ const tested = spec === undefined
139
+ ? undefined
140
+ : spliceContractTest(spec, SCAFFOLD_ENTRYPOINT);
141
+ if (tested !== undefined) {
142
+ keeping.push(...writeOf(E2E_SPEC, spec, tested));
143
+ replacing.push(...writeOf(E2E_SPEC, spec, tested));
144
+ }
122
145
  }
123
146
  else {
124
147
  instructions = wiringInstructions(wiring);
@@ -172,7 +195,7 @@ export function planInit(input) {
172
195
  ]
173
196
  : []),
174
197
  `Start the server: ${run} run start:dev`,
175
- "In your frontend: npx @aventara/client init",
198
+ `In your frontend: ${frontendInitCommand(packageManager, input.aventaraVersion)}`,
176
199
  ],
177
200
  };
178
201
  }
@@ -1,6 +1,6 @@
1
1
  import type { PackageManifest } from "../../apply/manifest.merger.js";
2
2
  import type { ClientShape, ExportReference } from "../../project/service.detector.js";
3
- import type { OrmProjectFacts, OrmScaffold, ReuseInput, ScaffoldInput } from "../template.registry.js";
3
+ import { type OrmProjectFacts, type OrmScaffold, type ReuseInput, type ScaffoldInput } from "../template.registry.js";
4
4
  declare function scaffold(input: ScaffoldInput): OrmScaffold;
5
5
  declare function reuse(input: ReuseInput): OrmScaffold;
6
6
  declare function readProject(directory: string, manifest: PackageManifest): OrmProjectFacts;
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { lowestVersionOf } from "../../catalog/range.reader.js";
4
+ import { SCAFFOLD_ENTRYPOINT, } from "../template.registry.js";
4
5
  /**
5
6
  * The `prisma7` entry's templates (D7): Prisma 7 knowledge as text the CLI
6
7
  * writes and reads, keyed by the providers `@aventara/prisma7-adapter`
@@ -141,7 +142,7 @@ function aventaraConfig(input) {
141
142
  " */",
142
143
  `export async function aventaraConfig(prisma: ${service}) {`,
143
144
  " return {",
144
- " entrypoint: '/api',",
145
+ ` entrypoint: '${SCAFFOLD_ENTRYPOINT}',`,
145
146
  " adapter: await createPrismaAdapter({",
146
147
  input.service.clientMember === undefined
147
148
  ? " client: prisma,"
@@ -28,6 +28,11 @@ export type ProviderTemplate = {
28
28
  readonly nativeBuilds: readonly string[];
29
29
  };
30
30
  export type ModuleKind = "esm" | "cjs";
31
+ /**
32
+ * The entrypoint every scaffold mounts the protocol at: written into
33
+ * `src/aventara.config.ts`, and tested by the e2e spec (pilot.1).
34
+ */
35
+ export declare const SCAFFOLD_ENTRYPOINT = "/api";
31
36
  /** What a template set is asked to scaffold. */
32
37
  export type ScaffoldInput = {
33
38
  readonly entry: CatalogEntry;
@@ -1,4 +1,9 @@
1
1
  import { PRISMA7_TEMPLATES } from "./prisma7/prisma7.templates.js";
2
+ /**
3
+ * The entrypoint every scaffold mounts the protocol at: written into
4
+ * `src/aventara.config.ts`, and tested by the e2e spec (pilot.1).
5
+ */
6
+ export const SCAFFOLD_ENTRYPOINT = "/api";
2
7
  export const TEMPLATE_REGISTRY = {
3
8
  prisma7: PRISMA7_TEMPLATES,
4
9
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aventara/cli",
3
- "version": "0.1.0-pilot.0",
3
+ "version": "0.1.0-pilot.1",
4
4
  "license": "SEE LICENSE IN LICENSE",
5
5
  "description": "Scaffolds an Aventara server: `aventara new` for a new NestJS project, `aventara init` for an existing one.",
6
6
  "type": "module",