@aventara/cli 0.0.0-stage → 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.
Files changed (54) hide show
  1. package/LICENSE +91 -0
  2. package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
  3. package/README.md +120 -2
  4. package/dist/apply/app-module.anchor.d.ts +30 -0
  5. package/dist/apply/app-module.anchor.js +185 -0
  6. package/dist/apply/conflict.confirmer.d.ts +24 -0
  7. package/dist/apply/conflict.confirmer.js +39 -0
  8. package/dist/apply/e2e-spec.anchor.d.ts +18 -0
  9. package/dist/apply/e2e-spec.anchor.js +40 -0
  10. package/dist/apply/manifest.merger.d.ts +47 -0
  11. package/dist/apply/manifest.merger.js +139 -0
  12. package/dist/aventara.bin.d.ts +2 -0
  13. package/dist/aventara.bin.js +15 -0
  14. package/dist/catalog/adapter.catalog.generated.d.ts +19 -0
  15. package/dist/catalog/adapter.catalog.generated.js +35 -0
  16. package/dist/catalog/catalog-entry.interface.d.ts +46 -0
  17. package/dist/catalog/catalog-entry.interface.js +1 -0
  18. package/dist/catalog/catalog.matcher.d.ts +46 -0
  19. package/dist/catalog/catalog.matcher.js +83 -0
  20. package/dist/catalog/range.reader.d.ts +17 -0
  21. package/dist/catalog/range.reader.js +77 -0
  22. package/dist/cli.d.ts +30 -0
  23. package/dist/cli.js +96 -0
  24. package/dist/command/command.parser.d.ts +36 -0
  25. package/dist/command/command.parser.js +163 -0
  26. package/dist/node-version.guard.d.ts +8 -0
  27. package/dist/node-version.guard.js +59 -0
  28. package/dist/plan/project.planner.d.ts +59 -0
  29. package/dist/plan/project.planner.js +201 -0
  30. package/dist/project/package-manager.detector.d.ts +12 -0
  31. package/dist/project/package-manager.detector.js +14 -0
  32. package/dist/project/project.inspector.d.ts +38 -0
  33. package/dist/project/project.inspector.js +84 -0
  34. package/dist/project/service.detector.d.ts +45 -0
  35. package/dist/project/service.detector.js +106 -0
  36. package/dist/project/source.scanner.d.ts +16 -0
  37. package/dist/project/source.scanner.js +134 -0
  38. package/dist/run/command.runner.d.ts +15 -0
  39. package/dist/run/command.runner.js +20 -0
  40. package/dist/run/init.orchestrator.d.ts +29 -0
  41. package/dist/run/init.orchestrator.js +222 -0
  42. package/dist/run/new.orchestrator.d.ts +21 -0
  43. package/dist/run/new.orchestrator.js +88 -0
  44. package/dist/templates/prisma7/prisma7.templates.d.ts +34 -0
  45. package/dist/templates/prisma7/prisma7.templates.js +397 -0
  46. package/dist/templates/template.registry.d.ts +156 -0
  47. package/dist/templates/template.registry.js +17 -0
  48. package/dist/wizard/answer.resolver.d.ts +49 -0
  49. package/dist/wizard/answer.resolver.js +106 -0
  50. package/dist/wizard/readline.prompter.d.ts +24 -0
  51. package/dist/wizard/readline.prompter.js +76 -0
  52. package/dist/wizard/wizard.questions.d.ts +114 -0
  53. package/dist/wizard/wizard.questions.js +261 -0
  54. package/package.json +28 -3
@@ -0,0 +1,88 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
3
+ import path from "node:path";
4
+ import { detectPackageManager } from "../project/package-manager.detector.js";
5
+ import { ProjectRefusedError } from "../project/project.inspector.js";
6
+ import { resolveAnswers } from "../wizard/answer.resolver.js";
7
+ import { NEW_QUESTIONS } from "../wizard/wizard.questions.js";
8
+ import { InstallFailedError, runInit, } from "./init.orchestrator.js";
9
+ /**
10
+ * `aventara new <name>` (R2, D1): the pinned `nest new`, then **exactly**
11
+ * `aventara init`'s pipeline over the fresh project, with the wizard's answers
12
+ * carried in — so every `new` run exercises the anchored edit, the merges and
13
+ * the templates `init` uses.
14
+ *
15
+ * Nest authors its own scaffold (D1): the CLI never writes Nest's files. Its
16
+ * output is pinned by digest (B24), so a `@nestjs/cli` that writes something
17
+ * else stops the run before anything of Aventara's is written over it.
18
+ */
19
+ /** D1: the `nest new` this CLI was measured against (B1, B24). */
20
+ export const NEST_CLI = "@nestjs/cli@12.0.8";
21
+ /** B24: `src/app.module.ts` as `@nestjs/cli` 12.0.8 writes it — the anchored edit's file. */
22
+ export const NEST_APP_MODULE_SHA256 = "fe98c4cd2552342272494652287c02e4ef58db3072fcb18513ea831c60f85778";
23
+ /** The command that runs a package from the registry once, per package manager (B11, B21). */
24
+ function oneOff(packageManager) {
25
+ return packageManager === "pnpm" ? ["pnpm", ["dlx"]] : ["npx", ["-y"]];
26
+ }
27
+ export async function runNew(command, io) {
28
+ const answers = await resolveAnswers(NEW_QUESTIONS, {
29
+ given: command.given,
30
+ yes: command.yes,
31
+ prompter: io.prompter,
32
+ context: {
33
+ catalog: io.catalog,
34
+ detectedPackageManager: detectPackageManager({
35
+ userAgent: io.env.npm_config_user_agent,
36
+ }),
37
+ },
38
+ });
39
+ const { name, orm, db, packageManager } = answers;
40
+ if (name === undefined ||
41
+ orm === undefined ||
42
+ db === undefined ||
43
+ packageManager === undefined) {
44
+ throw new Error("the new questions settled without every answer");
45
+ }
46
+ const directory = path.join(io.cwd, name);
47
+ if (existsSync(directory) && readdirSync(directory).length > 0) {
48
+ throw new ProjectRefusedError(`aventara new creates ${directory}, which already exists and is not empty; choose another name, or run aventara init inside an existing project`);
49
+ }
50
+ // stdout piped by the runner (B25): on a terminal, nest new asks the module
51
+ // system, with no flag to answer it; piped, it asks nothing and writes ESM.
52
+ const [runner, prefix] = oneOff(packageManager);
53
+ const args = [
54
+ ...prefix,
55
+ NEST_CLI,
56
+ "new",
57
+ name,
58
+ "--package-manager",
59
+ packageManager,
60
+ "--skip-install",
61
+ "--no-observe",
62
+ ...(command.skipGit ? ["--skip-git"] : []),
63
+ ];
64
+ io.stdout(`aventara: running ${runner} ${args.join(" ")}…\n`);
65
+ const created = await io.runner(runner, args, { cwd: io.cwd });
66
+ if (created.code !== 0) {
67
+ throw new InstallFailedError(`\`${runner} ${args.join(" ")}\` exited with code ${created.code}, and nothing of Aventara's was written; fix what it reports and run aventara new again:\n${created.stderr.trimEnd()}`);
68
+ }
69
+ const appModule = path.join(directory, "src", "app.module.ts");
70
+ const digest = existsSync(appModule)
71
+ ? createHash("sha256").update(readFileSync(appModule)).digest("hex")
72
+ : "(absent)";
73
+ if (digest !== NEST_APP_MODULE_SHA256) {
74
+ throw new InstallFailedError(`nest new's output changed: ${appModule} is not what ${NEST_CLI} writes (sha256 ${digest}), so nothing of Aventara's was written over it; run aventara init inside ${directory} to add Aventara to it`);
75
+ }
76
+ return runInit({
77
+ command: "init",
78
+ given: {
79
+ orm: `${orm.orm.family}@${orm.orm.major}`,
80
+ db,
81
+ packageManager,
82
+ },
83
+ skipInstall: command.skipInstall,
84
+ skipGit: false,
85
+ // A fresh project: there is nothing of the developer's to confirm over.
86
+ yes: true,
87
+ }, { ...io, directory, firstSteps: [`Enter the project: cd ${name}`] });
88
+ }
@@ -0,0 +1,34 @@
1
+ import type { PackageManifest } from "../../apply/manifest.merger.js";
2
+ import type { ClientShape, ExportReference } from "../../project/service.detector.js";
3
+ import { type OrmProjectFacts, type OrmScaffold, type ReuseInput, type ScaffoldInput } from "../template.registry.js";
4
+ declare function scaffold(input: ScaffoldInput): OrmScaffold;
5
+ declare function reuse(input: ReuseInput): OrmScaffold;
6
+ declare function readProject(directory: string, manifest: PackageManifest): OrmProjectFacts;
7
+ declare function clientShape(facts: OrmProjectFacts): ClientShape;
8
+ export declare const PRISMA7_TEMPLATES: {
9
+ readonly providers: {
10
+ readonly sqlite: {
11
+ readonly driverClass: "PrismaBetterSqlite3";
12
+ readonly driverOptions: "{ url }";
13
+ readonly databaseUrl: "file:./dev.db";
14
+ readonly gitignore: readonly ["/dev.db*"];
15
+ readonly nativeBuilds: readonly ["@prisma/engines", "better-sqlite3", "prisma"];
16
+ };
17
+ readonly postgresql: {
18
+ readonly driverClass: "PrismaPg";
19
+ readonly driverOptions: "{ connectionString: url }";
20
+ readonly databaseUrl: "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public";
21
+ readonly gitignore: readonly [];
22
+ readonly nativeBuilds: readonly ["@prisma/engines", "prisma"];
23
+ };
24
+ };
25
+ readonly scaffold: typeof scaffold;
26
+ readonly readProject: typeof readProject;
27
+ readonly clientShape: typeof clientShape;
28
+ readonly newService: {
29
+ readonly service: ExportReference;
30
+ readonly module: ExportReference;
31
+ };
32
+ readonly reuse: typeof reuse;
33
+ };
34
+ export {};
@@ -0,0 +1,397 @@
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
+ /**
6
+ * The `prisma7` entry's templates (D7): Prisma 7 knowledge as text the CLI
7
+ * writes and reads, keyed by the providers `@aventara/prisma7-adapter`
8
+ * declares. Every value that is an adapter fact — the ORM package, the driver
9
+ * per provider, the generator, the generate bin, the version floor — is read
10
+ * from the catalog entry handed in, not restated here.
11
+ *
12
+ * The texts follow the Nest scaffold's formatting (prettier, `singleQuote`,
13
+ * `trailingComma: "all"`, B1), so the project's own `format` script leaves them
14
+ * as they are.
15
+ */
16
+ /** The generated client goes under `src`, where `nest build` compiles it (B6). */
17
+ const CLIENT_OUTPUT = "src/generated/prisma";
18
+ const SCHEMA = "prisma/schema.prisma";
19
+ /** B5: the Prisma CLI loads either name, and `prisma7.config.ts` wins when both exist. */
20
+ const CONFIG_FILES = ["prisma7.config.ts", "prisma.config.ts"];
21
+ const DRIVER_PREFIX = "@prisma/adapter-";
22
+ const NEW_SERVICE = {
23
+ file: "src/prisma.service.ts",
24
+ exportName: "PrismaService",
25
+ };
26
+ const NEW_MODULE = {
27
+ file: "src/prisma.service.ts",
28
+ exportName: "PrismaModule",
29
+ };
30
+ const PROVIDERS = {
31
+ sqlite: {
32
+ driverClass: "PrismaBetterSqlite3",
33
+ driverOptions: "{ url }",
34
+ databaseUrl: "file:./dev.db",
35
+ gitignore: ["/dev.db*"],
36
+ // B9: pnpm 12 refuses these install scripts until they are allowed.
37
+ nativeBuilds: ["@prisma/engines", "better-sqlite3", "prisma"],
38
+ },
39
+ postgresql: {
40
+ driverClass: "PrismaPg",
41
+ driverOptions: "{ connectionString: url }",
42
+ // `prisma init`'s own placeholder (B5); printed in the next steps, never asked (Q16-8).
43
+ databaseUrl: "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public",
44
+ gitignore: [],
45
+ nativeBuilds: ["@prisma/engines", "prisma"],
46
+ },
47
+ };
48
+ /** A relative import specifier from `from` (a project file) to `to` (a project path, no extension). */
49
+ function specifier(from, to, js) {
50
+ const relative = path.posix.relative(path.posix.dirname(from), to);
51
+ return `${relative.startsWith(".") ? relative : `./${relative}`}${js}`;
52
+ }
53
+ function schema(input) {
54
+ return [
55
+ "generator client {",
56
+ ` provider = "${input.entry.generators[0]}"`,
57
+ ` output = "../${CLIENT_OUTPUT}"`,
58
+ ...(input.moduleKind === "cjs" ? [' moduleFormat = "cjs"'] : []),
59
+ "}",
60
+ "",
61
+ "datasource db {",
62
+ ` provider = "${input.provider}"`,
63
+ "}",
64
+ "",
65
+ // R12: Prisma's `--with-model` starter (B5).
66
+ "model User {",
67
+ " id Int @id @default(autoincrement())",
68
+ " email String @unique",
69
+ " name String?",
70
+ "}",
71
+ "",
72
+ ].join("\n");
73
+ }
74
+ /** `prisma.config.ts` (Q16-19). Prisma does not load `.env` itself (B5). */
75
+ function prismaConfig() {
76
+ return [
77
+ "import { existsSync } from 'node:fs';",
78
+ "import { defineConfig } from 'prisma/config';",
79
+ "",
80
+ "// Prisma reads DATABASE_URL from the environment and does not load .env itself.",
81
+ "if (existsSync('.env')) {",
82
+ " process.loadEnvFile('.env');",
83
+ "}",
84
+ "",
85
+ "export default defineConfig({",
86
+ ` schema: '${SCHEMA}',`,
87
+ " migrations: {",
88
+ " path: 'prisma/migrations',",
89
+ " },",
90
+ " datasource: {",
91
+ " url: process.env.DATABASE_URL,",
92
+ " },",
93
+ "});",
94
+ "",
95
+ ].join("\n");
96
+ }
97
+ /** The developer-owned service (P2: `extends PrismaClient`, so `client: prisma`). */
98
+ function prismaService(input) {
99
+ const provider = PROVIDERS[input.provider];
100
+ const driver = input.entry.drivers[input.provider];
101
+ return [
102
+ "import { Injectable, Module, type OnModuleDestroy } from '@nestjs/common';",
103
+ `import { ${provider.driverClass} } from '${driver}';`,
104
+ `import { PrismaClient } from '${specifier(NEW_SERVICE.file, `${input.clientOutput}/client`, input.js)}';`,
105
+ "",
106
+ "@Injectable()",
107
+ "export class PrismaService extends PrismaClient implements OnModuleDestroy {",
108
+ " constructor() {",
109
+ " const url = process.env.DATABASE_URL;",
110
+ " if (!url) {",
111
+ " // Thrown while Nest builds the application, before it listens.",
112
+ " throw new Error(",
113
+ " 'DATABASE_URL is not set: put it in .env (the start scripts load it) or in the environment.',",
114
+ " );",
115
+ " }",
116
+ ` super({ adapter: new ${provider.driverClass}(${provider.driverOptions}) });`,
117
+ " }",
118
+ "",
119
+ " async onModuleDestroy(): Promise<void> {",
120
+ " await this.$disconnect();",
121
+ " }",
122
+ "}",
123
+ "",
124
+ "@Module({ providers: [PrismaService], exports: [PrismaService] })",
125
+ "export class PrismaModule {}",
126
+ "",
127
+ ].join("\n");
128
+ }
129
+ /** `src/aventara.config.ts` (R8, Q16-17): the framework's configuration. */
130
+ function aventaraConfig(input) {
131
+ const file = "src/aventara.config.ts";
132
+ const service = input.service.exportName;
133
+ return [
134
+ `import { createPrismaAdapter } from '${input.entry.adapterPackage}';`,
135
+ `import { discovery } from '${specifier(file, input.artifact.replace(/\.ts$/, ""), input.js)}';`,
136
+ `import type { ${service} } from '${specifier(file, input.service.file.replace(/\.ts$/, ""), input.js)}';`,
137
+ "",
138
+ "/**",
139
+ " * Aventara's configuration: the entrypoint, and the restrictions and",
140
+ " * pipelines you add. AventaraModule (app.module.ts) builds the framework from",
141
+ " * it; `discovery` is regenerated by the aventara:prepare script.",
142
+ " */",
143
+ `export async function aventaraConfig(prisma: ${service}) {`,
144
+ " return {",
145
+ ` entrypoint: '${SCAFFOLD_ENTRYPOINT}',`,
146
+ " adapter: await createPrismaAdapter({",
147
+ input.service.clientMember === undefined
148
+ ? " client: prisma,"
149
+ : ` client: prisma.${input.service.clientMember},`,
150
+ " discovery,",
151
+ ` provider: '${input.provider}',`,
152
+ ` driver: '${input.entry.drivers[input.provider]}',`,
153
+ " }),",
154
+ " } as const;",
155
+ "}",
156
+ "",
157
+ ].join("\n");
158
+ }
159
+ function prepare(input) {
160
+ return [
161
+ input.entry.bin,
162
+ `--schema ${input.schema}`,
163
+ `--client ${input.clientOutput}`,
164
+ `--provider ${input.provider}`,
165
+ `--driver ${input.entry.drivers[input.provider]}`,
166
+ `--out ${input.artifact}`,
167
+ ].join(" ");
168
+ }
169
+ function appModule(service, module, js) {
170
+ const from = "src/app.module.ts";
171
+ const sameFile = service.file === module.file;
172
+ return {
173
+ imports: [
174
+ `import { aventaraConfig } from '${specifier(from, "src/aventara.config", js)}';`,
175
+ ...(sameFile
176
+ ? [
177
+ `import { ${module.exportName}, ${service.exportName} } from '${specifier(from, service.file.replace(/\.ts$/, ""), js)}';`,
178
+ ]
179
+ : [
180
+ `import { ${module.exportName} } from '${specifier(from, module.file.replace(/\.ts$/, ""), js)}';`,
181
+ `import { ${service.exportName} } from '${specifier(from, service.file.replace(/\.ts$/, ""), js)}';`,
182
+ ]),
183
+ ],
184
+ entries: [
185
+ `${module.exportName},`,
186
+ [
187
+ "AventaraModule.forRootAsync({",
188
+ ` imports: [${module.exportName}],`,
189
+ ` inject: [${service.exportName}],`,
190
+ ` useFactory: async (prisma: ${service.exportName}) => ({`,
191
+ " config: await aventaraConfig(prisma),",
192
+ " }),",
193
+ "}),",
194
+ ].join("\n"),
195
+ ],
196
+ };
197
+ }
198
+ function scaffold(input) {
199
+ const js = input.moduleKind === "esm" ? ".js" : "";
200
+ const floor = lowestVersionOf(input.entry.range);
201
+ const provider = PROVIDERS[input.provider];
202
+ const driver = input.entry.drivers[input.provider];
203
+ const run = input.packageManager === "pnpm" ? "pnpm exec" : "npx";
204
+ const artifact = "src/generated/aventara/discovery.artifact.ts";
205
+ return {
206
+ files: [
207
+ { path: SCHEMA, content: schema(input) },
208
+ { path: "prisma.config.ts", content: prismaConfig() },
209
+ {
210
+ path: NEW_SERVICE.file,
211
+ content: prismaService({ ...input, clientOutput: CLIENT_OUTPUT, js }),
212
+ },
213
+ {
214
+ path: "src/aventara.config.ts",
215
+ content: aventaraConfig({
216
+ ...input,
217
+ service: NEW_SERVICE,
218
+ artifact,
219
+ js,
220
+ }),
221
+ },
222
+ ],
223
+ env: { DATABASE_URL: provider.databaseUrl },
224
+ gitignore: ["/src/generated/", ...provider.gitignore],
225
+ // D1, D9: exactly the version floor the adapter declares; never bare `prisma`.
226
+ dependencies: { [input.entry.orm.package]: floor, [driver]: floor },
227
+ devDependencies: { prisma: floor },
228
+ prepare: prepare({
229
+ ...input,
230
+ schema: SCHEMA,
231
+ clientOutput: CLIENT_OUTPUT,
232
+ artifact,
233
+ }),
234
+ appModule: appModule(NEW_SERVICE, NEW_MODULE, js),
235
+ nativeBuilds: provider.nativeBuilds,
236
+ nextSteps: [
237
+ ...(input.provider === "postgresql"
238
+ ? ["Set DATABASE_URL in .env to your PostgreSQL database."]
239
+ : []),
240
+ `Create the database tables: ${run} prisma db push`,
241
+ ],
242
+ };
243
+ }
244
+ function reuse(input) {
245
+ const js = input.moduleKind === "esm" ? ".js" : "";
246
+ const { facts } = input;
247
+ return {
248
+ files: [
249
+ ...(input.writeService
250
+ ? [
251
+ {
252
+ path: NEW_SERVICE.file,
253
+ content: prismaService({
254
+ entry: input.entry,
255
+ provider: facts.provider,
256
+ clientOutput: facts.clientOutput,
257
+ js,
258
+ }),
259
+ },
260
+ ]
261
+ : []),
262
+ {
263
+ path: "src/aventara.config.ts",
264
+ content: aventaraConfig({
265
+ entry: input.entry,
266
+ provider: facts.provider,
267
+ service: input.service,
268
+ artifact: input.artifact,
269
+ js,
270
+ }),
271
+ },
272
+ ],
273
+ // R6: the developer's Prisma files, `.env` key and versions are theirs.
274
+ env: {},
275
+ gitignore: [`/${path.posix.dirname(input.artifact)}/`],
276
+ dependencies: {},
277
+ devDependencies: {},
278
+ prepare: prepare({
279
+ entry: input.entry,
280
+ schema: facts.schema,
281
+ clientOutput: facts.clientOutput,
282
+ provider: facts.provider,
283
+ artifact: input.artifact,
284
+ }),
285
+ appModule: appModule(input.service, input.module, js),
286
+ nativeBuilds: [],
287
+ nextSteps: [],
288
+ };
289
+ }
290
+ /* ------------------------------------------------------------------ *
291
+ * Reading an existing Prisma 7 setup (R6) *
292
+ * ------------------------------------------------------------------ */
293
+ function schemaLocation(directory, manifest) {
294
+ for (const config of CONFIG_FILES) {
295
+ const file = path.join(directory, config);
296
+ if (existsSync(file)) {
297
+ const declared = /\bschema\s*:\s*['"]([^'"]+)['"]/.exec(readFileSync(file, "utf8"))?.[1];
298
+ if (declared !== undefined) {
299
+ return path.posix.normalize(declared);
300
+ }
301
+ }
302
+ }
303
+ const prisma = manifest.prisma;
304
+ return typeof prisma?.schema === "string"
305
+ ? path.posix.normalize(prisma.schema)
306
+ : SCHEMA;
307
+ }
308
+ /** The `.prisma` files of a schema path: the file, or every file in a schema folder. */
309
+ function schemaFiles(directory, schemaPath) {
310
+ const full = path.join(directory, schemaPath);
311
+ if (!existsSync(full)) {
312
+ return [];
313
+ }
314
+ if (!statSync(full).isDirectory()) {
315
+ return [schemaPath];
316
+ }
317
+ return readdirSync(full, { recursive: true, withFileTypes: true })
318
+ .filter((entry) => entry.isFile() && entry.name.endsWith(".prisma"))
319
+ .map((entry) => path
320
+ .relative(directory, path.join(entry.parentPath, entry.name))
321
+ .split(path.sep)
322
+ .join("/"))
323
+ .sort();
324
+ }
325
+ function blocks(text, kind) {
326
+ return [
327
+ ...text.matchAll(new RegExp(`^\\s*${kind}\\s+\\w+\\s*\\{([^}]*)\\}`, "gm")),
328
+ ].map((match) => match[1]);
329
+ }
330
+ function setting(block, name) {
331
+ return new RegExp(`^\\s*${name}\\s*=\\s*"([^"]*)"`, "m").exec(block)?.[1];
332
+ }
333
+ function readProject(directory, manifest) {
334
+ const schemaPath = schemaLocation(directory, manifest);
335
+ let provider;
336
+ let generator;
337
+ let clientOutput;
338
+ for (const file of schemaFiles(directory, schemaPath)) {
339
+ const text = readFileSync(path.join(directory, file), "utf8").replace(/\/\/.*$/gm, "");
340
+ provider ??= blocks(text, "datasource")
341
+ .map((block) => setting(block, "provider"))
342
+ .find((value) => value !== undefined);
343
+ for (const block of blocks(text, "generator")) {
344
+ const declared = setting(block, "provider");
345
+ const output = setting(block, "output");
346
+ // The client generator, not a second generator (a zod or docs one) beside it.
347
+ if (generator === undefined || /^prisma-client/.test(declared ?? "")) {
348
+ generator = declared;
349
+ clientOutput =
350
+ output === undefined
351
+ ? undefined
352
+ : path.posix.normalize(path.posix.join(path.posix.dirname(file), output));
353
+ }
354
+ }
355
+ }
356
+ const dependencies = [
357
+ ...Object.keys(manifest.dependencies ?? {}),
358
+ ...Object.keys(manifest.devDependencies ?? {}),
359
+ ];
360
+ return {
361
+ schema: schemaPath,
362
+ provider,
363
+ generator,
364
+ clientOutput,
365
+ drivers: dependencies.filter((name) => name.startsWith(DRIVER_PREFIX)),
366
+ };
367
+ }
368
+ function clientShape(facts) {
369
+ return {
370
+ clientClass: "PrismaClient",
371
+ importsClient: (imported, file) => {
372
+ if (!imported.startsWith(".")) {
373
+ return imported === "@prisma/client";
374
+ }
375
+ if (facts.clientOutput === undefined) {
376
+ return false;
377
+ }
378
+ const resolved = path.posix
379
+ .join(path.posix.dirname(file), imported)
380
+ .replace(/\.(js|ts)$/, "");
381
+ return (resolved === facts.clientOutput ||
382
+ resolved === `${facts.clientOutput}/client` ||
383
+ resolved === `${facts.clientOutput}/index`);
384
+ },
385
+ skip: facts.clientOutput === undefined
386
+ ? ["src/generated"]
387
+ : ["src/generated", facts.clientOutput],
388
+ };
389
+ }
390
+ export const PRISMA7_TEMPLATES = {
391
+ providers: PROVIDERS,
392
+ scaffold,
393
+ readProject,
394
+ clientShape,
395
+ newService: { service: NEW_SERVICE, module: NEW_MODULE },
396
+ reuse,
397
+ };
@@ -0,0 +1,156 @@
1
+ import type { PackageManifest } from "../apply/manifest.merger.js";
2
+ import type { CatalogEntry, EntryId, ProviderOf } from "../catalog/catalog-entry.interface.js";
3
+ import type { ClientShape, ExportReference } from "../project/service.detector.js";
4
+ import type { PackageManager } from "../wizard/wizard.questions.js";
5
+ /**
6
+ * D7 — what the CLI writes for an ORM lives here as text, keyed by catalog id;
7
+ * the adapter itself runs only through its bin.
8
+ *
9
+ * The registry is **total** in both directions, and both are checked before a
10
+ * project is ever scaffolded:
11
+ * - at compile time, over the generated catalog's literal type: an entry with
12
+ * no template set, or a template set missing a provider its adapter declares,
13
+ * does not type-check;
14
+ * - at build time, by the catalog generator: a templates directory with no
15
+ * adapter, or an adapter with no templates directory, fails the build.
16
+ */
17
+ /** What changes with the database provider an entry offers. */
18
+ export type ProviderTemplate = {
19
+ /** The driver adapter's class, constructed in the written ORM service. */
20
+ readonly driverClass: string;
21
+ /** The driver class's constructor argument, given the connection string as `url`. */
22
+ readonly driverOptions: string;
23
+ /** The `DATABASE_URL` line's value; a placeholder where it cannot be known (Q16-8). */
24
+ readonly databaseUrl: string;
25
+ /** `.gitignore` lines this provider's files need (a SQLite database file). */
26
+ readonly gitignore: readonly string[];
27
+ /** Packages whose install scripts pnpm must be allowed to run (B9). */
28
+ readonly nativeBuilds: readonly string[];
29
+ };
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";
36
+ /** What a template set is asked to scaffold. */
37
+ export type ScaffoldInput = {
38
+ readonly entry: CatalogEntry;
39
+ /** One of `entry.providers`. */
40
+ readonly provider: string;
41
+ readonly moduleKind: ModuleKind;
42
+ readonly packageManager: PackageManager;
43
+ };
44
+ /**
45
+ * What an ORM's templates contribute to a project. Everything here is the
46
+ * ORM's; what is Nest's (the start scripts, the anchored `app.module.ts` edit,
47
+ * `AventaraModule`'s import) and what is Aventara's (its packages, the
48
+ * `aventara:prepare` and `postinstall` script names) is the planner's.
49
+ */
50
+ export type OrmScaffold = {
51
+ /** New, developer-owned files (§4.3). */
52
+ readonly files: readonly {
53
+ readonly path: string;
54
+ readonly content: string;
55
+ }[];
56
+ /** `.env` entries. */
57
+ readonly env: Readonly<Record<string, string>>;
58
+ readonly gitignore: readonly string[];
59
+ readonly dependencies: Readonly<Record<string, string>>;
60
+ readonly devDependencies: Readonly<Record<string, string>>;
61
+ /** The command `aventara:prepare` runs (D7: the adapter only through its bin). */
62
+ readonly prepare: string;
63
+ /** What the anchored `app.module.ts` edit adds: import lines, and `imports:` entries. */
64
+ readonly appModule: {
65
+ readonly imports: readonly string[];
66
+ readonly entries: readonly string[];
67
+ };
68
+ readonly nativeBuilds: readonly string[];
69
+ /** The ORM's next steps, after the install and before the server starts (R11). */
70
+ readonly nextSteps: readonly string[];
71
+ };
72
+ /**
73
+ * What a project's existing ORM setup states, read by the entry's templates
74
+ * (R6) — the ORM's own file layout is template knowledge (D7); judging it
75
+ * against the catalog is the matcher's (`catalog.matcher.ts`).
76
+ */
77
+ export type OrmProjectFacts = {
78
+ /** The schema the generate step reads, relative to the project root. */
79
+ readonly schema: string;
80
+ readonly provider: string | undefined;
81
+ readonly generator: string | undefined;
82
+ /** The generated client's directory, relative to the project root. */
83
+ readonly clientOutput: string | undefined;
84
+ /** The driver-adapter packages the project depends on. */
85
+ readonly drivers: readonly string[];
86
+ };
87
+ /** Reusing a project's ORM setup (R6): its schema, its output, its service. */
88
+ export type ReuseInput = {
89
+ readonly entry: CatalogEntry;
90
+ readonly facts: OrmProjectFacts & {
91
+ readonly provider: string;
92
+ readonly clientOutput: string;
93
+ };
94
+ readonly moduleKind: ModuleKind;
95
+ readonly packageManager: PackageManager;
96
+ /** The developer's service, or the template's new one when it is to be written. */
97
+ readonly service: ExportReference & {
98
+ readonly clientMember?: string;
99
+ };
100
+ readonly module: ExportReference;
101
+ /** Whether `service` and `module` are the template's new file, to be written. */
102
+ readonly writeService: boolean;
103
+ /** Where the discovery artifact goes, relative to the project root. */
104
+ readonly artifact: string;
105
+ };
106
+ export type TemplateSet<I extends EntryId> = {
107
+ readonly providers: {
108
+ readonly [P in ProviderOf<I>]: ProviderTemplate;
109
+ };
110
+ /** A project with no ORM: everything §4.3 lists. */
111
+ readonly scaffold: (input: ScaffoldInput) => OrmScaffold;
112
+ /** Reads an existing setup's facts; `undefined` when there is none to read. */
113
+ readonly readProject: (directory: string, manifest: PackageManifest) => OrmProjectFacts;
114
+ /** How the developer's service is recognised in their source. */
115
+ readonly clientShape: (facts: OrmProjectFacts) => ClientShape;
116
+ /** The service init writes when the project has none: its file and names. */
117
+ readonly newService: {
118
+ readonly service: ExportReference;
119
+ readonly module: ExportReference;
120
+ };
121
+ /** A project with the ORM already set up: no ORM file written or edited (R6). */
122
+ readonly reuse: (input: ReuseInput) => OrmScaffold;
123
+ };
124
+ export type TemplateRegistry = {
125
+ readonly [I in EntryId]: TemplateSet<I>;
126
+ };
127
+ export declare const TEMPLATE_REGISTRY: {
128
+ readonly prisma7: {
129
+ readonly providers: {
130
+ readonly sqlite: {
131
+ readonly driverClass: "PrismaBetterSqlite3";
132
+ readonly driverOptions: "{ url }";
133
+ readonly databaseUrl: "file:./dev.db";
134
+ readonly gitignore: readonly ["/dev.db*"];
135
+ readonly nativeBuilds: readonly ["@prisma/engines", "better-sqlite3", "prisma"];
136
+ };
137
+ readonly postgresql: {
138
+ readonly driverClass: "PrismaPg";
139
+ readonly driverOptions: "{ connectionString: url }";
140
+ readonly databaseUrl: "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public";
141
+ readonly gitignore: readonly [];
142
+ readonly nativeBuilds: readonly ["@prisma/engines", "prisma"];
143
+ };
144
+ };
145
+ readonly scaffold: (input: ScaffoldInput) => OrmScaffold;
146
+ readonly readProject: (directory: string, manifest: PackageManifest) => OrmProjectFacts;
147
+ readonly clientShape: (facts: OrmProjectFacts) => ClientShape;
148
+ readonly newService: {
149
+ readonly service: ExportReference;
150
+ readonly module: ExportReference;
151
+ };
152
+ readonly reuse: (input: ReuseInput) => OrmScaffold;
153
+ };
154
+ };
155
+ /** The template set for `entry` — every catalog entry has one (checked at build). */
156
+ export declare function templatesFor(entry: CatalogEntry): TemplateSet<EntryId>;