@aventara/cli 1.0.0-rc.1 → 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.
@@ -1,3 +1,4 @@
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
3
  export class AvCliCommandError extends Error {
3
4
  name = "AvCliCommandError";
@@ -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 AvCliCommandError(`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 AvCliCommandError(`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,10 +153,19 @@ 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
171
  throw new AvCliCommandError(`unexpected argument ${JSON.stringify(argument)}: aventara ${command} takes ${positional === undefined ? "no" : "one"} positional argument; ${HELP}`);
@@ -132,6 +176,28 @@ 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;
@@ -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";
@@ -0,0 +1,131 @@
1
+ import { spliceAppModule, wiringInstructions, } from "../apply/app-module.anchor.js";
2
+ import { E2E_SPEC, spliceContractTest } from "../apply/e2e-spec.anchor.js";
3
+ import { CORS_ORIGINS_VARIABLE, MAIN_FILE, mainCorsInstructions, SCAFFOLD_CORS_ORIGINS, spliceMainCors, } from "../apply/main-cors.anchor.js";
4
+ import { hostOf } from "../catalog/host-entry.interface.js";
5
+ import { APP_MODULE, } from "../project/project.inspector.js";
6
+ import { SCAFFOLD_API, } from "../templates/template.registry.js";
7
+ import { PREPARE_SCRIPT, } from "./host-plan.interface.js";
8
+ const PREPARED_SCRIPTS = [
9
+ "build",
10
+ "start",
11
+ "start:dev",
12
+ "start:debug",
13
+ ];
14
+ const START_SCRIPTS = {
15
+ start: { from: "nest start", to: "nest start --env-file .env" },
16
+ "start:dev": {
17
+ from: "nest start --watch",
18
+ to: "nest start --watch --env-file .env",
19
+ },
20
+ "start:debug": {
21
+ from: "nest start --debug --watch",
22
+ to: "nest start --debug --watch --env-file .env",
23
+ },
24
+ "start:prod": {
25
+ from: "node dist/main",
26
+ to: "node --env-file-if-exists=.env dist/main",
27
+ },
28
+ };
29
+ const E2E_SCRIPTS = {
30
+ esm: {
31
+ from: "vitest run --config ./vitest.config.e2e.ts",
32
+ to: "node --env-file-if-exists=.env ./node_modules/vitest/vitest.mjs run --config ./vitest.config.e2e.ts",
33
+ },
34
+ cjs: {
35
+ from: "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config ./test/jest-e2e.json",
36
+ to: "node --env-file-if-exists=.env --experimental-vm-modules ./node_modules/jest/bin/jest.js --config ./test/jest-e2e.json",
37
+ },
38
+ };
39
+ const NEST = hostOf("nest");
40
+ export function frontendInitCommand(packageManager, cliVersion) {
41
+ const tag = /^\d+\.\d+\.\d+-([0-9A-Za-z-]+)/.exec(cliVersion)?.[1];
42
+ const runner = packageManager === "pnpm" ? "pnpm dlx" : "npx";
43
+ return `${runner} @aventara/client${tag === undefined ? "" : `@${tag}`} init`;
44
+ }
45
+ export function planNestHost(builder, input) {
46
+ const { inspection, scaffold, packageManager } = input;
47
+ const wiring = {
48
+ imports: [
49
+ `import { AventaraModule } from '${NEST.package}';`,
50
+ ...scaffold.appModule.imports,
51
+ ],
52
+ entries: scaffold.appModule.entries,
53
+ };
54
+ const appModule = builder.read(APP_MODULE);
55
+ const edit = input.wiringByHand !== undefined
56
+ ? { kind: "not-anchored", reason: input.wiringByHand }
57
+ : appModule === undefined
58
+ ? {
59
+ kind: "not-anchored",
60
+ reason: `there is no ${APP_MODULE}`,
61
+ }
62
+ : spliceAppModule(appModule, wiring);
63
+ if (edit.kind === "edited") {
64
+ builder.write(APP_MODULE, appModule, edit.text);
65
+ const spec = builder.read(E2E_SPEC);
66
+ const tested = spec === undefined ? undefined : spliceContractTest(spec, SCAFFOLD_API);
67
+ if (tested !== undefined) {
68
+ builder.write(E2E_SPEC, spec, tested);
69
+ }
70
+ }
71
+ else {
72
+ builder.instruct(wiringInstructions(wiring));
73
+ builder.warn(`${APP_MODULE} was not edited: ${edit.reason}; add AventaraModule by hand, as printed below`);
74
+ }
75
+ const main = builder.read(MAIN_FILE);
76
+ const cors = main === undefined
77
+ ? { kind: "not-anchored", reason: `there is no ${MAIN_FILE}` }
78
+ : spliceMainCors(main);
79
+ if (cors.kind === "edited") {
80
+ builder.write(MAIN_FILE, main, cors.text);
81
+ }
82
+ else if (cors.kind === "configured") {
83
+ builder.warn(`${MAIN_FILE} already configures CORS, so it was left as it is; a browser frontend on another origin is answered only if that configuration allows it`);
84
+ }
85
+ else if (cors.kind === "not-anchored") {
86
+ builder.instruct(mainCorsInstructions());
87
+ builder.warn(`${MAIN_FILE} was not edited: ${cors.reason}; add CORS by hand, as printed below`);
88
+ }
89
+ const corsWanted = cors.kind !== "configured";
90
+ const scaffoldWritesEnv = Object.keys(scaffold.env).length > 0;
91
+ const env = builder.read(".env");
92
+ const corsStep = corsWanted && !scaffoldWritesEnv && env === undefined
93
+ ? [
94
+ `Let a browser frontend call the server: add ${CORS_ORIGINS_VARIABLE}="${SCAFFOLD_CORS_ORIGINS}" to .env (unset, CORS stays off)`,
95
+ ]
96
+ : [];
97
+ const run = packageManager === "pnpm" ? "pnpm" : "npm";
98
+ return {
99
+ dependencies: { [NEST.package]: input.aventaraVersion },
100
+ build: "aventara-build",
101
+ scripts: Object.fromEntries(PREPARED_SCRIPTS.map((script) => [
102
+ `pre${script}`,
103
+ `${packageManager} run ${PREPARE_SCRIPT}`,
104
+ ])),
105
+ rewrites: inspection.usesNestConfig
106
+ ? {}
107
+ : {
108
+ ...START_SCRIPTS,
109
+ "test:e2e": E2E_SCRIPTS[inspection.moduleKind],
110
+ },
111
+ ...(corsWanted
112
+ ? {
113
+ envEntry: {
114
+ key: CORS_ORIGINS_VARIABLE,
115
+ value: SCAFFOLD_CORS_ORIGINS,
116
+ },
117
+ }
118
+ : {}),
119
+ gitignore: [],
120
+ nextSteps: [
121
+ ...(inspection.usesNestConfig
122
+ ? [
123
+ "The start scripts were left as they are: @nestjs/config loads .env, so make sure it does before Prisma connects.",
124
+ ]
125
+ : []),
126
+ ...corsStep,
127
+ `Start the server: ${run} run start:dev`,
128
+ `In your frontend: ${frontendInitCommand(packageManager, input.aventaraVersion)}`,
129
+ ],
130
+ };
131
+ }
@@ -0,0 +1,133 @@
1
+ import { hostOf } from "../catalog/host-entry.interface.js";
2
+ import { SERVER_IMPORT } from "../templates/next-server.entry.js";
3
+ import { SCAFFOLD_API } from "../templates/template.registry.js";
4
+ import { PREPARE_SCRIPT, } from "./host-plan.interface.js";
5
+ const NEXT = hostOf("next");
6
+ const ROUTE_FILE = `src/app${SCAFFOLD_API}/[...aventara]/route.ts`;
7
+ const INSTRUMENTATION_FILE = "src/instrumentation.ts";
8
+ const INSTRUMENTATION_FILES = [INSTRUMENTATION_FILE, "src/instrumentation.js"];
9
+ const SERVER_ONLY = { "server-only": "0.0.1" };
10
+ function routeFile() {
11
+ return [
12
+ `import { handlers } from "${SERVER_IMPORT}";`,
13
+ "",
14
+ "// Aventara's API under the configuration's `api` path. Every method is exported,",
15
+ "// so one an operation does not take is answered by Aventara (405 with Allow).",
16
+ "export const { GET, POST, PUT, PATCH, DELETE } = handlers;",
17
+ "",
18
+ ].join("\n");
19
+ }
20
+ const REGISTER_BODY = [
21
+ ' if (process.env.NEXT_RUNTIME === "nodejs") {',
22
+ ` const { ready } = await import("${SERVER_IMPORT}");`,
23
+ " await ready();",
24
+ " }",
25
+ ];
26
+ function instrumentationFile() {
27
+ return [
28
+ "/**",
29
+ " * Next.js calls register() once, when the server starts. On the Node.js runtime",
30
+ " * it builds Aventara's framework, so a bad configuration stops `next start` and",
31
+ " * the first request finds it ready.",
32
+ " */",
33
+ "export async function register() {",
34
+ ...REGISTER_BODY,
35
+ "}",
36
+ "",
37
+ ].join("\n");
38
+ }
39
+ const NEXT_CONFIG_FILES = [
40
+ "next.config.ts",
41
+ "next.config.mts",
42
+ "next.config.mjs",
43
+ "next.config.js",
44
+ "next.config.cjs",
45
+ ];
46
+ const CONFIG_OPENING = /^const nextConfig(?:: NextConfig)? = \{\n/m;
47
+ function externalLines(packages, indent) {
48
+ return [
49
+ `${indent}// Loaded by Node.js once per server process, not bundled into each layer:`,
50
+ `${indent}// one Aventara core and one Prisma client, shared by every route and page.`,
51
+ `${indent}serverExternalPackages: [${packages.map((name) => JSON.stringify(name)).join(", ")}],`,
52
+ ];
53
+ }
54
+ function newNextConfig(packages) {
55
+ return [
56
+ 'import type { NextConfig } from "next";',
57
+ "",
58
+ "const nextConfig: NextConfig = {",
59
+ ...externalLines(packages, " "),
60
+ "};",
61
+ "",
62
+ "export default nextConfig;",
63
+ "",
64
+ ].join("\n");
65
+ }
66
+ function withServerExternals(text, packages) {
67
+ if (text.includes("serverExternalPackages")) {
68
+ return packages.every((name) => text.includes(JSON.stringify(name)))
69
+ ? text
70
+ : undefined;
71
+ }
72
+ const opening = CONFIG_OPENING.exec(text);
73
+ if (opening === null) {
74
+ return undefined;
75
+ }
76
+ const at = opening.index + opening[0].length;
77
+ const indent = /^[ \t]+/.exec(text.slice(at))?.[0] ?? " ";
78
+ return `${text.slice(0, at)}${externalLines(packages, indent).join("\n")}\n${text.slice(at)}`;
79
+ }
80
+ export function planNextHost(builder, input) {
81
+ builder.file(ROUTE_FILE, routeFile());
82
+ const existing = INSTRUMENTATION_FILES.find((file) => builder.read(file) !== undefined);
83
+ const wanted = instrumentationFile();
84
+ if (existing === undefined) {
85
+ builder.file(INSTRUMENTATION_FILE, wanted);
86
+ }
87
+ else if (builder.read(existing) !== wanted) {
88
+ builder.instruct([
89
+ `Add Aventara's startup to register() in ${existing}:`,
90
+ "",
91
+ ...REGISTER_BODY,
92
+ "",
93
+ ].join("\n"));
94
+ builder.warn(`${existing} is yours, so it was left as it is; add the lines printed below to its register()`);
95
+ }
96
+ const configFile = NEXT_CONFIG_FILES.find((file) => builder.read(file) !== undefined);
97
+ if (configFile === undefined) {
98
+ builder.file(NEXT_CONFIG_FILES[0], newNextConfig(input.externalPackages));
99
+ }
100
+ else {
101
+ const before = builder.read(configFile);
102
+ const after = withServerExternals(before, input.externalPackages);
103
+ if (after === undefined) {
104
+ builder.instruct([
105
+ `Add Aventara's server packages to the configuration object in ${configFile} (merge them into serverExternalPackages if it has one):`,
106
+ "",
107
+ ...externalLines(input.externalPackages, " "),
108
+ "",
109
+ ].join("\n"));
110
+ builder.warn(`${configFile} is yours, so it was left as it is; add the serverExternalPackages line printed below to it`);
111
+ }
112
+ else {
113
+ builder.write(configFile, before, after);
114
+ }
115
+ }
116
+ const { layout } = NEXT;
117
+ const run = input.packageManager === "pnpm" ? "pnpm" : "npm";
118
+ return {
119
+ dependencies: {
120
+ [NEXT.package]: input.aventaraVersion,
121
+ ...SERVER_ONLY,
122
+ },
123
+ build: "aventara-build --client",
124
+ scripts: { prebuild: `${input.packageManager} run ${PREPARE_SCRIPT}` },
125
+ rewrites: {},
126
+ gitignore: [
127
+ `/${layout.generated}/`,
128
+ ...(layout.client === undefined ? [] : [`/${layout.client}`]),
129
+ ...(layout.universal === undefined ? [] : [`/${layout.universal}`]),
130
+ ],
131
+ nextSteps: [`Start the app: ${run} run dev`],
132
+ };
133
+ }