@aventara/cli 0.1.0-pilot.1 → 0.1.0-pilot.3
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 +12 -5
- package/dist/apply/app-module.anchor.d.ts +5 -11
- package/dist/apply/app-module.anchor.js +0 -23
- package/dist/apply/conflict.confirmer.d.ts +0 -9
- package/dist/apply/conflict.confirmer.js +0 -15
- package/dist/apply/e2e-spec.anchor.js +0 -17
- package/dist/apply/main-cors.anchor.d.ts +50 -0
- package/dist/apply/main-cors.anchor.js +60 -0
- package/dist/apply/manifest.merger.d.ts +17 -13
- package/dist/apply/manifest.merger.js +8 -18
- package/dist/aventara.bin.js +0 -10
- package/dist/catalog/adapter.catalog.generated.d.ts +1 -0
- package/dist/catalog/adapter.catalog.generated.js +1 -4
- package/dist/catalog/catalog-entry.interface.d.ts +21 -13
- package/dist/catalog/catalog.matcher.d.ts +7 -13
- package/dist/catalog/catalog.matcher.js +0 -3
- package/dist/catalog/range.reader.d.ts +4 -10
- package/dist/catalog/range.reader.js +0 -14
- package/dist/cli.d.ts +2 -3
- package/dist/cli.js +5 -8
- package/dist/command/command.parser.d.ts +11 -9
- package/dist/command/command.parser.js +24 -18
- package/dist/node-version.guard.js +0 -12
- package/dist/plan/project.planner.d.ts +7 -13
- package/dist/plan/project.planner.js +44 -35
- package/dist/project/package-manager.detector.d.ts +0 -7
- package/dist/project/package-manager.detector.js +0 -7
- package/dist/project/project.inspector.d.ts +3 -9
- package/dist/project/project.inspector.js +0 -10
- package/dist/project/service.detector.d.ts +5 -10
- package/dist/project/service.detector.js +0 -2
- package/dist/project/source.scanner.js +0 -13
- package/dist/run/command.runner.d.ts +4 -4
- package/dist/run/init.orchestrator.d.ts +4 -6
- package/dist/run/init.orchestrator.js +3 -16
- package/dist/run/new.orchestrator.d.ts +8 -9
- package/dist/run/new.orchestrator.js +10 -18
- package/dist/run/scaffold.committer.d.ts +28 -0
- package/dist/run/scaffold.committer.js +53 -0
- package/dist/templates/docs.links.d.ts +8 -0
- package/dist/templates/docs.links.js +2 -0
- package/dist/templates/prisma7/prisma7.templates.js +0 -27
- package/dist/templates/template.registry.d.ts +13 -17
- package/dist/templates/template.registry.js +0 -5
- package/dist/wizard/answer.resolver.d.ts +5 -7
- package/dist/wizard/answer.resolver.js +0 -10
- package/dist/wizard/readline.prompter.d.ts +5 -7
- package/dist/wizard/readline.prompter.js +0 -10
- package/dist/wizard/wizard.questions.d.ts +28 -22
- package/dist/wizard/wizard.questions.js +21 -49
- package/package.json +2 -2
|
@@ -9,7 +9,6 @@ import { scanServices } from "../project/service.detector.js";
|
|
|
9
9
|
import { templatesFor, } from "../templates/template.registry.js";
|
|
10
10
|
import { resolveAnswers } from "../wizard/answer.resolver.js";
|
|
11
11
|
import { INIT_QUESTIONS, REUSE_QUESTIONS, } from "../wizard/wizard.questions.js";
|
|
12
|
-
/** The install step failed after the files were written. Reported, exit 1. */
|
|
13
12
|
export class InstallFailedError extends Error {
|
|
14
13
|
name = "InstallFailedError";
|
|
15
14
|
}
|
|
@@ -49,7 +48,6 @@ export async function runInit(command, io) {
|
|
|
49
48
|
prompter: io.prompter,
|
|
50
49
|
});
|
|
51
50
|
const writes = plan.conflicts.length === 0 ? plan.keeping : plan.replacing;
|
|
52
|
-
// The only file I/O (§4.1 `apply`).
|
|
53
51
|
for (const write of writes) {
|
|
54
52
|
const target = path.join(io.directory, write.path);
|
|
55
53
|
mkdirSync(path.dirname(target), { recursive: true });
|
|
@@ -76,7 +74,6 @@ export async function runInit(command, io) {
|
|
|
76
74
|
io.stdout(`Next:\n${[...(io.firstSteps ?? []), ...plan.nextSteps].map((step, index) => ` ${index + 1}. ${step}`).join("\n")}\n`);
|
|
77
75
|
return 0;
|
|
78
76
|
}
|
|
79
|
-
/** S6 — no ORM: the ORM and database are asked, and the entry's templates write it all. */
|
|
80
77
|
async function prepareFresh(command, io, inspection, detectedPackageManager) {
|
|
81
78
|
const answers = await resolveAnswers(INIT_QUESTIONS, {
|
|
82
79
|
given: command.given,
|
|
@@ -101,7 +98,6 @@ async function prepareFresh(command, io, inspection, detectedPackageManager) {
|
|
|
101
98
|
warnings: [],
|
|
102
99
|
};
|
|
103
100
|
}
|
|
104
|
-
/** The installed version of `name`, looked up the way Node resolves it: upward from the project. */
|
|
105
101
|
function installedVersion(directory, name) {
|
|
106
102
|
let current = directory;
|
|
107
103
|
for (;;) {
|
|
@@ -117,11 +113,6 @@ function installedVersion(directory, name) {
|
|
|
117
113
|
current = parent;
|
|
118
114
|
}
|
|
119
115
|
}
|
|
120
|
-
/**
|
|
121
|
-
* S7 — the ORM is already set up (R6, R7): the catalog entry is matched against
|
|
122
|
-
* what is installed, the setup against what the entry declares, and the
|
|
123
|
-
* developer's own service is found or asked for. Nothing of theirs is written.
|
|
124
|
-
*/
|
|
125
116
|
async function prepareReuse(command, io, inspection, detectedPackageManager) {
|
|
126
117
|
const dependencies = [
|
|
127
118
|
...Object.keys(inspection.manifest.dependencies ?? {}),
|
|
@@ -146,14 +137,11 @@ async function prepareReuse(command, io, inspection, detectedPackageManager) {
|
|
|
146
137
|
if (facts.clientOutput === undefined) {
|
|
147
138
|
throw new ProjectRefusedError(`this project's ${facts.generator} generator block in ${facts.schema} has no output, and the generate step reads the client from it; give it one, such as ../src/generated/${entry.orm.family}`);
|
|
148
139
|
}
|
|
149
|
-
// A flag naming the ORM or database must name what the project has.
|
|
150
140
|
for (const [flag, given, found] of [
|
|
151
|
-
["--orm", command.given.orm,
|
|
141
|
+
["--orm", command.given.orm, entry.alias],
|
|
152
142
|
["--db", command.given.db, provider],
|
|
153
143
|
]) {
|
|
154
|
-
if (given !== undefined &&
|
|
155
|
-
given.trim().toLowerCase() !== found &&
|
|
156
|
-
!(flag === "--orm" && given.trim().toLowerCase() === entry.orm.family)) {
|
|
144
|
+
if (given !== undefined && given.trim().toLowerCase() !== found) {
|
|
157
145
|
throw new ProjectRefusedError(`${flag} ${given} does not match this project, which already uses ${found}`);
|
|
158
146
|
}
|
|
159
147
|
}
|
|
@@ -181,7 +169,6 @@ async function prepareReuse(command, io, inspection, detectedPackageManager) {
|
|
|
181
169
|
packageManager === undefined) {
|
|
182
170
|
throw new Error("the reuse questions settled without a service, module and package manager");
|
|
183
171
|
}
|
|
184
|
-
// Q16-21: beside the client's output, unless that output contains it (the adapter refuses that).
|
|
185
172
|
const preferred = "src/generated/aventara/discovery.artifact.ts";
|
|
186
173
|
const artifact = `${preferred}/`.startsWith(`${facts.clientOutput}/`)
|
|
187
174
|
? "src/aventara/discovery.artifact.ts"
|
|
@@ -190,7 +177,7 @@ async function prepareReuse(command, io, inspection, detectedPackageManager) {
|
|
|
190
177
|
...(facts.clientOutput.startsWith("src/")
|
|
191
178
|
? []
|
|
192
179
|
: [
|
|
193
|
-
`the ${entry.orm.label} client is generated into ${facts.clientOutput}, outside src/, which nest build does not compile
|
|
180
|
+
`the ${entry.orm.label} client is generated into ${facts.clientOutput}, outside src/, which nest build does not compile; your build is your own, and init does not change it`,
|
|
194
181
|
]),
|
|
195
182
|
...(service.clientMember === undefined
|
|
196
183
|
? []
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import type { ScaffoldCommand } from "../command/command.parser.js";
|
|
2
2
|
import { type InitIo } from "./init.orchestrator.js";
|
|
3
3
|
/**
|
|
4
|
-
* `aventara new <name
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* the
|
|
4
|
+
* `aventara new <name>`: the pinned `nest new`, then **exactly** `aventara init`'s
|
|
5
|
+
* pipeline over the fresh project, with the wizard's answers carried in — so every
|
|
6
|
+
* `new` run exercises the anchored edit, the merges and the templates `init` uses
|
|
7
|
+
* — and, after the install, the project's first commit (`scaffold.committer.ts`,
|
|
8
|
+
* pilot.3).
|
|
8
9
|
*
|
|
9
|
-
* Nest authors its own scaffold
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* Nest authors its own scaffold: the CLI never writes Nest's files. Its output is
|
|
11
|
+
* pinned by digest, so a `@nestjs/cli` that writes something else stops the run
|
|
12
|
+
* before anything of Aventara's is written over it.
|
|
12
13
|
*/
|
|
13
|
-
/** D1: the `nest new` this CLI was measured against (B1, B24). */
|
|
14
14
|
export declare const NEST_CLI = "@nestjs/cli@12.0.8";
|
|
15
|
-
/** B24: `src/app.module.ts` as `@nestjs/cli` 12.0.8 writes it — the anchored edit's file. */
|
|
16
15
|
export declare const NEST_APP_MODULE_SHA256 = "fe98c4cd2552342272494652287c02e4ef58db3072fcb18513ea831c60f85778";
|
|
17
16
|
export type NewIo = Omit<InitIo, "directory"> & {
|
|
18
17
|
/** Where `new` creates the project directory. */
|
|
@@ -6,21 +6,9 @@ import { ProjectRefusedError } from "../project/project.inspector.js";
|
|
|
6
6
|
import { resolveAnswers } from "../wizard/answer.resolver.js";
|
|
7
7
|
import { NEW_QUESTIONS } from "../wizard/wizard.questions.js";
|
|
8
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). */
|
|
9
|
+
import { commitScaffold, gitSurroundings } from "./scaffold.committer.js";
|
|
20
10
|
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
11
|
export const NEST_APP_MODULE_SHA256 = "fe98c4cd2552342272494652287c02e4ef58db3072fcb18513ea831c60f85778";
|
|
23
|
-
/** The command that runs a package from the registry once, per package manager (B11, B21). */
|
|
24
12
|
function oneOff(packageManager) {
|
|
25
13
|
return packageManager === "pnpm" ? ["pnpm", ["dlx"]] : ["npx", ["-y"]];
|
|
26
14
|
}
|
|
@@ -47,8 +35,9 @@ export async function runNew(command, io) {
|
|
|
47
35
|
if (existsSync(directory) && readdirSync(directory).length > 0) {
|
|
48
36
|
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
37
|
}
|
|
50
|
-
|
|
51
|
-
|
|
38
|
+
const surroundings = command.skipGit
|
|
39
|
+
? undefined
|
|
40
|
+
: await gitSurroundings(io.runner, io.cwd);
|
|
52
41
|
const [runner, prefix] = oneOff(packageManager);
|
|
53
42
|
const args = [
|
|
54
43
|
...prefix,
|
|
@@ -73,16 +62,19 @@ export async function runNew(command, io) {
|
|
|
73
62
|
if (digest !== NEST_APP_MODULE_SHA256) {
|
|
74
63
|
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
64
|
}
|
|
76
|
-
|
|
65
|
+
const initialized = await runInit({
|
|
77
66
|
command: "init",
|
|
78
67
|
given: {
|
|
79
|
-
orm:
|
|
68
|
+
orm: orm.alias,
|
|
80
69
|
db,
|
|
81
70
|
packageManager,
|
|
82
71
|
},
|
|
83
72
|
skipInstall: command.skipInstall,
|
|
84
73
|
skipGit: false,
|
|
85
|
-
// A fresh project: there is nothing of the developer's to confirm over.
|
|
86
74
|
yes: true,
|
|
87
75
|
}, { ...io, directory, firstSteps: [`Enter the project: cd ${name}`] });
|
|
76
|
+
if (initialized === 0 && surroundings !== undefined) {
|
|
77
|
+
io.stdout(`aventara: ${await commitScaffold(io.runner, directory, surroundings, io.cwd)}.\n`);
|
|
78
|
+
}
|
|
79
|
+
return initialized;
|
|
88
80
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { CommandRunner } from "./command.runner.js";
|
|
2
|
+
/**
|
|
3
|
+
* pilot.3 — `aventara new` ends with one commit of the whole scaffold, the
|
|
4
|
+
* install's lockfile included, under one fixed message. `.env` is not in it: the
|
|
5
|
+
* scaffold's `.gitignore` lists it (Nest's own does too). `init` never commits:
|
|
6
|
+
* the project is the developer's.
|
|
7
|
+
*
|
|
8
|
+
* Never a failure: where a commit cannot or should not be made, one line says why
|
|
9
|
+
* and the run still succeeds —
|
|
10
|
+
* - git is not installed;
|
|
11
|
+
* - the directory `new` ran in is inside a git work tree already (a monorepo): the
|
|
12
|
+
* project belongs to that repository, and gets no commit of its own;
|
|
13
|
+
* - git has no `user.name` and `user.email` to commit as. `--skip-git` (passed to
|
|
14
|
+
* `nest new`, which then initializes no repository) makes no commit and says
|
|
15
|
+
* nothing.
|
|
16
|
+
*
|
|
17
|
+
* git runs through the one process seam, `CommandRunner`.
|
|
18
|
+
*/
|
|
19
|
+
export declare const INITIAL_COMMIT_MESSAGE = "Initial Aventara Scaffold";
|
|
20
|
+
/** Where `new` runs, as git sees it — read BEFORE `nest new` initializes a repository. */
|
|
21
|
+
export type GitSurroundings = "no-git" | "inside-work-tree" | "outside";
|
|
22
|
+
/** Whether git is installed, and whether `directory` lies in a work tree. */
|
|
23
|
+
export declare function gitSurroundings(runner: CommandRunner, directory: string): Promise<GitSurroundings>;
|
|
24
|
+
/**
|
|
25
|
+
* Commits the project in `directory` as its first commit, or says why not: the
|
|
26
|
+
* line to print, without the `aventara: ` prefix or the final period.
|
|
27
|
+
*/
|
|
28
|
+
export declare function commitScaffold(runner: CommandRunner, directory: string, surroundings: GitSurroundings, enclosing: string): Promise<string>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
export const INITIAL_COMMIT_MESSAGE = "Initial Aventara Scaffold";
|
|
3
|
+
function isMissingCommand(error) {
|
|
4
|
+
return (error instanceof Error &&
|
|
5
|
+
error.code === "ENOENT");
|
|
6
|
+
}
|
|
7
|
+
export async function gitSurroundings(runner, directory) {
|
|
8
|
+
try {
|
|
9
|
+
const answered = await runner("git", ["rev-parse", "--is-inside-work-tree"], { cwd: directory });
|
|
10
|
+
return answered.code === 0 && answered.stdout.trim() === "true"
|
|
11
|
+
? "inside-work-tree"
|
|
12
|
+
: "outside";
|
|
13
|
+
}
|
|
14
|
+
catch (error) {
|
|
15
|
+
if (isMissingCommand(error)) {
|
|
16
|
+
return "no-git";
|
|
17
|
+
}
|
|
18
|
+
throw error;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export async function commitScaffold(runner, directory, surroundings, enclosing) {
|
|
22
|
+
if (surroundings === "no-git") {
|
|
23
|
+
return "no initial commit: git is not installed";
|
|
24
|
+
}
|
|
25
|
+
if (surroundings === "inside-work-tree") {
|
|
26
|
+
return `no initial commit: ${enclosing} is inside a git repository already; commit the project there`;
|
|
27
|
+
}
|
|
28
|
+
const git = (args) => runner("git", args, { cwd: directory });
|
|
29
|
+
for (const key of ["user.name", "user.email"]) {
|
|
30
|
+
const set = await git(["config", key]);
|
|
31
|
+
if (set.code !== 0 || set.stdout.trim() === "") {
|
|
32
|
+
return "no initial commit: git has no user.name and user.email set; set them, then commit the project yourself";
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const top = await git(["rev-parse", "--show-toplevel"]);
|
|
36
|
+
if (top.code !== 0 ||
|
|
37
|
+
realpathSync(top.stdout.trim()) !== realpathSync(directory)) {
|
|
38
|
+
const initialized = await git(["init", "-q"]);
|
|
39
|
+
if (initialized.code !== 0) {
|
|
40
|
+
return `no initial commit: \`git init\` exited with code ${initialized.code}: ${initialized.stderr.trim()}`;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
for (const args of [
|
|
44
|
+
["add", "-A"],
|
|
45
|
+
["commit", "-q", "-m", INITIAL_COMMIT_MESSAGE],
|
|
46
|
+
]) {
|
|
47
|
+
const ran = await git(args);
|
|
48
|
+
if (ran.code !== 0) {
|
|
49
|
+
return `no initial commit: \`git ${args.join(" ")}\` exited with code ${ran.code}: ${ran.stderr.trim()}`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return `committed the scaffold: "${INITIAL_COMMIT_MESSAGE}"`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where the public documentation lives: the one base every link the scaffold
|
|
3
|
+
* writes into a project starts from. A placeholder until the docs domain is
|
|
4
|
+
* settled; changing it changes only comments in the files the CLI writes.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DOCS_BASE_URL = "https://<docs-domain>";
|
|
7
|
+
/** The documentation's rate-limiting section, linked from the scaffold's `main.ts`. */
|
|
8
|
+
export declare const RATE_LIMITING_DOCS = "https://<docs-domain>/docs/limits-and-safety#rate-limiting";
|
|
@@ -2,21 +2,8 @@ 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
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
5
|
const CLIENT_OUTPUT = "src/generated/prisma";
|
|
18
6
|
const SCHEMA = "prisma/schema.prisma";
|
|
19
|
-
/** B5: the Prisma CLI loads either name, and `prisma7.config.ts` wins when both exist. */
|
|
20
7
|
const CONFIG_FILES = ["prisma7.config.ts", "prisma.config.ts"];
|
|
21
8
|
const DRIVER_PREFIX = "@prisma/adapter-";
|
|
22
9
|
const NEW_SERVICE = {
|
|
@@ -33,19 +20,16 @@ const PROVIDERS = {
|
|
|
33
20
|
driverOptions: "{ url }",
|
|
34
21
|
databaseUrl: "file:./dev.db",
|
|
35
22
|
gitignore: ["/dev.db*"],
|
|
36
|
-
// B9: pnpm 12 refuses these install scripts until they are allowed.
|
|
37
23
|
nativeBuilds: ["@prisma/engines", "better-sqlite3", "prisma"],
|
|
38
24
|
},
|
|
39
25
|
postgresql: {
|
|
40
26
|
driverClass: "PrismaPg",
|
|
41
27
|
driverOptions: "{ connectionString: url }",
|
|
42
|
-
// `prisma init`'s own placeholder (B5); printed in the next steps, never asked (Q16-8).
|
|
43
28
|
databaseUrl: "postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public",
|
|
44
29
|
gitignore: [],
|
|
45
30
|
nativeBuilds: ["@prisma/engines", "prisma"],
|
|
46
31
|
},
|
|
47
32
|
};
|
|
48
|
-
/** A relative import specifier from `from` (a project file) to `to` (a project path, no extension). */
|
|
49
33
|
function specifier(from, to, js) {
|
|
50
34
|
const relative = path.posix.relative(path.posix.dirname(from), to);
|
|
51
35
|
return `${relative.startsWith(".") ? relative : `./${relative}`}${js}`;
|
|
@@ -62,7 +46,6 @@ function schema(input) {
|
|
|
62
46
|
` provider = "${input.provider}"`,
|
|
63
47
|
"}",
|
|
64
48
|
"",
|
|
65
|
-
// R12: Prisma's `--with-model` starter (B5).
|
|
66
49
|
"model User {",
|
|
67
50
|
" id Int @id @default(autoincrement())",
|
|
68
51
|
" email String @unique",
|
|
@@ -71,7 +54,6 @@ function schema(input) {
|
|
|
71
54
|
"",
|
|
72
55
|
].join("\n");
|
|
73
56
|
}
|
|
74
|
-
/** `prisma.config.ts` (Q16-19). Prisma does not load `.env` itself (B5). */
|
|
75
57
|
function prismaConfig() {
|
|
76
58
|
return [
|
|
77
59
|
"import { existsSync } from 'node:fs';",
|
|
@@ -94,7 +76,6 @@ function prismaConfig() {
|
|
|
94
76
|
"",
|
|
95
77
|
].join("\n");
|
|
96
78
|
}
|
|
97
|
-
/** The developer-owned service (P2: `extends PrismaClient`, so `client: prisma`). */
|
|
98
79
|
function prismaService(input) {
|
|
99
80
|
const provider = PROVIDERS[input.provider];
|
|
100
81
|
const driver = input.entry.drivers[input.provider];
|
|
@@ -126,7 +107,6 @@ function prismaService(input) {
|
|
|
126
107
|
"",
|
|
127
108
|
].join("\n");
|
|
128
109
|
}
|
|
129
|
-
/** `src/aventara.config.ts` (R8, Q16-17): the framework's configuration. */
|
|
130
110
|
function aventaraConfig(input) {
|
|
131
111
|
const file = "src/aventara.config.ts";
|
|
132
112
|
const service = input.service.exportName;
|
|
@@ -222,7 +202,6 @@ function scaffold(input) {
|
|
|
222
202
|
],
|
|
223
203
|
env: { DATABASE_URL: provider.databaseUrl },
|
|
224
204
|
gitignore: ["/src/generated/", ...provider.gitignore],
|
|
225
|
-
// D1, D9: exactly the version floor the adapter declares; never bare `prisma`.
|
|
226
205
|
dependencies: { [input.entry.orm.package]: floor, [driver]: floor },
|
|
227
206
|
devDependencies: { prisma: floor },
|
|
228
207
|
prepare: prepare({
|
|
@@ -270,7 +249,6 @@ function reuse(input) {
|
|
|
270
249
|
}),
|
|
271
250
|
},
|
|
272
251
|
],
|
|
273
|
-
// R6: the developer's Prisma files, `.env` key and versions are theirs.
|
|
274
252
|
env: {},
|
|
275
253
|
gitignore: [`/${path.posix.dirname(input.artifact)}/`],
|
|
276
254
|
dependencies: {},
|
|
@@ -287,9 +265,6 @@ function reuse(input) {
|
|
|
287
265
|
nextSteps: [],
|
|
288
266
|
};
|
|
289
267
|
}
|
|
290
|
-
/* ------------------------------------------------------------------ *
|
|
291
|
-
* Reading an existing Prisma 7 setup (R6) *
|
|
292
|
-
* ------------------------------------------------------------------ */
|
|
293
268
|
function schemaLocation(directory, manifest) {
|
|
294
269
|
for (const config of CONFIG_FILES) {
|
|
295
270
|
const file = path.join(directory, config);
|
|
@@ -305,7 +280,6 @@ function schemaLocation(directory, manifest) {
|
|
|
305
280
|
? path.posix.normalize(prisma.schema)
|
|
306
281
|
: SCHEMA;
|
|
307
282
|
}
|
|
308
|
-
/** The `.prisma` files of a schema path: the file, or every file in a schema folder. */
|
|
309
283
|
function schemaFiles(directory, schemaPath) {
|
|
310
284
|
const full = path.join(directory, schemaPath);
|
|
311
285
|
if (!existsSync(full)) {
|
|
@@ -343,7 +317,6 @@ function readProject(directory, manifest) {
|
|
|
343
317
|
for (const block of blocks(text, "generator")) {
|
|
344
318
|
const declared = setting(block, "provider");
|
|
345
319
|
const output = setting(block, "output");
|
|
346
|
-
// The client generator, not a second generator (a zod or docs one) beside it.
|
|
347
320
|
if (generator === undefined || /^prisma-client/.test(declared ?? "")) {
|
|
348
321
|
generator = declared;
|
|
349
322
|
clientOutput =
|
|
@@ -3,14 +3,11 @@ import type { CatalogEntry, EntryId, ProviderOf } from "../catalog/catalog-entry
|
|
|
3
3
|
import type { ClientShape, ExportReference } from "../project/service.detector.js";
|
|
4
4
|
import type { PackageManager } from "../wizard/wizard.questions.js";
|
|
5
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
6
|
* The registry is **total** in both directions, and both are checked before a
|
|
10
7
|
* project is ever scaffolded:
|
|
11
|
-
* - at compile time, over the generated catalog's literal type: an entry with
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* - at compile time, over the generated catalog's literal type: an entry with no
|
|
9
|
+
* template set, or a template set missing a provider its adapter declares, does
|
|
10
|
+
* not type-check;
|
|
14
11
|
* - at build time, by the catalog generator: a templates directory with no
|
|
15
12
|
* adapter, or an adapter with no templates directory, fails the build.
|
|
16
13
|
*/
|
|
@@ -20,11 +17,11 @@ export type ProviderTemplate = {
|
|
|
20
17
|
readonly driverClass: string;
|
|
21
18
|
/** The driver class's constructor argument, given the connection string as `url`. */
|
|
22
19
|
readonly driverOptions: string;
|
|
23
|
-
/** The `DATABASE_URL` line's value; a placeholder where it cannot be known
|
|
20
|
+
/** The `DATABASE_URL` line's value; a placeholder where it cannot be known. */
|
|
24
21
|
readonly databaseUrl: string;
|
|
25
22
|
/** `.gitignore` lines this provider's files need (a SQLite database file). */
|
|
26
23
|
readonly gitignore: readonly string[];
|
|
27
|
-
/** Packages whose install scripts pnpm must be allowed to run
|
|
24
|
+
/** Packages whose install scripts pnpm must be allowed to run. */
|
|
28
25
|
readonly nativeBuilds: readonly string[];
|
|
29
26
|
};
|
|
30
27
|
export type ModuleKind = "esm" | "cjs";
|
|
@@ -48,7 +45,7 @@ export type ScaffoldInput = {
|
|
|
48
45
|
* `aventara:prepare` and `postinstall` script names) is the planner's.
|
|
49
46
|
*/
|
|
50
47
|
export type OrmScaffold = {
|
|
51
|
-
/** New, developer-owned files
|
|
48
|
+
/** New, developer-owned files. */
|
|
52
49
|
readonly files: readonly {
|
|
53
50
|
readonly path: string;
|
|
54
51
|
readonly content: string;
|
|
@@ -58,7 +55,7 @@ export type OrmScaffold = {
|
|
|
58
55
|
readonly gitignore: readonly string[];
|
|
59
56
|
readonly dependencies: Readonly<Record<string, string>>;
|
|
60
57
|
readonly devDependencies: Readonly<Record<string, string>>;
|
|
61
|
-
/** The command `aventara:prepare` runs
|
|
58
|
+
/** The command `aventara:prepare` runs. */
|
|
62
59
|
readonly prepare: string;
|
|
63
60
|
/** What the anchored `app.module.ts` edit adds: import lines, and `imports:` entries. */
|
|
64
61
|
readonly appModule: {
|
|
@@ -66,13 +63,13 @@ export type OrmScaffold = {
|
|
|
66
63
|
readonly entries: readonly string[];
|
|
67
64
|
};
|
|
68
65
|
readonly nativeBuilds: readonly string[];
|
|
69
|
-
/** The ORM's next steps, after the install and before the server starts
|
|
66
|
+
/** The ORM's next steps, after the install and before the server starts. */
|
|
70
67
|
readonly nextSteps: readonly string[];
|
|
71
68
|
};
|
|
72
69
|
/**
|
|
73
|
-
* What a project's existing ORM setup states, read by the entry's templates
|
|
74
|
-
*
|
|
75
|
-
*
|
|
70
|
+
* What a project's existing ORM setup states, read by the entry's templates — the
|
|
71
|
+
* ORM's own file layout is template knowledge; judging it against the catalog is
|
|
72
|
+
* the matcher's (`catalog.matcher.ts`).
|
|
76
73
|
*/
|
|
77
74
|
export type OrmProjectFacts = {
|
|
78
75
|
/** The schema the generate step reads, relative to the project root. */
|
|
@@ -84,7 +81,7 @@ export type OrmProjectFacts = {
|
|
|
84
81
|
/** The driver-adapter packages the project depends on. */
|
|
85
82
|
readonly drivers: readonly string[];
|
|
86
83
|
};
|
|
87
|
-
/** Reusing a project's ORM setup
|
|
84
|
+
/** Reusing a project's ORM setup: its schema, its output, its service. */
|
|
88
85
|
export type ReuseInput = {
|
|
89
86
|
readonly entry: CatalogEntry;
|
|
90
87
|
readonly facts: OrmProjectFacts & {
|
|
@@ -107,7 +104,6 @@ export type TemplateSet<I extends EntryId> = {
|
|
|
107
104
|
readonly providers: {
|
|
108
105
|
readonly [P in ProviderOf<I>]: ProviderTemplate;
|
|
109
106
|
};
|
|
110
|
-
/** A project with no ORM: everything §4.3 lists. */
|
|
111
107
|
readonly scaffold: (input: ScaffoldInput) => OrmScaffold;
|
|
112
108
|
/** Reads an existing setup's facts; `undefined` when there is none to read. */
|
|
113
109
|
readonly readProject: (directory: string, manifest: PackageManifest) => OrmProjectFacts;
|
|
@@ -118,7 +114,7 @@ export type TemplateSet<I extends EntryId> = {
|
|
|
118
114
|
readonly service: ExportReference;
|
|
119
115
|
readonly module: ExportReference;
|
|
120
116
|
};
|
|
121
|
-
/** A project with the ORM already set up: no ORM file written or edited
|
|
117
|
+
/** A project with the ORM already set up: no ORM file written or edited. */
|
|
122
118
|
readonly reuse: (input: ReuseInput) => OrmScaffold;
|
|
123
119
|
};
|
|
124
120
|
export type TemplateRegistry = {
|
|
@@ -1,13 +1,8 @@
|
|
|
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
2
|
export const SCAFFOLD_ENTRYPOINT = "/api";
|
|
7
3
|
export const TEMPLATE_REGISTRY = {
|
|
8
4
|
prisma7: PRISMA7_TEMPLATES,
|
|
9
5
|
};
|
|
10
|
-
/** The template set for `entry` — every catalog entry has one (checked at build). */
|
|
11
6
|
export function templatesFor(entry) {
|
|
12
7
|
const set = TEMPLATE_REGISTRY[entry.id];
|
|
13
8
|
if (set === undefined) {
|
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import type { AnyWizardQuestion, QuestionId, WizardAnswers, WizardContext } from "./wizard.questions.js";
|
|
2
2
|
/**
|
|
3
|
-
* R5 — how each question gets its answer, in this order:
|
|
4
|
-
*
|
|
5
3
|
* 1. **its flag**, validated by the question (a bad value is refused, never
|
|
6
4
|
* re-asked);
|
|
7
|
-
* 2. **the only choice there is
|
|
5
|
+
* 2. **the only choice there is**: shown on a terminal with no keypress;
|
|
8
6
|
* 3. **`--yes`**: the question's default, when it has one;
|
|
9
7
|
* 4. **the prompt**, when someone can answer (stdin is a terminal);
|
|
10
8
|
* 5. otherwise the question is **unanswered**, and once every question has been
|
|
11
9
|
* tried the run refuses in one sentence naming exactly those flags.
|
|
12
10
|
*
|
|
13
11
|
* `--yes` comes before the prompt because it means "accept each unanswered
|
|
14
|
-
* question's default"
|
|
15
|
-
*
|
|
12
|
+
* question's default", on a terminal as much as anywhere; a question with no
|
|
13
|
+
* default (`new`'s `<name>`) is still asked on a terminal.
|
|
16
14
|
*/
|
|
17
|
-
/** The seam to a person
|
|
15
|
+
/** The seam to a person: `node:readline/promises` on a terminal, a script in specs. */
|
|
18
16
|
export type Prompter = {
|
|
19
17
|
/** Whether someone can answer: stdin is a terminal. */
|
|
20
18
|
readonly interactive: boolean;
|
|
21
19
|
/** Asks once; resolves with what was typed, `""` for Enter alone. */
|
|
22
20
|
readonly ask: (request: PromptRequest) => Promise<string>;
|
|
23
|
-
/** Says one line without asking
|
|
21
|
+
/** Says one line without asking. */
|
|
24
22
|
readonly say: (line: string) => void;
|
|
25
23
|
};
|
|
26
24
|
export type PromptRequest = {
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { WizardAnswerError } from "./wizard.questions.js";
|
|
2
|
-
/** Nobody can answer and some questions are unanswered. A refusal: one sentence. */
|
|
3
2
|
export class UnansweredQuestionsError extends Error {
|
|
4
3
|
name = "UnansweredQuestionsError";
|
|
5
4
|
}
|
|
6
|
-
/** `--db <sqlite|postgresql>`, or `<name>` for the positional. */
|
|
7
5
|
function flagSpelling(question, context) {
|
|
8
6
|
if (!question.flag.startsWith("--")) {
|
|
9
7
|
return question.flag;
|
|
@@ -18,7 +16,6 @@ function listed(items) {
|
|
|
18
16
|
? items.join("")
|
|
19
17
|
: `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
20
18
|
}
|
|
21
|
-
/** The refusal sentence: the unanswered flags, exactly, and `--yes` for those with a default. */
|
|
22
19
|
function unansweredSentence(missing, yes) {
|
|
23
20
|
const defaulted = missing.filter((item) => item.defaulted);
|
|
24
21
|
const yesClause = !yes && defaulted.length > 0
|
|
@@ -51,12 +48,6 @@ async function prompt(question, context, prompter) {
|
|
|
51
48
|
}
|
|
52
49
|
}
|
|
53
50
|
}
|
|
54
|
-
/**
|
|
55
|
-
* Settles `questions` in order, each later question seeing the earlier answers.
|
|
56
|
-
*
|
|
57
|
-
* @throws WizardAnswerError when a flag's value is not an answer.
|
|
58
|
-
* @throws UnansweredQuestionsError when nobody can answer and something is unanswered.
|
|
59
|
-
*/
|
|
60
51
|
export async function resolveAnswers(questions, sources) {
|
|
61
52
|
const answered = {};
|
|
62
53
|
const missing = [];
|
|
@@ -65,7 +56,6 @@ export async function resolveAnswers(questions, sources) {
|
|
|
65
56
|
...sources.context,
|
|
66
57
|
answered: answered,
|
|
67
58
|
};
|
|
68
|
-
// A question that depends on one left unanswered cannot be asked yet.
|
|
69
59
|
if (question.after !== undefined &&
|
|
70
60
|
answered[question.after] === undefined) {
|
|
71
61
|
missing.push({ spelling: question.flag, defaulted: false });
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import type { Readable, Writable } from "node:stream";
|
|
2
2
|
import type { Prompter } from "./answer.resolver.js";
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* answers into
|
|
9
|
-
* a line nobody is waiting for. Measured: with `readline/promises`' `question`,
|
|
10
|
-
* three answers piped into `script(1)` reached the first question only.
|
|
4
|
+
* Lines are **queued** as they arrive and handed to questions in order. A terminal
|
|
5
|
+
* delivers whatever was typed ahead of a question — and a script piping answers
|
|
6
|
+
* into a pseudo-terminal delivers them all at once — and readline drops a line
|
|
7
|
+
* nobody is waiting for. Measured: with `readline/promises`' `question`, three
|
|
8
|
+
* answers piped into `script(1)` reached the first question only.
|
|
11
9
|
*/
|
|
12
10
|
/** Input ended while a question was open: the person closed the wizard. A refusal. */
|
|
13
11
|
export declare class WizardCancelledError extends Error {
|
|
@@ -1,14 +1,4 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
|
-
/**
|
|
3
|
-
* P6, B26 — the {@link Prompter} over `node:readline`: no prompt dependency.
|
|
4
|
-
*
|
|
5
|
-
* Lines are **queued** as they arrive and handed to questions in order. A
|
|
6
|
-
* terminal delivers whatever was typed ahead of a question — and a script piping
|
|
7
|
-
* answers into a pseudo-terminal delivers them all at once — and readline drops
|
|
8
|
-
* a line nobody is waiting for. Measured: with `readline/promises`' `question`,
|
|
9
|
-
* three answers piped into `script(1)` reached the first question only.
|
|
10
|
-
*/
|
|
11
|
-
/** Input ended while a question was open: the person closed the wizard. A refusal. */
|
|
12
2
|
export class WizardCancelledError extends Error {
|
|
13
3
|
name = "WizardCancelledError";
|
|
14
4
|
}
|