@aventara/cli 0.1.0-pilot.4 → 1.0.0-rc.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.
- package/CHANGELOG.md +345 -0
- package/README.md +55 -42
- package/dist/apply/conflict.confirmer.js +4 -4
- package/dist/cli.js +18 -14
- package/dist/command/command.parser.js +17 -10
- package/dist/plan/project.planner.js +2 -4
- package/dist/project/project.inspector.js +8 -8
- package/dist/run/command.runner.js +2 -0
- package/dist/run/init.orchestrator.js +40 -20
- package/dist/run/new.orchestrator.js +32 -14
- package/dist/run/step.reporter.js +87 -0
- package/dist/templates/docs.links.js +2 -1
- package/dist/templates/prisma7/prisma7.templates.js +26 -5
- package/dist/templates/template.registry.js +1 -1
- package/dist/wizard/answer.resolver.js +5 -5
- package/dist/wizard/readline.prompter.js +3 -3
- package/dist/wizard/wizard.questions.js +9 -9
- package/package.json +4 -3
- package/dist/apply/app-module.anchor.d.ts +0 -24
- package/dist/apply/conflict.confirmer.d.ts +0 -15
- package/dist/apply/e2e-spec.anchor.d.ts +0 -18
- package/dist/apply/main-cors.anchor.d.ts +0 -50
- package/dist/apply/manifest.merger.d.ts +0 -51
- package/dist/aventara.bin.d.ts +0 -2
- package/dist/catalog/adapter.catalog.generated.d.ts +0 -20
- package/dist/catalog/catalog-entry.interface.d.ts +0 -54
- package/dist/catalog/catalog.matcher.d.ts +0 -40
- package/dist/catalog/range.reader.d.ts +0 -11
- package/dist/cli.d.ts +0 -29
- package/dist/command/command.parser.d.ts +0 -38
- package/dist/node-version.guard.d.ts +0 -8
- package/dist/plan/project.planner.d.ts +0 -53
- package/dist/project/package-manager.detector.d.ts +0 -5
- package/dist/project/project.inspector.d.ts +0 -32
- package/dist/project/service.detector.d.ts +0 -40
- package/dist/project/source.scanner.d.ts +0 -16
- package/dist/run/command.runner.d.ts +0 -15
- package/dist/run/init.orchestrator.d.ts +0 -27
- package/dist/run/new.orchestrator.d.ts +0 -20
- package/dist/run/scaffold.committer.d.ts +0 -28
- package/dist/templates/docs.links.d.ts +0 -8
- package/dist/templates/prisma7/prisma7.templates.d.ts +0 -34
- package/dist/templates/template.registry.d.ts +0 -152
- package/dist/wizard/answer.resolver.d.ts +0 -47
- package/dist/wizard/readline.prompter.d.ts +0 -22
- package/dist/wizard/wizard.questions.d.ts +0 -120
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { INIT_FLAG_QUESTIONS, NEW_QUESTIONS, ormAliasesSentence, } from "../wizard/wizard.questions.js";
|
|
2
|
-
export class
|
|
3
|
-
name = "
|
|
2
|
+
export class AvCliCommandError extends Error {
|
|
3
|
+
name = "AvCliCommandError";
|
|
4
4
|
}
|
|
5
5
|
const OPTIONS = [
|
|
6
6
|
{
|
|
@@ -22,6 +22,12 @@ const OPTIONS = [
|
|
|
22
22
|
commands: ["new", "init"],
|
|
23
23
|
summary: "accept every unanswered question's default, and confirm overwriting existing content",
|
|
24
24
|
},
|
|
25
|
+
{
|
|
26
|
+
flag: "--verbose",
|
|
27
|
+
key: "verbose",
|
|
28
|
+
commands: ["new", "init"],
|
|
29
|
+
summary: "show the output of every command it runs as it runs (nest new, the install)",
|
|
30
|
+
},
|
|
25
31
|
];
|
|
26
32
|
const QUESTIONS = {
|
|
27
33
|
new: NEW_QUESTIONS,
|
|
@@ -95,7 +101,7 @@ ${asked(catalog)}`;
|
|
|
95
101
|
export function parseCliCommand(argv) {
|
|
96
102
|
const [command, ...rest] = argv;
|
|
97
103
|
if (command === undefined) {
|
|
98
|
-
throw new
|
|
104
|
+
throw new AvCliCommandError(`a command is required: new or init; ${HELP}`);
|
|
99
105
|
}
|
|
100
106
|
if (command === "--help" || command === "-h") {
|
|
101
107
|
return { command: "help" };
|
|
@@ -104,7 +110,7 @@ export function parseCliCommand(argv) {
|
|
|
104
110
|
return { command: "version" };
|
|
105
111
|
}
|
|
106
112
|
if (command !== "new" && command !== "init") {
|
|
107
|
-
throw new
|
|
113
|
+
throw new AvCliCommandError(`unknown command ${JSON.stringify(command)}: the commands are new and init; ${HELP}`);
|
|
108
114
|
}
|
|
109
115
|
if (rest.includes("--help") || rest.includes("-h")) {
|
|
110
116
|
return { command: "help", topic: command };
|
|
@@ -118,7 +124,7 @@ export function parseCliCommand(argv) {
|
|
|
118
124
|
const argument = rest[index];
|
|
119
125
|
if (!argument.startsWith("-")) {
|
|
120
126
|
if (positional === undefined || given[positional.id] !== undefined) {
|
|
121
|
-
throw new
|
|
127
|
+
throw new AvCliCommandError(`unexpected argument ${JSON.stringify(argument)}: aventara ${command} takes ${positional === undefined ? "no" : "one"} positional argument; ${HELP}`);
|
|
122
128
|
}
|
|
123
129
|
given[positional.id] = argument;
|
|
124
130
|
continue;
|
|
@@ -131,20 +137,20 @@ export function parseCliCommand(argv) {
|
|
|
131
137
|
const canonical = question?.flag ?? option?.flag ?? flag;
|
|
132
138
|
if (question === undefined && option === undefined) {
|
|
133
139
|
const elsewhere = ["new", "init"].filter((other) => QUESTIONS[other].some((candidate) => candidate.flag === flag));
|
|
134
|
-
throw new
|
|
140
|
+
throw new AvCliCommandError(elsewhere.length === 0
|
|
135
141
|
? `unknown flag ${flag}; ${HELP}`
|
|
136
142
|
: `${flag} is a flag of aventara ${elsewhere.join(" and ")}, not aventara ${command}; ${HELP}`);
|
|
137
143
|
}
|
|
138
144
|
if (option !== undefined && !option.commands.includes(command)) {
|
|
139
|
-
throw new
|
|
145
|
+
throw new AvCliCommandError(`${option.flag} is a flag of aventara ${option.commands.join(" and ")}, not aventara ${command}; ${HELP}`);
|
|
140
146
|
}
|
|
141
147
|
if (seen.has(canonical)) {
|
|
142
|
-
throw new
|
|
148
|
+
throw new AvCliCommandError(`${canonical} was given twice; ${HELP}`);
|
|
143
149
|
}
|
|
144
150
|
seen.add(canonical);
|
|
145
151
|
if (question === undefined) {
|
|
146
152
|
if (inline !== undefined) {
|
|
147
|
-
throw new
|
|
153
|
+
throw new AvCliCommandError(`${canonical} takes no value; ${HELP}`);
|
|
148
154
|
}
|
|
149
155
|
options[option.key] = true;
|
|
150
156
|
continue;
|
|
@@ -152,7 +158,7 @@ export function parseCliCommand(argv) {
|
|
|
152
158
|
const value = inline ?? rest[index + 1];
|
|
153
159
|
if (value === undefined ||
|
|
154
160
|
(inline === undefined && value.startsWith("-"))) {
|
|
155
|
-
throw new
|
|
161
|
+
throw new AvCliCommandError(`${canonical} takes a value; ${HELP}`);
|
|
156
162
|
}
|
|
157
163
|
if (inline === undefined) {
|
|
158
164
|
index += 1;
|
|
@@ -165,5 +171,6 @@ export function parseCliCommand(argv) {
|
|
|
165
171
|
skipInstall: options.skipInstall === true,
|
|
166
172
|
skipGit: options.skipGit === true,
|
|
167
173
|
yes: options.yes === true,
|
|
174
|
+
...(options.verbose === true ? { verbose: true } : {}),
|
|
168
175
|
};
|
|
169
176
|
}
|
|
@@ -3,7 +3,7 @@ import { E2E_SPEC, spliceContractTest } from "../apply/e2e-spec.anchor.js";
|
|
|
3
3
|
import { CORS_ORIGINS_VARIABLE, MAIN_FILE, mainCorsInstructions, SCAFFOLD_CORS_ORIGINS, spliceMainCors, } from "../apply/main-cors.anchor.js";
|
|
4
4
|
import { appendEnvEntryIfAbsent, mergeEnvFile, mergeGitignore, mergePackageManifest, mergePnpmWorkspace, } from "../apply/manifest.merger.js";
|
|
5
5
|
import { APP_MODULE, } from "../project/project.inspector.js";
|
|
6
|
-
import {
|
|
6
|
+
import { SCAFFOLD_API, } from "../templates/template.registry.js";
|
|
7
7
|
export const PREPARE_SCRIPT = "aventara:prepare";
|
|
8
8
|
const PREPARED_SCRIPTS = [
|
|
9
9
|
"build",
|
|
@@ -113,9 +113,7 @@ export function planInit(input) {
|
|
|
113
113
|
keeping.push(...writeOf(APP_MODULE, appModule, edit.text));
|
|
114
114
|
replacing.push(...writeOf(APP_MODULE, appModule, edit.text));
|
|
115
115
|
const spec = input.read(E2E_SPEC);
|
|
116
|
-
const tested = spec === undefined
|
|
117
|
-
? undefined
|
|
118
|
-
: spliceContractTest(spec, SCAFFOLD_ENTRYPOINT);
|
|
116
|
+
const tested = spec === undefined ? undefined : spliceContractTest(spec, SCAFFOLD_API);
|
|
119
117
|
if (tested !== undefined) {
|
|
120
118
|
keeping.push(...writeOf(E2E_SPEC, spec, tested));
|
|
121
119
|
replacing.push(...writeOf(E2E_SPEC, spec, tested));
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { lowestMajorOf } from "../catalog/range.reader.js";
|
|
4
|
-
export class
|
|
5
|
-
name = "
|
|
4
|
+
export class AvProjectRefusedError extends Error {
|
|
5
|
+
name = "AvProjectRefusedError";
|
|
6
6
|
}
|
|
7
7
|
const NEST_MAJOR = 12;
|
|
8
8
|
const FIRST_UNSUPPORTED_TYPESCRIPT = 7;
|
|
@@ -23,32 +23,32 @@ function matchesFamily(pattern, name) {
|
|
|
23
23
|
export function inspectProject(directory, catalog) {
|
|
24
24
|
const manifestPath = path.join(directory, "package.json");
|
|
25
25
|
if (!existsSync(manifestPath)) {
|
|
26
|
-
throw new
|
|
26
|
+
throw new AvProjectRefusedError(`aventara init runs in a NestJS ${NEST_MAJOR} project, and there is no package.json in ${directory}`);
|
|
27
27
|
}
|
|
28
28
|
const manifestText = readFileSync(manifestPath, "utf8");
|
|
29
29
|
const manifest = JSON.parse(manifestText);
|
|
30
30
|
const nest = dependencyRange(manifest, "@nestjs/core");
|
|
31
31
|
if (nest === undefined) {
|
|
32
|
-
throw new
|
|
32
|
+
throw new AvProjectRefusedError(`aventara init runs in a NestJS ${NEST_MAJOR} project, and this package.json has no @nestjs/core dependency`);
|
|
33
33
|
}
|
|
34
34
|
if (lowestMajorOf(nest) !== NEST_MAJOR) {
|
|
35
|
-
throw new
|
|
35
|
+
throw new AvProjectRefusedError(`aventara init supports NestJS ${NEST_MAJOR} only, and this project depends on @nestjs/core ${nest}`);
|
|
36
36
|
}
|
|
37
37
|
const typescript = dependencyRange(manifest, "typescript");
|
|
38
38
|
const typescriptMajor = typescript === undefined ? undefined : lowestMajorOf(typescript);
|
|
39
39
|
if (typescriptMajor !== undefined &&
|
|
40
40
|
typescriptMajor >= FIRST_UNSUPPORTED_TYPESCRIPT) {
|
|
41
|
-
throw new
|
|
41
|
+
throw new AvProjectRefusedError(`this project declares typescript ${typescript}, and TypeScript ${FIRST_UNSUPPORTED_TYPESCRIPT} has no classic compiler API for Aventara's generate steps to read types with; use TypeScript 6 or 5`);
|
|
42
42
|
}
|
|
43
43
|
const lockfiles = LOCKFILES.filter(([file]) => existsSync(path.join(directory, file))).map(([, manager]) => manager);
|
|
44
44
|
const known = lockfiles.filter((manager) => manager !== "yarn");
|
|
45
45
|
if (lockfiles.includes("yarn") && known.length === 0) {
|
|
46
|
-
throw new
|
|
46
|
+
throw new AvProjectRefusedError("this project has only a yarn.lock, and aventara init supports npm and pnpm");
|
|
47
47
|
}
|
|
48
48
|
const appModule = path.join(directory, APP_MODULE);
|
|
49
49
|
if (existsSync(appModule) &&
|
|
50
50
|
/\bAventaraModule\b/.test(readFileSync(appModule, "utf8"))) {
|
|
51
|
-
throw new
|
|
51
|
+
throw new AvProjectRefusedError(`this project is already initialized: ${APP_MODULE} uses AventaraModule`);
|
|
52
52
|
}
|
|
53
53
|
const dependencies = [
|
|
54
54
|
...Object.keys(manifest.dependencies ?? {}),
|
|
@@ -9,9 +9,11 @@ export const runCommand = (command, args, options) => new Promise((resolve, reje
|
|
|
9
9
|
let stderr = "";
|
|
10
10
|
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
11
11
|
stdout += chunk;
|
|
12
|
+
options.onOutput?.(chunk);
|
|
12
13
|
});
|
|
13
14
|
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
14
15
|
stderr += chunk;
|
|
16
|
+
options.onOutput?.(chunk);
|
|
15
17
|
});
|
|
16
18
|
child.once("error", reject);
|
|
17
19
|
child.once("close", (code) => {
|
|
@@ -4,15 +4,25 @@ import { confirmConflicts } from "../apply/conflict.confirmer.js";
|
|
|
4
4
|
import { matchInstalledOrm, matchOrmSetup, } from "../catalog/catalog.matcher.js";
|
|
5
5
|
import { planInit } from "../plan/project.planner.js";
|
|
6
6
|
import { detectPackageManager } from "../project/package-manager.detector.js";
|
|
7
|
-
import {
|
|
7
|
+
import { AvProjectRefusedError, inspectProject, } from "../project/project.inspector.js";
|
|
8
8
|
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
|
-
|
|
13
|
-
|
|
12
|
+
import { createStepReporter, } from "./step.reporter.js";
|
|
13
|
+
export function printNextSteps(stdout, steps) {
|
|
14
|
+
stdout(`Next:\n${steps.map((step, index) => ` ${index + 1}. ${step}`).join("\n")}\n`);
|
|
15
|
+
}
|
|
16
|
+
export class AvInstallFailedError extends Error {
|
|
17
|
+
name = "AvInstallFailedError";
|
|
14
18
|
}
|
|
15
19
|
export async function runInit(command, io) {
|
|
20
|
+
const { progress, nextSteps } = await initProject(command, io);
|
|
21
|
+
progress.finish("aventara", "done");
|
|
22
|
+
printNextSteps(io.stdout, nextSteps);
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
export async function initProject(command, io) {
|
|
16
26
|
const inspection = inspectProject(io.directory, io.catalog);
|
|
17
27
|
const read = (file) => {
|
|
18
28
|
try {
|
|
@@ -48,11 +58,20 @@ export async function runInit(command, io) {
|
|
|
48
58
|
prompter: io.prompter,
|
|
49
59
|
});
|
|
50
60
|
const writes = plan.conflicts.length === 0 ? plan.keeping : plan.replacing;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
const progress = io.progress ??
|
|
62
|
+
createStepReporter({
|
|
63
|
+
write: io.stderr,
|
|
64
|
+
mode: io.progressMode,
|
|
65
|
+
total: command.skipInstall ? 1 : 2,
|
|
66
|
+
verbose: command.verbose === true,
|
|
67
|
+
});
|
|
68
|
+
await progress.step("Writing Aventara's files", () => {
|
|
69
|
+
for (const write of writes) {
|
|
70
|
+
const target = path.join(io.directory, write.path);
|
|
71
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
72
|
+
writeFileSync(target, write.content);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
56
75
|
for (const warning of plan.warnings) {
|
|
57
76
|
io.stderr(`aventara: warning: ${warning}.\n`);
|
|
58
77
|
}
|
|
@@ -63,16 +82,17 @@ export async function runInit(command, io) {
|
|
|
63
82
|
io.stdout(`${plan.instructions}\n`);
|
|
64
83
|
}
|
|
65
84
|
if (!command.skipInstall) {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
85
|
+
await progress.step(`Installing dependencies (${packageManager} install)`, async () => {
|
|
86
|
+
const installed = await io.runner(packageManager, ["install"], {
|
|
87
|
+
cwd: io.directory,
|
|
88
|
+
onOutput: progress.output,
|
|
89
|
+
});
|
|
90
|
+
if (installed.code !== 0) {
|
|
91
|
+
throw new AvInstallFailedError(`\`${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()}`);
|
|
92
|
+
}
|
|
69
93
|
});
|
|
70
|
-
if (installed.code !== 0) {
|
|
71
|
-
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()}`);
|
|
72
|
-
}
|
|
73
94
|
}
|
|
74
|
-
|
|
75
|
-
return 0;
|
|
95
|
+
return { progress, nextSteps: plan.nextSteps };
|
|
76
96
|
}
|
|
77
97
|
async function prepareFresh(command, io, inspection, detectedPackageManager) {
|
|
78
98
|
const answers = await resolveAnswers(INIT_QUESTIONS, {
|
|
@@ -124,25 +144,25 @@ async function prepareReuse(command, io, inspection, detectedPackageManager) {
|
|
|
124
144
|
installCommand: `${detectedPackageManager} install`,
|
|
125
145
|
});
|
|
126
146
|
if (verdict.kind === "refused") {
|
|
127
|
-
throw new
|
|
147
|
+
throw new AvProjectRefusedError(verdict.sentence);
|
|
128
148
|
}
|
|
129
149
|
const { entry } = verdict;
|
|
130
150
|
const templates = templatesFor(entry);
|
|
131
151
|
const facts = templates.readProject(io.directory, inspection.manifest);
|
|
132
152
|
const problem = matchOrmSetup(entry, facts);
|
|
133
153
|
if (problem !== undefined) {
|
|
134
|
-
throw new
|
|
154
|
+
throw new AvProjectRefusedError(problem);
|
|
135
155
|
}
|
|
136
156
|
const provider = facts.provider;
|
|
137
157
|
if (facts.clientOutput === undefined) {
|
|
138
|
-
throw new
|
|
158
|
+
throw new AvProjectRefusedError(`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}`);
|
|
139
159
|
}
|
|
140
160
|
for (const [flag, given, found] of [
|
|
141
161
|
["--orm", command.given.orm, entry.alias],
|
|
142
162
|
["--db", command.given.db, provider],
|
|
143
163
|
]) {
|
|
144
164
|
if (given !== undefined && given.trim().toLowerCase() !== found) {
|
|
145
|
-
throw new
|
|
165
|
+
throw new AvProjectRefusedError(`${flag} ${given} does not match this project, which already uses ${found}`);
|
|
146
166
|
}
|
|
147
167
|
}
|
|
148
168
|
const scan = scanServices(io.directory, templates.clientShape(facts));
|
|
@@ -2,11 +2,12 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { detectPackageManager } from "../project/package-manager.detector.js";
|
|
5
|
-
import {
|
|
5
|
+
import { AvProjectRefusedError } 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
|
-
import {
|
|
8
|
+
import { AvInstallFailedError, initProject, printNextSteps, } from "./init.orchestrator.js";
|
|
9
9
|
import { commitScaffold, gitSurroundings } from "./scaffold.committer.js";
|
|
10
|
+
import { createStepReporter } from "./step.reporter.js";
|
|
10
11
|
export const NEST_CLI = "@nestjs/cli@12.0.8";
|
|
11
12
|
export const NEST_APP_MODULE_SHA256 = "fe98c4cd2552342272494652287c02e4ef58db3072fcb18513ea831c60f85778";
|
|
12
13
|
function oneOff(packageManager) {
|
|
@@ -33,7 +34,7 @@ export async function runNew(command, io) {
|
|
|
33
34
|
}
|
|
34
35
|
const directory = path.join(io.cwd, name);
|
|
35
36
|
if (existsSync(directory) && readdirSync(directory).length > 0) {
|
|
36
|
-
throw new
|
|
37
|
+
throw new AvProjectRefusedError(`aventara new creates ${directory}, which already exists and is not empty; choose another name, or run aventara init inside an existing project`);
|
|
37
38
|
}
|
|
38
39
|
const surroundings = command.skipGit
|
|
39
40
|
? undefined
|
|
@@ -50,19 +51,29 @@ export async function runNew(command, io) {
|
|
|
50
51
|
"--no-observe",
|
|
51
52
|
...(command.skipGit ? ["--skip-git"] : []),
|
|
52
53
|
];
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
const progress = createStepReporter({
|
|
55
|
+
write: io.stderr,
|
|
56
|
+
mode: io.progressMode,
|
|
57
|
+
total: 2 + (command.skipInstall ? 0 : 1) + (command.skipGit ? 0 : 1),
|
|
58
|
+
verbose: command.verbose === true,
|
|
59
|
+
});
|
|
60
|
+
await progress.step(`Creating the NestJS project (${runner} ${[...prefix, NEST_CLI, "new", name].join(" ")})`, async () => {
|
|
61
|
+
const created = await io.runner(runner, args, {
|
|
62
|
+
cwd: io.cwd,
|
|
63
|
+
onOutput: progress.output,
|
|
64
|
+
});
|
|
65
|
+
if (created.code !== 0) {
|
|
66
|
+
throw new AvInstallFailedError(`\`${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()}`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
58
69
|
const appModule = path.join(directory, "src", "app.module.ts");
|
|
59
70
|
const digest = existsSync(appModule)
|
|
60
71
|
? createHash("sha256").update(readFileSync(appModule)).digest("hex")
|
|
61
72
|
: "(absent)";
|
|
62
73
|
if (digest !== NEST_APP_MODULE_SHA256) {
|
|
63
|
-
throw new
|
|
74
|
+
throw new AvInstallFailedError(`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`);
|
|
64
75
|
}
|
|
65
|
-
const
|
|
76
|
+
const { nextSteps } = await initProject({
|
|
66
77
|
command: "init",
|
|
67
78
|
given: {
|
|
68
79
|
orm: orm.alias,
|
|
@@ -72,9 +83,16 @@ export async function runNew(command, io) {
|
|
|
72
83
|
skipInstall: command.skipInstall,
|
|
73
84
|
skipGit: false,
|
|
74
85
|
yes: true,
|
|
75
|
-
}, {
|
|
76
|
-
|
|
77
|
-
|
|
86
|
+
}, {
|
|
87
|
+
...io,
|
|
88
|
+
directory,
|
|
89
|
+
progress,
|
|
90
|
+
});
|
|
91
|
+
if (surroundings !== undefined) {
|
|
92
|
+
const committed = await progress.step("Committing the scaffold", () => commitScaffold(io.runner, directory, surroundings, io.cwd));
|
|
93
|
+
io.stdout(`aventara: ${committed}.\n`);
|
|
78
94
|
}
|
|
79
|
-
|
|
95
|
+
progress.finish("aventara", `created ${name}`);
|
|
96
|
+
printNextSteps(io.stdout, [`Enter the project: cd ${name}`, ...nextSteps]);
|
|
97
|
+
return 0;
|
|
80
98
|
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
export function progressModeOf(isTTY, env) {
|
|
2
|
+
const ci = env.CI;
|
|
3
|
+
if (isTTY !== true ||
|
|
4
|
+
(ci !== undefined && ci !== "" && ci !== "0" && ci !== "false") ||
|
|
5
|
+
(env.NO_COLOR !== undefined && env.NO_COLOR !== "") ||
|
|
6
|
+
env.TERM === "dumb") {
|
|
7
|
+
return "plain";
|
|
8
|
+
}
|
|
9
|
+
return "live";
|
|
10
|
+
}
|
|
11
|
+
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
12
|
+
const CLEAR_LINE = "\r\u001b[2K";
|
|
13
|
+
const GREEN = (text) => `\u001b[32m${text}\u001b[39m`;
|
|
14
|
+
const RED = (text) => `\u001b[31m${text}\u001b[39m`;
|
|
15
|
+
const DIM = (text) => `\u001b[2m${text}\u001b[22m`;
|
|
16
|
+
export function formatElapsed(milliseconds) {
|
|
17
|
+
const seconds = milliseconds / 1000;
|
|
18
|
+
if (seconds < 60) {
|
|
19
|
+
return `${seconds.toFixed(1)}s`;
|
|
20
|
+
}
|
|
21
|
+
const whole = Math.round(seconds);
|
|
22
|
+
return `${Math.floor(whole / 60)}m ${String(whole % 60).padStart(2, "0")}s`;
|
|
23
|
+
}
|
|
24
|
+
export function createStepReporter(options) {
|
|
25
|
+
const now = options.now ?? Date.now;
|
|
26
|
+
const started = now();
|
|
27
|
+
const verbose = options.verbose === true && options.mode !== "off";
|
|
28
|
+
const mode = verbose ? "plain" : options.mode;
|
|
29
|
+
let index = 0;
|
|
30
|
+
const step = async (title, body) => {
|
|
31
|
+
index += 1;
|
|
32
|
+
const label = `[${index}/${options.total}] ${title}`;
|
|
33
|
+
const at = now();
|
|
34
|
+
const elapsed = () => formatElapsed(now() - at);
|
|
35
|
+
if (mode === "off") {
|
|
36
|
+
return await body();
|
|
37
|
+
}
|
|
38
|
+
if (mode === "plain") {
|
|
39
|
+
options.write(verbose ? `${label}…\n` : `${label}… `);
|
|
40
|
+
try {
|
|
41
|
+
const value = await body();
|
|
42
|
+
options.write(verbose ? `${label}… done (${elapsed()})\n` : `done (${elapsed()})\n`);
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
options.write(verbose
|
|
47
|
+
? `${label}… failed (${elapsed()})\n`
|
|
48
|
+
: `failed (${elapsed()})\n`);
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
let frame = 0;
|
|
53
|
+
const draw = () => {
|
|
54
|
+
const seconds = Math.floor((now() - at) / 1000);
|
|
55
|
+
options.write(`${CLEAR_LINE}${SPINNER[frame % SPINNER.length]} ${label}…${seconds > 0 ? DIM(` ${seconds}s`) : ""}`);
|
|
56
|
+
frame += 1;
|
|
57
|
+
};
|
|
58
|
+
draw();
|
|
59
|
+
const timer = setInterval(draw, options.interval ?? 80);
|
|
60
|
+
timer.unref?.();
|
|
61
|
+
try {
|
|
62
|
+
const value = await body();
|
|
63
|
+
clearInterval(timer);
|
|
64
|
+
options.write(`${CLEAR_LINE}${GREEN("✓")} ${label} ${DIM(elapsed())}\n`);
|
|
65
|
+
return value;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
clearInterval(timer);
|
|
69
|
+
options.write(`${CLEAR_LINE}${RED("✗")} ${label} ${DIM(elapsed())}\n`);
|
|
70
|
+
throw error;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
return {
|
|
74
|
+
step,
|
|
75
|
+
output: (chunk) => {
|
|
76
|
+
if (verbose) {
|
|
77
|
+
options.write(chunk);
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
finish: (prefix, what) => {
|
|
81
|
+
if (mode !== "off") {
|
|
82
|
+
options.write(`${prefix}: ${what} in ${formatElapsed(now() - started)}.\n`);
|
|
83
|
+
}
|
|
84
|
+
},
|
|
85
|
+
verbose,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
export const DOCS_BASE_URL = "https://aventara-
|
|
1
|
+
export const DOCS_BASE_URL = "https://aventara-framework.vercel.app";
|
|
2
|
+
export const EXPOSURE_DOCS = `${DOCS_BASE_URL}/docs/exposure`;
|
|
2
3
|
export const RATE_LIMITING_DOCS = `${DOCS_BASE_URL}/docs/limits-and-safety#rate-limiting`;
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
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
|
-
import {
|
|
4
|
+
import { EXPOSURE_DOCS } from "../docs.links.js";
|
|
5
|
+
import { SCAFFOLD_API, } from "../template.registry.js";
|
|
5
6
|
const CLIENT_OUTPUT = "src/generated/prisma";
|
|
6
7
|
const SCHEMA = "prisma/schema.prisma";
|
|
7
8
|
const CONFIG_FILES = ["prisma7.config.ts", "prisma.config.ts"];
|
|
@@ -111,18 +112,38 @@ function aventaraConfig(input) {
|
|
|
111
112
|
const file = "src/aventara.config.ts";
|
|
112
113
|
const service = input.service.exportName;
|
|
113
114
|
return [
|
|
115
|
+
"import type { AvDiagnostic } from '@aventara/core';",
|
|
114
116
|
`import { createPrismaAdapter } from '${input.entry.adapterPackage}';`,
|
|
117
|
+
"import { Logger } from '@nestjs/common';",
|
|
115
118
|
`import { discovery } from '${specifier(file, input.artifact.replace(/\.ts$/, ""), input.js)}';`,
|
|
116
119
|
`import type { ${service} } from '${specifier(file, input.service.file.replace(/\.ts$/, ""), input.js)}';`,
|
|
117
120
|
"",
|
|
121
|
+
"const logger = new Logger('Aventara');",
|
|
122
|
+
"",
|
|
118
123
|
"/**",
|
|
119
|
-
" * Aventara's configuration: the
|
|
120
|
-
" *
|
|
121
|
-
" *
|
|
124
|
+
" * Aventara's configuration: where the API is mounted (`api`), and what you add",
|
|
125
|
+
" * to it (`exposed` and `exemptions`, `pipelines`, and the per-Resource",
|
|
126
|
+
" * `resources` tree).",
|
|
127
|
+
" * AventaraModule (app.module.ts) builds the framework from it; `discovery` is",
|
|
128
|
+
" * regenerated by the aventara:prepare script.",
|
|
122
129
|
" */",
|
|
123
130
|
`export async function aventaraConfig(prisma: ${service}) {`,
|
|
124
131
|
" return {",
|
|
125
|
-
`
|
|
132
|
+
` api: '${SCAFFOLD_API}',`,
|
|
133
|
+
" // Everything the adapter offers is exposed. List what to hide under",
|
|
134
|
+
" // `exemptions`, or set `exposed: false` and list what to expose instead.",
|
|
135
|
+
` // See: ${EXPOSURE_DOCS}`,
|
|
136
|
+
" exposed: true,",
|
|
137
|
+
" // When something fails inside the server, the caller gets a plain 500 and",
|
|
138
|
+
" // Aventara logs one line naming the request. This logs the error itself,",
|
|
139
|
+
" // with its stack, through Nest's logger.",
|
|
140
|
+
" diagnostics: (diagnostic: AvDiagnostic) => {",
|
|
141
|
+
" const { error } = diagnostic;",
|
|
142
|
+
" logger.error(",
|
|
143
|
+
` \`\${diagnostic.code} (request \${diagnostic.requestId}): raw error\`,`,
|
|
144
|
+
" error instanceof Error ? error.stack : String(error),",
|
|
145
|
+
" );",
|
|
146
|
+
" },",
|
|
126
147
|
" adapter: await createPrismaAdapter({",
|
|
127
148
|
input.service.clientMember === undefined
|
|
128
149
|
? " client: prisma,"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export class
|
|
3
|
-
name = "
|
|
1
|
+
import { AvWizardAnswerError } from "./wizard.questions.js";
|
|
2
|
+
export class AvUnansweredQuestionsError extends Error {
|
|
3
|
+
name = "AvUnansweredQuestionsError";
|
|
4
4
|
}
|
|
5
5
|
function flagSpelling(question, context) {
|
|
6
6
|
if (!question.flag.startsWith("--")) {
|
|
@@ -41,7 +41,7 @@ async function prompt(question, context, prompter) {
|
|
|
41
41
|
return question.parse(raw, context);
|
|
42
42
|
}
|
|
43
43
|
catch (error) {
|
|
44
|
-
if (!(error instanceof
|
|
44
|
+
if (!(error instanceof AvWizardAnswerError)) {
|
|
45
45
|
throw error;
|
|
46
46
|
}
|
|
47
47
|
prompter.say(`${error.message}.`);
|
|
@@ -90,7 +90,7 @@ export async function resolveAnswers(questions, sources) {
|
|
|
90
90
|
});
|
|
91
91
|
}
|
|
92
92
|
if (missing.length > 0) {
|
|
93
|
-
throw new
|
|
93
|
+
throw new AvUnansweredQuestionsError(unansweredSentence(missing, sources.yes));
|
|
94
94
|
}
|
|
95
95
|
return answered;
|
|
96
96
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { createInterface } from "node:readline";
|
|
2
|
-
export class
|
|
3
|
-
name = "
|
|
2
|
+
export class AvWizardCancelledError extends Error {
|
|
3
|
+
name = "AvWizardCancelledError";
|
|
4
4
|
}
|
|
5
5
|
export function createReadlinePrompter(streams) {
|
|
6
6
|
let session;
|
|
7
7
|
let ended = false;
|
|
8
8
|
const lines = [];
|
|
9
9
|
const waiting = [];
|
|
10
|
-
const cancelled = () => new
|
|
10
|
+
const cancelled = () => new AvWizardCancelledError("the wizard was closed before it was answered");
|
|
11
11
|
const open = () => {
|
|
12
12
|
if (session === undefined) {
|
|
13
13
|
session = createInterface({
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { modulesProviding, } from "../project/service.detector.js";
|
|
2
2
|
export const PACKAGE_MANAGERS = ["npm", "pnpm"];
|
|
3
|
-
export class
|
|
4
|
-
name = "
|
|
3
|
+
export class AvWizardAnswerError extends Error {
|
|
4
|
+
name = "AvWizardAnswerError";
|
|
5
5
|
}
|
|
6
6
|
function ormLabel(entry) {
|
|
7
7
|
return `${entry.orm.label} ${entry.orm.major}`;
|
|
@@ -32,7 +32,7 @@ export const NAME_QUESTION = {
|
|
|
32
32
|
parse: (raw) => {
|
|
33
33
|
const name = raw.trim();
|
|
34
34
|
if (!NAME.test(name)) {
|
|
35
|
-
throw new
|
|
35
|
+
throw new AvWizardAnswerError(`${JSON.stringify(raw)} is not a project name: use letters, digits, ".", "_" and "-", starting with a letter or digit`);
|
|
36
36
|
}
|
|
37
37
|
return name;
|
|
38
38
|
},
|
|
@@ -52,7 +52,7 @@ export const ORM_QUESTION = {
|
|
|
52
52
|
const alias = raw.trim().toLowerCase();
|
|
53
53
|
const entry = context.catalog.find((candidate) => candidate.alias === alias);
|
|
54
54
|
if (entry === undefined) {
|
|
55
|
-
throw new
|
|
55
|
+
throw new AvWizardAnswerError(`there is no Aventara adapter for the ORM ${JSON.stringify(raw)}; ${ormAliasesSentence(context.catalog)}`);
|
|
56
56
|
}
|
|
57
57
|
return entry;
|
|
58
58
|
},
|
|
@@ -82,7 +82,7 @@ export const DB_QUESTION = {
|
|
|
82
82
|
const orm = requireOrm(context);
|
|
83
83
|
const db = raw.trim().toLowerCase();
|
|
84
84
|
if (!orm.providers.includes(db)) {
|
|
85
|
-
throw new
|
|
85
|
+
throw new AvWizardAnswerError(`the ${ormLabel(orm)} adapter does not support the database ${JSON.stringify(raw)}; choose ${orm.providers.join(" or ")}`);
|
|
86
86
|
}
|
|
87
87
|
return db;
|
|
88
88
|
},
|
|
@@ -98,7 +98,7 @@ export const PACKAGE_MANAGER_QUESTION = {
|
|
|
98
98
|
parse: (raw) => {
|
|
99
99
|
const manager = PACKAGE_MANAGERS.find((candidate) => candidate === raw.trim().toLowerCase());
|
|
100
100
|
if (manager === undefined) {
|
|
101
|
-
throw new
|
|
101
|
+
throw new AvWizardAnswerError(`${JSON.stringify(raw)} is not a supported package manager; choose ${PACKAGE_MANAGERS.join(" or ")}`);
|
|
102
102
|
}
|
|
103
103
|
return manager;
|
|
104
104
|
},
|
|
@@ -109,7 +109,7 @@ function reference(raw, flag) {
|
|
|
109
109
|
const exportName = raw.slice(hash + 1).trim();
|
|
110
110
|
const file = raw.slice(0, hash).trim().replace(/^\.\//, "");
|
|
111
111
|
if (hash <= 0 || !/^[A-Za-z_$][\w$]*$/.test(exportName)) {
|
|
112
|
-
throw new
|
|
112
|
+
throw new AvWizardAnswerError(`${flag} takes <path#Export>, a file and the name it exports, such as src/prisma.service.ts#PrismaService; got ${JSON.stringify(raw)}`);
|
|
113
113
|
}
|
|
114
114
|
return { file, exportName };
|
|
115
115
|
}
|
|
@@ -166,7 +166,7 @@ export const SERVICE_QUESTION = {
|
|
|
166
166
|
const options = serviceOptions(services);
|
|
167
167
|
const chosen = options.find((option) => option.file === wanted.file && option.exportName === wanted.exportName);
|
|
168
168
|
if (chosen === undefined) {
|
|
169
|
-
throw new
|
|
169
|
+
throw new AvWizardAnswerError(`${spelled(wanted)} is not a service found in src/ (a class extending ${services.clientClass}, or one holding it); found: ${options
|
|
170
170
|
.filter((option) => !option.isNew)
|
|
171
171
|
.map(spelled)
|
|
172
172
|
.join(", ") || "none"}`);
|
|
@@ -202,7 +202,7 @@ export const MODULE_QUESTION = {
|
|
|
202
202
|
: modulesProviding(services.scan, service);
|
|
203
203
|
const chosen = modules.find((module) => module.file === wanted.file && module.exportName === wanted.exportName);
|
|
204
204
|
if (chosen === undefined) {
|
|
205
|
-
throw new
|
|
205
|
+
throw new AvWizardAnswerError(`${spelled(wanted)} is not a module that provides and exports ${service.exportName}; found: ${modules.map(spelled).join(", ") || "none"}`);
|
|
206
206
|
}
|
|
207
207
|
return { file: chosen.file, exportName: chosen.exportName };
|
|
208
208
|
},
|