@aventara/cli 0.0.0-stage → 0.1.0-pilot.0
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/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +88 -2
- package/dist/apply/app-module.anchor.d.ts +30 -0
- package/dist/apply/app-module.anchor.js +185 -0
- package/dist/apply/conflict.confirmer.d.ts +24 -0
- package/dist/apply/conflict.confirmer.js +39 -0
- package/dist/apply/manifest.merger.d.ts +47 -0
- package/dist/apply/manifest.merger.js +139 -0
- package/dist/aventara.bin.d.ts +2 -0
- package/dist/aventara.bin.js +15 -0
- package/dist/catalog/adapter.catalog.generated.d.ts +19 -0
- package/dist/catalog/adapter.catalog.generated.js +35 -0
- package/dist/catalog/catalog-entry.interface.d.ts +46 -0
- package/dist/catalog/catalog-entry.interface.js +1 -0
- package/dist/catalog/catalog.matcher.d.ts +46 -0
- package/dist/catalog/catalog.matcher.js +83 -0
- package/dist/catalog/range.reader.d.ts +17 -0
- package/dist/catalog/range.reader.js +77 -0
- package/dist/cli.d.ts +30 -0
- package/dist/cli.js +96 -0
- package/dist/command/command.parser.d.ts +31 -0
- package/dist/command/command.parser.js +138 -0
- package/dist/node-version.guard.d.ts +8 -0
- package/dist/node-version.guard.js +59 -0
- package/dist/plan/project.planner.d.ts +52 -0
- package/dist/plan/project.planner.js +178 -0
- package/dist/project/package-manager.detector.d.ts +12 -0
- package/dist/project/package-manager.detector.js +14 -0
- package/dist/project/project.inspector.d.ts +38 -0
- package/dist/project/project.inspector.js +84 -0
- package/dist/project/service.detector.d.ts +45 -0
- package/dist/project/service.detector.js +106 -0
- package/dist/project/source.scanner.d.ts +16 -0
- package/dist/project/source.scanner.js +134 -0
- package/dist/run/command.runner.d.ts +15 -0
- package/dist/run/command.runner.js +20 -0
- package/dist/run/init.orchestrator.d.ts +29 -0
- package/dist/run/init.orchestrator.js +222 -0
- package/dist/run/new.orchestrator.d.ts +21 -0
- package/dist/run/new.orchestrator.js +88 -0
- package/dist/templates/prisma7/prisma7.templates.d.ts +34 -0
- package/dist/templates/prisma7/prisma7.templates.js +396 -0
- package/dist/templates/template.registry.d.ts +151 -0
- package/dist/templates/template.registry.js +12 -0
- package/dist/wizard/answer.resolver.d.ts +49 -0
- package/dist/wizard/answer.resolver.js +106 -0
- package/dist/wizard/readline.prompter.d.ts +24 -0
- package/dist/wizard/readline.prompter.js +76 -0
- package/dist/wizard/wizard.questions.d.ts +114 -0
- package/dist/wizard/wizard.questions.js +261 -0
- package/package.json +28 -3
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A lexical view of TypeScript source — enough to find decorators, classes,
|
|
3
|
+
* imports and brackets **in code**, never inside a comment, a string or a
|
|
4
|
+
* template literal's text — with no compiler: `aventara new` runs before
|
|
5
|
+
* anything (TypeScript included) is installed in the project, and the CLI
|
|
6
|
+
* carries no TypeScript of its own. Shared by the anchored `app.module.ts` edit
|
|
7
|
+
* and the detection of a developer's ORM service.
|
|
8
|
+
*/
|
|
9
|
+
/** For each character: is it code (not inside a comment, string or template text)? */
|
|
10
|
+
export function codeMask(text) {
|
|
11
|
+
const code = new Array(text.length).fill(true);
|
|
12
|
+
/** Open template literals' brace depths: `${` re-enters code until its `}`. */
|
|
13
|
+
const templates = [];
|
|
14
|
+
let braces = 0;
|
|
15
|
+
let index = 0;
|
|
16
|
+
const mark = (from, to) => {
|
|
17
|
+
for (let at = from; at < to && at < text.length; at += 1) {
|
|
18
|
+
code[at] = false;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const templateText = (from) => {
|
|
22
|
+
// Scans template text from `from` until "`" (closes) or "${" (re-enters code).
|
|
23
|
+
let at = from;
|
|
24
|
+
while (at < text.length) {
|
|
25
|
+
if (text[at] === "\\") {
|
|
26
|
+
at += 2;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (text[at] === "`") {
|
|
30
|
+
mark(from, at + 1);
|
|
31
|
+
return at + 1;
|
|
32
|
+
}
|
|
33
|
+
if (text[at] === "$" && text[at + 1] === "{") {
|
|
34
|
+
mark(from, at + 2);
|
|
35
|
+
templates.push(braces);
|
|
36
|
+
braces += 1;
|
|
37
|
+
return at + 2;
|
|
38
|
+
}
|
|
39
|
+
at += 1;
|
|
40
|
+
}
|
|
41
|
+
mark(from, at);
|
|
42
|
+
return at;
|
|
43
|
+
};
|
|
44
|
+
while (index < text.length) {
|
|
45
|
+
const char = text[index];
|
|
46
|
+
const next = text[index + 1];
|
|
47
|
+
if (char === "/" && next === "/") {
|
|
48
|
+
const end = text.indexOf("\n", index);
|
|
49
|
+
const stop = end === -1 ? text.length : end;
|
|
50
|
+
mark(index, stop);
|
|
51
|
+
index = stop;
|
|
52
|
+
}
|
|
53
|
+
else if (char === "/" && next === "*") {
|
|
54
|
+
const end = text.indexOf("*/", index + 2);
|
|
55
|
+
const stop = end === -1 ? text.length : end + 2;
|
|
56
|
+
mark(index, stop);
|
|
57
|
+
index = stop;
|
|
58
|
+
}
|
|
59
|
+
else if (char === "'" || char === '"') {
|
|
60
|
+
let at = index + 1;
|
|
61
|
+
while (at < text.length && text[at] !== char && text[at] !== "\n") {
|
|
62
|
+
at += text[at] === "\\" ? 2 : 1;
|
|
63
|
+
}
|
|
64
|
+
mark(index, at + 1);
|
|
65
|
+
index = at + 1;
|
|
66
|
+
}
|
|
67
|
+
else if (char === "`") {
|
|
68
|
+
index = templateText(index);
|
|
69
|
+
}
|
|
70
|
+
else if (char === "{") {
|
|
71
|
+
braces += 1;
|
|
72
|
+
index += 1;
|
|
73
|
+
}
|
|
74
|
+
else if (char === "}") {
|
|
75
|
+
braces -= 1;
|
|
76
|
+
if (templates.length > 0 && templates[templates.length - 1] === braces) {
|
|
77
|
+
templates.pop();
|
|
78
|
+
code[index] = false;
|
|
79
|
+
index = templateText(index + 1);
|
|
80
|
+
}
|
|
81
|
+
else {
|
|
82
|
+
index += 1;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
index += 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return code;
|
|
90
|
+
}
|
|
91
|
+
/** Every index where `token` starts in code. */
|
|
92
|
+
export function codeIndexes(text, code, token) {
|
|
93
|
+
const found = [];
|
|
94
|
+
for (const match of text.matchAll(token)) {
|
|
95
|
+
if (code[match.index]) {
|
|
96
|
+
found.push(match.index);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return found;
|
|
100
|
+
}
|
|
101
|
+
export const OPENERS = {
|
|
102
|
+
"(": ")",
|
|
103
|
+
"[": "]",
|
|
104
|
+
"{": "}",
|
|
105
|
+
};
|
|
106
|
+
/** The index of the bracket closing the one at `open`, counting code brackets only. */
|
|
107
|
+
export function closing(text, code, open) {
|
|
108
|
+
const stack = [];
|
|
109
|
+
for (let at = open; at < text.length; at += 1) {
|
|
110
|
+
if (!code[at]) {
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
const char = text[at];
|
|
114
|
+
if (OPENERS[char] !== undefined) {
|
|
115
|
+
stack.push(OPENERS[char]);
|
|
116
|
+
}
|
|
117
|
+
else if (char === ")" || char === "]" || char === "}") {
|
|
118
|
+
if (stack.pop() !== char) {
|
|
119
|
+
return -1;
|
|
120
|
+
}
|
|
121
|
+
if (stack.length === 0) {
|
|
122
|
+
return at;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return -1;
|
|
127
|
+
}
|
|
128
|
+
export function nextCode(text, code, from) {
|
|
129
|
+
let at = from;
|
|
130
|
+
while (at < text.length && (!code[at] || /\s/.test(text[at]))) {
|
|
131
|
+
at += 1;
|
|
132
|
+
}
|
|
133
|
+
return at;
|
|
134
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one external process seam (§4.2): a command, its arguments, a working
|
|
3
|
+
* directory. Its stdout is always **piped**, never inherited — `nest new` asks a
|
|
4
|
+
* question it has no flag for when its stdout is a terminal (B25) — and both
|
|
5
|
+
* streams are collected, so a failure can be reported with what it said.
|
|
6
|
+
*/
|
|
7
|
+
export type CommandResult = {
|
|
8
|
+
readonly code: number;
|
|
9
|
+
readonly stdout: string;
|
|
10
|
+
readonly stderr: string;
|
|
11
|
+
};
|
|
12
|
+
export type CommandRunner = (command: string, args: readonly string[], options: {
|
|
13
|
+
readonly cwd: string;
|
|
14
|
+
}) => Promise<CommandResult>;
|
|
15
|
+
export declare const runCommand: CommandRunner;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
export const runCommand = (command, args, options) => new Promise((resolve, reject) => {
|
|
3
|
+
const child = spawn(command, args, {
|
|
4
|
+
cwd: options.cwd,
|
|
5
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
6
|
+
env: process.env,
|
|
7
|
+
});
|
|
8
|
+
let stdout = "";
|
|
9
|
+
let stderr = "";
|
|
10
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
11
|
+
stdout += chunk;
|
|
12
|
+
});
|
|
13
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
14
|
+
stderr += chunk;
|
|
15
|
+
});
|
|
16
|
+
child.once("error", reject);
|
|
17
|
+
child.once("close", (code) => {
|
|
18
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AdapterCatalog } from "../catalog/catalog-entry.interface.js";
|
|
2
|
+
import type { ScaffoldCommand } from "../command/command.parser.js";
|
|
3
|
+
import { type Prompter } from "../wizard/answer.resolver.js";
|
|
4
|
+
import type { CommandRunner } from "./command.runner.js";
|
|
5
|
+
/**
|
|
6
|
+
* `aventara init` in §4.1's order — inspect, ask, plan, confirm, and only then
|
|
7
|
+
* write and install. Every step before the write reads; so a refusal anywhere
|
|
8
|
+
* before it leaves the project exactly as it was. A failure after it (the
|
|
9
|
+
* install) names the command, its exit code and what to re-run; nothing is
|
|
10
|
+
* rolled back.
|
|
11
|
+
*/
|
|
12
|
+
export type InitIo = {
|
|
13
|
+
readonly directory: string;
|
|
14
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
15
|
+
readonly stdout: (text: string) => void;
|
|
16
|
+
readonly stderr: (text: string) => void;
|
|
17
|
+
readonly prompter: Prompter;
|
|
18
|
+
readonly runner: CommandRunner;
|
|
19
|
+
readonly catalog: AdapterCatalog;
|
|
20
|
+
/** This CLI's own version (D1). */
|
|
21
|
+
readonly aventaraVersion: string;
|
|
22
|
+
/** Next steps that come before init's own (`new`'s `cd <name>`). */
|
|
23
|
+
readonly firstSteps?: readonly string[];
|
|
24
|
+
};
|
|
25
|
+
/** The install step failed after the files were written. Reported, exit 1. */
|
|
26
|
+
export declare class InstallFailedError extends Error {
|
|
27
|
+
readonly name = "InstallFailedError";
|
|
28
|
+
}
|
|
29
|
+
export declare function runInit(command: ScaffoldCommand, io: InitIo): Promise<number>;
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { confirmConflicts } from "../apply/conflict.confirmer.js";
|
|
4
|
+
import { matchInstalledOrm, matchOrmSetup, } from "../catalog/catalog.matcher.js";
|
|
5
|
+
import { planInit } from "../plan/project.planner.js";
|
|
6
|
+
import { detectPackageManager } from "../project/package-manager.detector.js";
|
|
7
|
+
import { inspectProject, ProjectRefusedError, } from "../project/project.inspector.js";
|
|
8
|
+
import { scanServices } from "../project/service.detector.js";
|
|
9
|
+
import { templatesFor, } from "../templates/template.registry.js";
|
|
10
|
+
import { resolveAnswers } from "../wizard/answer.resolver.js";
|
|
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
|
+
export class InstallFailedError extends Error {
|
|
14
|
+
name = "InstallFailedError";
|
|
15
|
+
}
|
|
16
|
+
export async function runInit(command, io) {
|
|
17
|
+
const inspection = inspectProject(io.directory, io.catalog);
|
|
18
|
+
const read = (file) => {
|
|
19
|
+
try {
|
|
20
|
+
return readFileSync(path.join(io.directory, file), "utf8");
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
const detectedPackageManager = detectPackageManager({
|
|
27
|
+
lockfile: inspection.lockfile,
|
|
28
|
+
userAgent: io.env.npm_config_user_agent,
|
|
29
|
+
});
|
|
30
|
+
const prepared = inspection.orm.kind === "present"
|
|
31
|
+
? await prepareReuse(command, io, inspection, detectedPackageManager)
|
|
32
|
+
: await prepareFresh(command, io, inspection, detectedPackageManager);
|
|
33
|
+
const { entry, provider, scaffold, packageManager } = prepared;
|
|
34
|
+
const plan = planInit({
|
|
35
|
+
inspection,
|
|
36
|
+
entry,
|
|
37
|
+
scaffold,
|
|
38
|
+
packageManager,
|
|
39
|
+
aventaraVersion: io.aventaraVersion,
|
|
40
|
+
skipInstall: command.skipInstall,
|
|
41
|
+
read,
|
|
42
|
+
...(prepared.wiringByHand === undefined
|
|
43
|
+
? {}
|
|
44
|
+
: { wiringByHand: prepared.wiringByHand }),
|
|
45
|
+
warnings: prepared.warnings,
|
|
46
|
+
});
|
|
47
|
+
await confirmConflicts(plan.conflicts, {
|
|
48
|
+
yes: command.yes,
|
|
49
|
+
prompter: io.prompter,
|
|
50
|
+
});
|
|
51
|
+
const writes = plan.conflicts.length === 0 ? plan.keeping : plan.replacing;
|
|
52
|
+
// The only file I/O (§4.1 `apply`).
|
|
53
|
+
for (const write of writes) {
|
|
54
|
+
const target = path.join(io.directory, write.path);
|
|
55
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
56
|
+
writeFileSync(target, write.content);
|
|
57
|
+
}
|
|
58
|
+
for (const warning of plan.warnings) {
|
|
59
|
+
io.stderr(`aventara: warning: ${warning}.\n`);
|
|
60
|
+
}
|
|
61
|
+
io.stdout(`aventara: initialized ${io.directory} for ${entry.orm.label} ${entry.orm.major} on ${provider}: ${writes
|
|
62
|
+
.map((write) => `${write.change === "create" ? "created" : "updated"} ${write.path}`)
|
|
63
|
+
.join(", ")}.\n`);
|
|
64
|
+
if (plan.instructions !== undefined) {
|
|
65
|
+
io.stdout(`${plan.instructions}\n`);
|
|
66
|
+
}
|
|
67
|
+
if (!command.skipInstall) {
|
|
68
|
+
io.stdout(`aventara: running ${packageManager} install…\n`);
|
|
69
|
+
const installed = await io.runner(packageManager, ["install"], {
|
|
70
|
+
cwd: io.directory,
|
|
71
|
+
});
|
|
72
|
+
if (installed.code !== 0) {
|
|
73
|
+
throw new InstallFailedError(`\`${packageManager} install\` exited with code ${installed.code} after the files were written; fix what it reports and run \`${packageManager} install\` again:\n${installed.stderr.trimEnd()}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
io.stdout(`Next:\n${[...(io.firstSteps ?? []), ...plan.nextSteps].map((step, index) => ` ${index + 1}. ${step}`).join("\n")}\n`);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
/** S6 — no ORM: the ORM and database are asked, and the entry's templates write it all. */
|
|
80
|
+
async function prepareFresh(command, io, inspection, detectedPackageManager) {
|
|
81
|
+
const answers = await resolveAnswers(INIT_QUESTIONS, {
|
|
82
|
+
given: command.given,
|
|
83
|
+
yes: command.yes,
|
|
84
|
+
prompter: io.prompter,
|
|
85
|
+
context: { catalog: io.catalog, detectedPackageManager },
|
|
86
|
+
});
|
|
87
|
+
const { orm: entry, db, packageManager } = answers;
|
|
88
|
+
if (entry === undefined || db === undefined || packageManager === undefined) {
|
|
89
|
+
throw new Error("the init questions settled without an ORM, database and package manager");
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
entry,
|
|
93
|
+
provider: db,
|
|
94
|
+
packageManager,
|
|
95
|
+
scaffold: templatesFor(entry).scaffold({
|
|
96
|
+
entry,
|
|
97
|
+
provider: db,
|
|
98
|
+
moduleKind: inspection.moduleKind,
|
|
99
|
+
packageManager,
|
|
100
|
+
}),
|
|
101
|
+
warnings: [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/** The installed version of `name`, looked up the way Node resolves it: upward from the project. */
|
|
105
|
+
function installedVersion(directory, name) {
|
|
106
|
+
let current = directory;
|
|
107
|
+
for (;;) {
|
|
108
|
+
const manifest = path.join(current, "node_modules", name, "package.json");
|
|
109
|
+
if (existsSync(manifest)) {
|
|
110
|
+
const version = JSON.parse(readFileSync(manifest, "utf8")).version;
|
|
111
|
+
return typeof version === "string" ? version : undefined;
|
|
112
|
+
}
|
|
113
|
+
const parent = path.dirname(current);
|
|
114
|
+
if (parent === current) {
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
current = parent;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
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
|
+
async function prepareReuse(command, io, inspection, detectedPackageManager) {
|
|
126
|
+
const dependencies = [
|
|
127
|
+
...Object.keys(inspection.manifest.dependencies ?? {}),
|
|
128
|
+
...Object.keys(inspection.manifest.devDependencies ?? {}),
|
|
129
|
+
];
|
|
130
|
+
const verdict = matchInstalledOrm(io.catalog, {
|
|
131
|
+
dependencies,
|
|
132
|
+
installedVersion: (name) => installedVersion(io.directory, name),
|
|
133
|
+
installCommand: `${detectedPackageManager} install`,
|
|
134
|
+
});
|
|
135
|
+
if (verdict.kind === "refused") {
|
|
136
|
+
throw new ProjectRefusedError(verdict.sentence);
|
|
137
|
+
}
|
|
138
|
+
const { entry } = verdict;
|
|
139
|
+
const templates = templatesFor(entry);
|
|
140
|
+
const facts = templates.readProject(io.directory, inspection.manifest);
|
|
141
|
+
const problem = matchOrmSetup(entry, facts);
|
|
142
|
+
if (problem !== undefined) {
|
|
143
|
+
throw new ProjectRefusedError(problem);
|
|
144
|
+
}
|
|
145
|
+
const provider = facts.provider;
|
|
146
|
+
if (facts.clientOutput === undefined) {
|
|
147
|
+
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
|
+
}
|
|
149
|
+
// A flag naming the ORM or database must name what the project has.
|
|
150
|
+
for (const [flag, given, found] of [
|
|
151
|
+
["--orm", command.given.orm, `${entry.orm.family}@${entry.orm.major}`],
|
|
152
|
+
["--db", command.given.db, provider],
|
|
153
|
+
]) {
|
|
154
|
+
if (given !== undefined &&
|
|
155
|
+
given.trim().toLowerCase() !== found &&
|
|
156
|
+
!(flag === "--orm" && given.trim().toLowerCase() === entry.orm.family)) {
|
|
157
|
+
throw new ProjectRefusedError(`${flag} ${given} does not match this project, which already uses ${found}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const scan = scanServices(io.directory, templates.clientShape(facts));
|
|
161
|
+
const answers = await resolveAnswers(REUSE_QUESTIONS, {
|
|
162
|
+
given: command.given,
|
|
163
|
+
yes: command.yes,
|
|
164
|
+
prompter: io.prompter,
|
|
165
|
+
context: {
|
|
166
|
+
catalog: io.catalog,
|
|
167
|
+
detectedPackageManager,
|
|
168
|
+
services: {
|
|
169
|
+
scan,
|
|
170
|
+
clientClass: templates.clientShape(facts).clientClass,
|
|
171
|
+
newService: templates.newService.service,
|
|
172
|
+
newModule: templates.newService.module,
|
|
173
|
+
newServiceFileExists: existsSync(path.join(io.directory, templates.newService.service.file)),
|
|
174
|
+
label: entry.orm.label,
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
const { service, module, packageManager } = answers;
|
|
179
|
+
if (service === undefined ||
|
|
180
|
+
module === undefined ||
|
|
181
|
+
packageManager === undefined) {
|
|
182
|
+
throw new Error("the reuse questions settled without a service, module and package manager");
|
|
183
|
+
}
|
|
184
|
+
// Q16-21: beside the client's output, unless that output contains it (the adapter refuses that).
|
|
185
|
+
const preferred = "src/generated/aventara/discovery.artifact.ts";
|
|
186
|
+
const artifact = `${preferred}/`.startsWith(`${facts.clientOutput}/`)
|
|
187
|
+
? "src/aventara/discovery.artifact.ts"
|
|
188
|
+
: preferred;
|
|
189
|
+
const warnings = [
|
|
190
|
+
...(facts.clientOutput.startsWith("src/")
|
|
191
|
+
? []
|
|
192
|
+
: [
|
|
193
|
+
`the ${entry.orm.label} client is generated into ${facts.clientOutput}, outside src/, which nest build does not compile (B6); your build is your own, and init does not change it`,
|
|
194
|
+
]),
|
|
195
|
+
...(service.clientMember === undefined
|
|
196
|
+
? []
|
|
197
|
+
: [
|
|
198
|
+
`${service.file}#${service.exportName} holds its client in .${service.clientMember} instead of extending ${templates.clientShape(facts).clientClass}, so src/aventara.config.ts passes ${service.exportName}.${service.clientMember}`,
|
|
199
|
+
]),
|
|
200
|
+
];
|
|
201
|
+
return {
|
|
202
|
+
entry,
|
|
203
|
+
provider,
|
|
204
|
+
packageManager,
|
|
205
|
+
scaffold: templates.reuse({
|
|
206
|
+
entry,
|
|
207
|
+
facts: { ...facts, provider, clientOutput: facts.clientOutput },
|
|
208
|
+
moduleKind: inspection.moduleKind,
|
|
209
|
+
packageManager,
|
|
210
|
+
service,
|
|
211
|
+
module,
|
|
212
|
+
writeService: service.isNew,
|
|
213
|
+
artifact,
|
|
214
|
+
}),
|
|
215
|
+
...(service.clientMember === undefined
|
|
216
|
+
? {}
|
|
217
|
+
: {
|
|
218
|
+
wiringByHand: `${service.exportName} holds its ${entry.orm.label} client instead of extending it`,
|
|
219
|
+
}),
|
|
220
|
+
warnings,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ScaffoldCommand } from "../command/command.parser.js";
|
|
2
|
+
import { type InitIo } from "./init.orchestrator.js";
|
|
3
|
+
/**
|
|
4
|
+
* `aventara new <name>` (R2, D1): the pinned `nest new`, then **exactly**
|
|
5
|
+
* `aventara init`'s pipeline over the fresh project, with the wizard's answers
|
|
6
|
+
* carried in — so every `new` run exercises the anchored edit, the merges and
|
|
7
|
+
* the templates `init` uses.
|
|
8
|
+
*
|
|
9
|
+
* Nest authors its own scaffold (D1): the CLI never writes Nest's files. Its
|
|
10
|
+
* output is pinned by digest (B24), so a `@nestjs/cli` that writes something
|
|
11
|
+
* else stops the run before anything of Aventara's is written over it.
|
|
12
|
+
*/
|
|
13
|
+
/** D1: the `nest new` this CLI was measured against (B1, B24). */
|
|
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
|
+
export declare const NEST_APP_MODULE_SHA256 = "fe98c4cd2552342272494652287c02e4ef58db3072fcb18513ea831c60f85778";
|
|
17
|
+
export type NewIo = Omit<InitIo, "directory"> & {
|
|
18
|
+
/** Where `new` creates the project directory. */
|
|
19
|
+
readonly cwd: string;
|
|
20
|
+
};
|
|
21
|
+
export declare function runNew(command: ScaffoldCommand, io: NewIo): Promise<number>;
|
|
@@ -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, OrmScaffold, ReuseInput, 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 {};
|