@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.
@@ -1,370 +1,12 @@
1
- import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
2
- import path from "node:path";
3
- import { lowestVersionOf } from "../../catalog/range.reader.js";
4
- import { SCAFFOLD_ENTRYPOINT, } from "../template.registry.js";
5
- const CLIENT_OUTPUT = "src/generated/prisma";
6
- const SCHEMA = "prisma/schema.prisma";
7
- const CONFIG_FILES = ["prisma7.config.ts", "prisma.config.ts"];
8
- const DRIVER_PREFIX = "@prisma/adapter-";
9
- const NEW_SERVICE = {
10
- file: "src/prisma.service.ts",
11
- exportName: "PrismaService",
12
- };
13
- const NEW_MODULE = {
14
- file: "src/prisma.service.ts",
15
- exportName: "PrismaModule",
16
- };
17
- const PROVIDERS = {
18
- sqlite: {
19
- driverClass: "PrismaBetterSqlite3",
20
- driverOptions: "{ url }",
21
- databaseUrl: "file:./dev.db",
22
- gitignore: ["/dev.db*"],
23
- nativeBuilds: ["@prisma/engines", "better-sqlite3", "prisma"],
24
- },
25
- postgresql: {
26
- driverClass: "PrismaPg",
27
- driverOptions: "{ connectionString: url }",
28
- databaseUrl: "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public",
29
- gitignore: [],
30
- nativeBuilds: ["@prisma/engines", "prisma"],
31
- },
32
- };
33
- function specifier(from, to, js) {
34
- const relative = path.posix.relative(path.posix.dirname(from), to);
35
- return `${relative.startsWith(".") ? relative : `./${relative}`}${js}`;
36
- }
37
- function schema(input) {
38
- return [
39
- "generator client {",
40
- ` provider = "${input.entry.generators[0]}"`,
41
- ` output = "../${CLIENT_OUTPUT}"`,
42
- ...(input.moduleKind === "cjs" ? [' moduleFormat = "cjs"'] : []),
43
- "}",
44
- "",
45
- "datasource db {",
46
- ` provider = "${input.provider}"`,
47
- "}",
48
- "",
49
- "model User {",
50
- " id Int @id @default(autoincrement())",
51
- " email String @unique",
52
- " name String?",
53
- "}",
54
- "",
55
- ].join("\n");
56
- }
57
- function prismaConfig() {
58
- return [
59
- "import { existsSync } from 'node:fs';",
60
- "import { defineConfig } from 'prisma/config';",
61
- "",
62
- "// Prisma reads DATABASE_URL from the environment and does not load .env itself.",
63
- "if (existsSync('.env')) {",
64
- " process.loadEnvFile('.env');",
65
- "}",
66
- "",
67
- "export default defineConfig({",
68
- ` schema: '${SCHEMA}',`,
69
- " migrations: {",
70
- " path: 'prisma/migrations',",
71
- " },",
72
- " datasource: {",
73
- " url: process.env.DATABASE_URL,",
74
- " },",
75
- "});",
76
- "",
77
- ].join("\n");
78
- }
79
- function prismaService(input) {
80
- const provider = PROVIDERS[input.provider];
81
- const driver = input.entry.drivers[input.provider];
82
- return [
83
- "import { Injectable, Module, type OnModuleDestroy } from '@nestjs/common';",
84
- `import { ${provider.driverClass} } from '${driver}';`,
85
- `import { PrismaClient } from '${specifier(NEW_SERVICE.file, `${input.clientOutput}/client`, input.js)}';`,
86
- "",
87
- "@Injectable()",
88
- "export class PrismaService extends PrismaClient implements OnModuleDestroy {",
89
- " constructor() {",
90
- " const url = process.env.DATABASE_URL;",
91
- " if (!url) {",
92
- " // Thrown while Nest builds the application, before it listens.",
93
- " throw new Error(",
94
- " 'DATABASE_URL is not set: put it in .env (the start scripts load it) or in the environment.',",
95
- " );",
96
- " }",
97
- ` super({ adapter: new ${provider.driverClass}(${provider.driverOptions}) });`,
98
- " }",
99
- "",
100
- " async onModuleDestroy(): Promise<void> {",
101
- " await this.$disconnect();",
102
- " }",
103
- "}",
104
- "",
105
- "@Module({ providers: [PrismaService], exports: [PrismaService] })",
106
- "export class PrismaModule {}",
107
- "",
108
- ].join("\n");
109
- }
110
- function aventaraConfig(input) {
111
- const file = "src/aventara.config.ts";
112
- const service = input.service.exportName;
113
- return [
114
- `import { createPrismaAdapter } from '${input.entry.adapterPackage}';`,
115
- `import { discovery } from '${specifier(file, input.artifact.replace(/\.ts$/, ""), input.js)}';`,
116
- `import type { ${service} } from '${specifier(file, input.service.file.replace(/\.ts$/, ""), input.js)}';`,
117
- "",
118
- "/**",
119
- " * Aventara's configuration: the entrypoint, and the restrictions and",
120
- " * pipelines you add. AventaraModule (app.module.ts) builds the framework from",
121
- " * it; `discovery` is regenerated by the aventara:prepare script.",
122
- " */",
123
- `export async function aventaraConfig(prisma: ${service}) {`,
124
- " return {",
125
- ` entrypoint: '${SCAFFOLD_ENTRYPOINT}',`,
126
- " adapter: await createPrismaAdapter({",
127
- input.service.clientMember === undefined
128
- ? " client: prisma,"
129
- : ` client: prisma.${input.service.clientMember},`,
130
- " discovery,",
131
- ` provider: '${input.provider}',`,
132
- ` driver: '${input.entry.drivers[input.provider]}',`,
133
- " }),",
134
- " } as const;",
135
- "}",
136
- "",
137
- ].join("\n");
138
- }
139
- function prepare(input) {
140
- return [
141
- input.entry.bin,
142
- `--schema ${input.schema}`,
143
- `--client ${input.clientOutput}`,
144
- `--provider ${input.provider}`,
145
- `--driver ${input.entry.drivers[input.provider]}`,
146
- `--out ${input.artifact}`,
147
- ].join(" ");
148
- }
149
- function appModule(service, module, js) {
150
- const from = "src/app.module.ts";
151
- const sameFile = service.file === module.file;
152
- return {
153
- imports: [
154
- `import { aventaraConfig } from '${specifier(from, "src/aventara.config", js)}';`,
155
- ...(sameFile
156
- ? [
157
- `import { ${module.exportName}, ${service.exportName} } from '${specifier(from, service.file.replace(/\.ts$/, ""), js)}';`,
158
- ]
159
- : [
160
- `import { ${module.exportName} } from '${specifier(from, module.file.replace(/\.ts$/, ""), js)}';`,
161
- `import { ${service.exportName} } from '${specifier(from, service.file.replace(/\.ts$/, ""), js)}';`,
162
- ]),
163
- ],
164
- entries: [
165
- `${module.exportName},`,
166
- [
167
- "AventaraModule.forRootAsync({",
168
- ` imports: [${module.exportName}],`,
169
- ` inject: [${service.exportName}],`,
170
- ` useFactory: async (prisma: ${service.exportName}) => ({`,
171
- " config: await aventaraConfig(prisma),",
172
- " }),",
173
- "}),",
174
- ].join("\n"),
175
- ],
176
- };
177
- }
178
- function scaffold(input) {
179
- const js = input.moduleKind === "esm" ? ".js" : "";
180
- const floor = lowestVersionOf(input.entry.range);
181
- const provider = PROVIDERS[input.provider];
182
- const driver = input.entry.drivers[input.provider];
183
- const run = input.packageManager === "pnpm" ? "pnpm exec" : "npx";
184
- const artifact = "src/generated/aventara/discovery.artifact.ts";
185
- return {
186
- files: [
187
- { path: SCHEMA, content: schema(input) },
188
- { path: "prisma.config.ts", content: prismaConfig() },
189
- {
190
- path: NEW_SERVICE.file,
191
- content: prismaService({ ...input, clientOutput: CLIENT_OUTPUT, js }),
192
- },
193
- {
194
- path: "src/aventara.config.ts",
195
- content: aventaraConfig({
196
- ...input,
197
- service: NEW_SERVICE,
198
- artifact,
199
- js,
200
- }),
201
- },
202
- ],
203
- env: { DATABASE_URL: provider.databaseUrl },
204
- gitignore: ["/src/generated/", ...provider.gitignore],
205
- dependencies: { [input.entry.orm.package]: floor, [driver]: floor },
206
- devDependencies: { prisma: floor },
207
- prepare: prepare({
208
- ...input,
209
- schema: SCHEMA,
210
- clientOutput: CLIENT_OUTPUT,
211
- artifact,
212
- }),
213
- appModule: appModule(NEW_SERVICE, NEW_MODULE, js),
214
- nativeBuilds: provider.nativeBuilds,
215
- nextSteps: [
216
- ...(input.provider === "postgresql"
217
- ? ["Set DATABASE_URL in .env to your PostgreSQL database."]
218
- : []),
219
- `Create the database tables: ${run} prisma db push`,
220
- ],
221
- };
222
- }
223
- function reuse(input) {
224
- const js = input.moduleKind === "esm" ? ".js" : "";
225
- const { facts } = input;
226
- return {
227
- files: [
228
- ...(input.writeService
229
- ? [
230
- {
231
- path: NEW_SERVICE.file,
232
- content: prismaService({
233
- entry: input.entry,
234
- provider: facts.provider,
235
- clientOutput: facts.clientOutput,
236
- js,
237
- }),
238
- },
239
- ]
240
- : []),
241
- {
242
- path: "src/aventara.config.ts",
243
- content: aventaraConfig({
244
- entry: input.entry,
245
- provider: facts.provider,
246
- service: input.service,
247
- artifact: input.artifact,
248
- js,
249
- }),
250
- },
251
- ],
252
- env: {},
253
- gitignore: [`/${path.posix.dirname(input.artifact)}/`],
254
- dependencies: {},
255
- devDependencies: {},
256
- prepare: prepare({
257
- entry: input.entry,
258
- schema: facts.schema,
259
- clientOutput: facts.clientOutput,
260
- provider: facts.provider,
261
- artifact: input.artifact,
262
- }),
263
- appModule: appModule(input.service, input.module, js),
264
- nativeBuilds: [],
265
- nextSteps: [],
266
- };
267
- }
268
- function schemaLocation(directory, manifest) {
269
- for (const config of CONFIG_FILES) {
270
- const file = path.join(directory, config);
271
- if (existsSync(file)) {
272
- const declared = /\bschema\s*:\s*['"]([^'"]+)['"]/.exec(readFileSync(file, "utf8"))?.[1];
273
- if (declared !== undefined) {
274
- return path.posix.normalize(declared);
275
- }
276
- }
277
- }
278
- const prisma = manifest.prisma;
279
- return typeof prisma?.schema === "string"
280
- ? path.posix.normalize(prisma.schema)
281
- : SCHEMA;
282
- }
283
- function schemaFiles(directory, schemaPath) {
284
- const full = path.join(directory, schemaPath);
285
- if (!existsSync(full)) {
286
- return [];
287
- }
288
- if (!statSync(full).isDirectory()) {
289
- return [schemaPath];
290
- }
291
- return readdirSync(full, { recursive: true, withFileTypes: true })
292
- .filter((entry) => entry.isFile() && entry.name.endsWith(".prisma"))
293
- .map((entry) => path
294
- .relative(directory, path.join(entry.parentPath, entry.name))
295
- .split(path.sep)
296
- .join("/"))
297
- .sort();
298
- }
299
- function blocks(text, kind) {
300
- return [
301
- ...text.matchAll(new RegExp(`^\\s*${kind}\\s+\\w+\\s*\\{([^}]*)\\}`, "gm")),
302
- ].map((match) => match[1]);
303
- }
304
- function setting(block, name) {
305
- return new RegExp(`^\\s*${name}\\s*=\\s*"([^"]*)"`, "m").exec(block)?.[1];
306
- }
307
- function readProject(directory, manifest) {
308
- const schemaPath = schemaLocation(directory, manifest);
309
- let provider;
310
- let generator;
311
- let clientOutput;
312
- for (const file of schemaFiles(directory, schemaPath)) {
313
- const text = readFileSync(path.join(directory, file), "utf8").replace(/\/\/.*$/gm, "");
314
- provider ??= blocks(text, "datasource")
315
- .map((block) => setting(block, "provider"))
316
- .find((value) => value !== undefined);
317
- for (const block of blocks(text, "generator")) {
318
- const declared = setting(block, "provider");
319
- const output = setting(block, "output");
320
- if (generator === undefined || /^prisma-client/.test(declared ?? "")) {
321
- generator = declared;
322
- clientOutput =
323
- output === undefined
324
- ? undefined
325
- : path.posix.normalize(path.posix.join(path.posix.dirname(file), output));
326
- }
327
- }
328
- }
329
- const dependencies = [
330
- ...Object.keys(manifest.dependencies ?? {}),
331
- ...Object.keys(manifest.devDependencies ?? {}),
332
- ];
333
- return {
334
- schema: schemaPath,
335
- provider,
336
- generator,
337
- clientOutput,
338
- drivers: dependencies.filter((name) => name.startsWith(DRIVER_PREFIX)),
339
- };
340
- }
341
- function clientShape(facts) {
342
- return {
343
- clientClass: "PrismaClient",
344
- importsClient: (imported, file) => {
345
- if (!imported.startsWith(".")) {
346
- return imported === "@prisma/client";
347
- }
348
- if (facts.clientOutput === undefined) {
349
- return false;
350
- }
351
- const resolved = path.posix
352
- .join(path.posix.dirname(file), imported)
353
- .replace(/\.(js|ts)$/, "");
354
- return (resolved === facts.clientOutput ||
355
- resolved === `${facts.clientOutput}/client` ||
356
- resolved === `${facts.clientOutput}/index`);
357
- },
358
- skip: facts.clientOutput === undefined
359
- ? ["src/generated"]
360
- : ["src/generated", facts.clientOutput],
361
- };
362
- }
1
+ import { PRISMA7_NEST_TEMPLATES } from "./prisma7-nest.templates.js";
2
+ import { PRISMA7_NEXT_TEMPLATES } from "./prisma7-next.templates.js";
3
+ import { clientShape, PROVIDERS, readProject, } from "./prisma7-orm.templates.js";
363
4
  export const PRISMA7_TEMPLATES = {
364
5
  providers: PROVIDERS,
365
- scaffold,
366
6
  readProject,
367
7
  clientShape,
368
- newService: { service: NEW_SERVICE, module: NEW_MODULE },
369
- reuse,
8
+ hosts: {
9
+ nest: PRISMA7_NEST_TEMPLATES,
10
+ next: PRISMA7_NEXT_TEMPLATES,
11
+ },
370
12
  };
@@ -1,6 +1,6 @@
1
1
  import { PRISMA7_TEMPLATES } from "./prisma7/prisma7.templates.js";
2
- export const SCAFFOLD_ENTRYPOINT = "/api";
3
- export const TEMPLATE_REGISTRY = {
2
+ export const SCAFFOLD_API = "/api";
3
+ const TEMPLATE_REGISTRY = {
4
4
  prisma7: PRISMA7_TEMPLATES,
5
5
  };
6
6
  export function templatesFor(entry) {
@@ -1,6 +1,6 @@
1
- import { WizardAnswerError } from "./wizard.questions.js";
2
- export class UnansweredQuestionsError extends Error {
3
- name = "UnansweredQuestionsError";
1
+ import { AvWizardAnswerError } from "./wizard.questions.js";
2
+ export class AvUnansweredQuestionsError extends Error {
3
+ name = "AvUnansweredQuestionsError";
4
4
  }
5
5
  function flagSpelling(question, context) {
6
6
  if (!question.flag.startsWith("--")) {
@@ -41,7 +41,7 @@ async function prompt(question, context, prompter) {
41
41
  return question.parse(raw, context);
42
42
  }
43
43
  catch (error) {
44
- if (!(error instanceof WizardAnswerError)) {
44
+ if (!(error instanceof AvWizardAnswerError)) {
45
45
  throw error;
46
46
  }
47
47
  prompter.say(`${error.message}.`);
@@ -90,7 +90,7 @@ export async function resolveAnswers(questions, sources) {
90
90
  });
91
91
  }
92
92
  if (missing.length > 0) {
93
- throw new UnansweredQuestionsError(unansweredSentence(missing, sources.yes));
93
+ throw new AvUnansweredQuestionsError(unansweredSentence(missing, sources.yes));
94
94
  }
95
95
  return answered;
96
96
  }
@@ -1,13 +1,13 @@
1
1
  import { createInterface } from "node:readline";
2
- export class WizardCancelledError extends Error {
3
- name = "WizardCancelledError";
2
+ export class AvWizardCancelledError extends Error {
3
+ name = "AvWizardCancelledError";
4
4
  }
5
5
  export function createReadlinePrompter(streams) {
6
6
  let session;
7
7
  let ended = false;
8
8
  const lines = [];
9
9
  const waiting = [];
10
- const cancelled = () => new WizardCancelledError("the wizard was closed before it was answered");
10
+ const cancelled = () => new AvWizardCancelledError("the wizard was closed before it was answered");
11
11
  const open = () => {
12
12
  if (session === undefined) {
13
13
  session = createInterface({
@@ -1,7 +1,7 @@
1
1
  import { modulesProviding, } from "../project/service.detector.js";
2
2
  export const PACKAGE_MANAGERS = ["npm", "pnpm"];
3
- export class WizardAnswerError extends Error {
4
- name = "WizardAnswerError";
3
+ export class AvWizardAnswerError extends Error {
4
+ name = "AvWizardAnswerError";
5
5
  }
6
6
  function ormLabel(entry) {
7
7
  return `${entry.orm.label} ${entry.orm.major}`;
@@ -32,7 +32,7 @@ export const NAME_QUESTION = {
32
32
  parse: (raw) => {
33
33
  const name = raw.trim();
34
34
  if (!NAME.test(name)) {
35
- throw new WizardAnswerError(`${JSON.stringify(raw)} is not a project name: use letters, digits, ".", "_" and "-", starting with a letter or digit`);
35
+ throw new AvWizardAnswerError(`${JSON.stringify(raw)} is not a project name: use letters, digits, ".", "_" and "-", starting with a letter or digit`);
36
36
  }
37
37
  return name;
38
38
  },
@@ -52,7 +52,7 @@ export const ORM_QUESTION = {
52
52
  const alias = raw.trim().toLowerCase();
53
53
  const entry = context.catalog.find((candidate) => candidate.alias === alias);
54
54
  if (entry === undefined) {
55
- throw new WizardAnswerError(`there is no Aventara adapter for the ORM ${JSON.stringify(raw)}; ${ormAliasesSentence(context.catalog)}`);
55
+ throw new AvWizardAnswerError(`there is no Aventara adapter for the ORM ${JSON.stringify(raw)}; ${ormAliasesSentence(context.catalog)}`);
56
56
  }
57
57
  return entry;
58
58
  },
@@ -82,7 +82,7 @@ export const DB_QUESTION = {
82
82
  const orm = requireOrm(context);
83
83
  const db = raw.trim().toLowerCase();
84
84
  if (!orm.providers.includes(db)) {
85
- throw new WizardAnswerError(`the ${ormLabel(orm)} adapter does not support the database ${JSON.stringify(raw)}; choose ${orm.providers.join(" or ")}`);
85
+ throw new AvWizardAnswerError(`the ${ormLabel(orm)} adapter does not support the database ${JSON.stringify(raw)}; choose ${orm.providers.join(" or ")}`);
86
86
  }
87
87
  return db;
88
88
  },
@@ -98,7 +98,7 @@ export const PACKAGE_MANAGER_QUESTION = {
98
98
  parse: (raw) => {
99
99
  const manager = PACKAGE_MANAGERS.find((candidate) => candidate === raw.trim().toLowerCase());
100
100
  if (manager === undefined) {
101
- throw new WizardAnswerError(`${JSON.stringify(raw)} is not a supported package manager; choose ${PACKAGE_MANAGERS.join(" or ")}`);
101
+ throw new AvWizardAnswerError(`${JSON.stringify(raw)} is not a supported package manager; choose ${PACKAGE_MANAGERS.join(" or ")}`);
102
102
  }
103
103
  return manager;
104
104
  },
@@ -109,7 +109,7 @@ function reference(raw, flag) {
109
109
  const exportName = raw.slice(hash + 1).trim();
110
110
  const file = raw.slice(0, hash).trim().replace(/^\.\//, "");
111
111
  if (hash <= 0 || !/^[A-Za-z_$][\w$]*$/.test(exportName)) {
112
- throw new WizardAnswerError(`${flag} takes <path#Export>, a file and the name it exports, such as src/prisma.service.ts#PrismaService; got ${JSON.stringify(raw)}`);
112
+ throw new AvWizardAnswerError(`${flag} takes <path#Export>, a file and the name it exports, such as src/prisma.service.ts#PrismaService; got ${JSON.stringify(raw)}`);
113
113
  }
114
114
  return { file, exportName };
115
115
  }
@@ -136,7 +136,7 @@ function serviceOptions(services) {
136
136
  ? found
137
137
  : [{ ...services.newService, isNew: true }];
138
138
  }
139
- export const SERVICE_QUESTION = {
139
+ const SERVICE_QUESTION = {
140
140
  id: "service",
141
141
  flag: "--prisma-service",
142
142
  placeholder: "<path#Export>",
@@ -166,7 +166,7 @@ export const SERVICE_QUESTION = {
166
166
  const options = serviceOptions(services);
167
167
  const chosen = options.find((option) => option.file === wanted.file && option.exportName === wanted.exportName);
168
168
  if (chosen === undefined) {
169
- throw new WizardAnswerError(`${spelled(wanted)} is not a service found in src/ (a class extending ${services.clientClass}, or one holding it); found: ${options
169
+ throw new AvWizardAnswerError(`${spelled(wanted)} is not a service found in src/ (a class extending ${services.clientClass}, or one holding it); found: ${options
170
170
  .filter((option) => !option.isNew)
171
171
  .map(spelled)
172
172
  .join(", ") || "none"}`);
@@ -175,7 +175,7 @@ export const SERVICE_QUESTION = {
175
175
  },
176
176
  describe: spelled,
177
177
  };
178
- export const MODULE_QUESTION = {
178
+ const MODULE_QUESTION = {
179
179
  id: "module",
180
180
  flag: "--prisma-module",
181
181
  placeholder: "<path#Export>",
@@ -202,7 +202,7 @@ export const MODULE_QUESTION = {
202
202
  : modulesProviding(services.scan, service);
203
203
  const chosen = modules.find((module) => module.file === wanted.file && module.exportName === wanted.exportName);
204
204
  if (chosen === undefined) {
205
- throw new WizardAnswerError(`${spelled(wanted)} is not a module that provides and exports ${service.exportName}; found: ${modules.map(spelled).join(", ") || "none"}`);
205
+ throw new AvWizardAnswerError(`${spelled(wanted)} is not a module that provides and exports ${service.exportName}; found: ${modules.map(spelled).join(", ") || "none"}`);
206
206
  }
207
207
  return { file: chosen.file, exportName: chosen.exportName };
208
208
  },
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "@aventara/cli",
3
- "version": "1.0.0-rc.0",
3
+ "version": "1.0.0-rc.10",
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",
7
7
  "engines": {
8
- "node": "^22.18.0 || >=24.2.0"
8
+ "node": "^22.23.4 || >=24.22.1"
9
9
  },
10
10
  "bin": {
11
11
  "aventara": "./dist/aventara.bin.js"
@@ -23,7 +23,7 @@
23
23
  "typescript": "^5.9.2"
24
24
  },
25
25
  "scripts": {
26
- "prebuild": "node scripts/adapter-catalog.generator.ts",
26
+ "prebuild": "node scripts/adapter-catalog.generator.ts && node ../../scripts/build/host-catalog.generator.ts --out src/catalog/host.catalog.generated.ts && node scripts/agent-skill.generator.ts",
27
27
  "build": "node ../../scripts/build/build-output.cleaner.ts dist && tsc -p tsconfig.json --removeComments",
28
28
  "typecheck": "tsc -p tsconfig.typecheck.json --noEmit",
29
29
  "test": "vitest run --typecheck --config ../../vitest.config.mts --root .",