@aventara/cli 1.0.0-rc.1 → 1.0.0-rc.11
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 +261 -1
- package/README.md +55 -6
- package/dist/apply/conflict.confirmer.js +1 -1
- package/dist/catalog/adapter.catalog.generated.js +1 -1
- package/dist/catalog/host-entry.interface.js +10 -0
- package/dist/catalog/host.catalog.generated.js +32 -0
- package/dist/cli.js +15 -2
- package/dist/command/command.parser.js +91 -5
- package/dist/plan/host-plan.interface.js +1 -0
- package/dist/plan/nest-host.planner.js +131 -0
- package/dist/plan/next-host.planner.js +133 -0
- package/dist/plan/project.planner.js +94 -165
- package/dist/project/project.inspector.js +53 -14
- package/dist/run/command.runner.js +37 -19
- package/dist/run/init.orchestrator.js +47 -30
- package/dist/run/new.orchestrator.js +102 -27
- package/dist/run/scaffold.committer.js +1 -1
- package/dist/run/skill.orchestrator.js +54 -0
- package/dist/skill/agent-skill.generated.js +8 -0
- package/dist/skill/agent-skill.planner.js +116 -0
- package/dist/templates/docs.links.js +1 -1
- package/dist/templates/next-server.entry.js +5 -0
- package/dist/templates/prisma7/prisma7-nest.templates.js +241 -0
- package/dist/templates/prisma7/prisma7-next.templates.js +189 -0
- package/dist/templates/prisma7/prisma7-orm.templates.js +180 -0
- package/dist/templates/prisma7/prisma7.templates.js +7 -386
- package/dist/templates/template.registry.js +1 -1
- package/dist/wizard/wizard.questions.js +2 -2
- package/package.json +3 -3
|
@@ -1,43 +1,10 @@
|
|
|
1
|
-
import { spliceAppModule, wiringInstructions, } from "../apply/app-module.anchor.js";
|
|
2
|
-
import { E2E_SPEC, spliceContractTest } from "../apply/e2e-spec.anchor.js";
|
|
3
|
-
import { CORS_ORIGINS_VARIABLE, MAIN_FILE, mainCorsInstructions, SCAFFOLD_CORS_ORIGINS, spliceMainCors, } from "../apply/main-cors.anchor.js";
|
|
4
1
|
import { appendEnvEntryIfAbsent, mergeEnvFile, mergeGitignore, mergePackageManifest, mergePnpmWorkspace, } from "../apply/manifest.merger.js";
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"build",
|
|
10
|
-
"start",
|
|
11
|
-
"start:dev",
|
|
12
|
-
"start:debug",
|
|
13
|
-
];
|
|
14
|
-
const START_SCRIPTS = {
|
|
15
|
-
start: { from: "nest start", to: "nest start --env-file .env" },
|
|
16
|
-
"start:dev": {
|
|
17
|
-
from: "nest start --watch",
|
|
18
|
-
to: "nest start --watch --env-file .env",
|
|
19
|
-
},
|
|
20
|
-
"start:debug": {
|
|
21
|
-
from: "nest start --debug --watch",
|
|
22
|
-
to: "nest start --debug --watch --env-file .env",
|
|
23
|
-
},
|
|
24
|
-
"start:prod": {
|
|
25
|
-
from: "node dist/main",
|
|
26
|
-
to: "node --env-file-if-exists=.env dist/main",
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
const E2E_SCRIPTS = {
|
|
30
|
-
esm: {
|
|
31
|
-
from: "vitest run --config ./vitest.config.e2e.ts",
|
|
32
|
-
to: "node --env-file-if-exists=.env ./node_modules/vitest/vitest.mjs run --config ./vitest.config.e2e.ts",
|
|
33
|
-
},
|
|
34
|
-
cjs: {
|
|
35
|
-
from: "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config ./test/jest-e2e.json",
|
|
36
|
-
to: "node --env-file-if-exists=.env --experimental-vm-modules ./node_modules/jest/bin/jest.js --config ./test/jest-e2e.json",
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
const NEST_HOST = "@aventara/nest";
|
|
2
|
+
import { planAgentSkill, SKILL_DIRECTORY, } from "../skill/agent-skill.planner.js";
|
|
3
|
+
import { PREPARE_SCRIPT, } from "./host-plan.interface.js";
|
|
4
|
+
import { planNestHost } from "./nest-host.planner.js";
|
|
5
|
+
import { planNextHost } from "./next-host.planner.js";
|
|
40
6
|
const CORE = "@aventara/core";
|
|
7
|
+
const BUILD = "@aventara/build";
|
|
41
8
|
function writeOf(path, before, after) {
|
|
42
9
|
return before === after
|
|
43
10
|
? []
|
|
@@ -49,170 +16,132 @@ function writeOf(path, before, after) {
|
|
|
49
16
|
},
|
|
50
17
|
];
|
|
51
18
|
}
|
|
52
|
-
export function frontendInitCommand(packageManager, cliVersion) {
|
|
53
|
-
const tag = /^\d+\.\d+\.\d+-([0-9A-Za-z-]+)/.exec(cliVersion)?.[1];
|
|
54
|
-
const runner = packageManager === "pnpm" ? "pnpm dlx" : "npx";
|
|
55
|
-
return `${runner} @aventara/client${tag === undefined ? "" : `@${tag}`} init`;
|
|
56
|
-
}
|
|
57
19
|
export function planInit(input) {
|
|
58
|
-
const { inspection,
|
|
20
|
+
const { inspection, hosted, packageManager } = input;
|
|
21
|
+
const { scaffold } = hosted;
|
|
59
22
|
const conflicts = new Set();
|
|
60
23
|
const keeping = [];
|
|
61
24
|
const replacing = [];
|
|
62
25
|
const warnings = [...(input.warnings ?? [])];
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
26
|
+
const instructions = [];
|
|
27
|
+
const builder = {
|
|
28
|
+
read: input.read,
|
|
29
|
+
file: (path, content) => {
|
|
30
|
+
const before = input.read(path);
|
|
31
|
+
if (before === undefined) {
|
|
32
|
+
keeping.push({ path, content, change: "create" });
|
|
33
|
+
replacing.push({ path, content, change: "create" });
|
|
34
|
+
}
|
|
35
|
+
else if (before !== content) {
|
|
36
|
+
conflicts.add(path);
|
|
37
|
+
replacing.push({ path, content, change: "replace" });
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
write: (path, before, after) => {
|
|
41
|
+
keeping.push(...writeOf(path, before, after));
|
|
42
|
+
replacing.push(...writeOf(path, before, after));
|
|
43
|
+
},
|
|
44
|
+
both: (path, before, merge) => {
|
|
45
|
+
const kept = merge(false);
|
|
46
|
+
const replaced = merge(true);
|
|
47
|
+
for (const conflict of replaced.conflicts) {
|
|
48
|
+
conflicts.add(conflict);
|
|
49
|
+
}
|
|
50
|
+
keeping.push(...writeOf(path, before, kept.text));
|
|
51
|
+
replacing.push(...writeOf(path, before, replaced.text));
|
|
52
|
+
},
|
|
53
|
+
warn: (warning) => {
|
|
54
|
+
warnings.push(warning);
|
|
55
|
+
},
|
|
56
|
+
instruct: (text) => {
|
|
57
|
+
instructions.push(text);
|
|
58
|
+
},
|
|
71
59
|
};
|
|
72
60
|
for (const file of scaffold.files) {
|
|
73
|
-
|
|
74
|
-
if (before === undefined) {
|
|
75
|
-
keeping.push({
|
|
76
|
-
path: file.path,
|
|
77
|
-
content: file.content,
|
|
78
|
-
change: "create",
|
|
79
|
-
});
|
|
80
|
-
replacing.push({
|
|
81
|
-
path: file.path,
|
|
82
|
-
content: file.content,
|
|
83
|
-
change: "create",
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
else if (before !== file.content) {
|
|
87
|
-
conflicts.add(file.path);
|
|
88
|
-
replacing.push({
|
|
89
|
-
path: file.path,
|
|
90
|
-
content: file.content,
|
|
91
|
-
change: "replace",
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
const wiring = {
|
|
96
|
-
imports: [
|
|
97
|
-
`import { AventaraModule } from '${NEST_HOST}';`,
|
|
98
|
-
...scaffold.appModule.imports,
|
|
99
|
-
],
|
|
100
|
-
entries: scaffold.appModule.entries,
|
|
101
|
-
};
|
|
102
|
-
const appModule = input.read(APP_MODULE);
|
|
103
|
-
const edit = input.wiringByHand !== undefined
|
|
104
|
-
? { kind: "not-anchored", reason: input.wiringByHand }
|
|
105
|
-
: appModule === undefined
|
|
106
|
-
? {
|
|
107
|
-
kind: "not-anchored",
|
|
108
|
-
reason: `there is no ${APP_MODULE}`,
|
|
109
|
-
}
|
|
110
|
-
: spliceAppModule(appModule, wiring);
|
|
111
|
-
let instructions;
|
|
112
|
-
if (edit.kind === "edited") {
|
|
113
|
-
keeping.push(...writeOf(APP_MODULE, appModule, edit.text));
|
|
114
|
-
replacing.push(...writeOf(APP_MODULE, appModule, edit.text));
|
|
115
|
-
const spec = input.read(E2E_SPEC);
|
|
116
|
-
const tested = spec === undefined ? undefined : spliceContractTest(spec, SCAFFOLD_API);
|
|
117
|
-
if (tested !== undefined) {
|
|
118
|
-
keeping.push(...writeOf(E2E_SPEC, spec, tested));
|
|
119
|
-
replacing.push(...writeOf(E2E_SPEC, spec, tested));
|
|
120
|
-
}
|
|
61
|
+
builder.file(file.path, file.content);
|
|
121
62
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
instructions = [instructions, mainCorsInstructions()]
|
|
139
|
-
.filter((text) => text !== undefined)
|
|
140
|
-
.join("\n");
|
|
141
|
-
warnings.push(`${MAIN_FILE} was not edited: ${cors.reason}; add CORS by hand, as printed below`);
|
|
142
|
-
}
|
|
143
|
-
const corsWanted = cors.kind !== "configured";
|
|
63
|
+
const host = hosted.host === "nest"
|
|
64
|
+
? planNestHost(builder, {
|
|
65
|
+
inspection,
|
|
66
|
+
scaffold: hosted.scaffold,
|
|
67
|
+
packageManager,
|
|
68
|
+
aventaraVersion: input.aventaraVersion,
|
|
69
|
+
...(hosted.wiringByHand === undefined
|
|
70
|
+
? {}
|
|
71
|
+
: { wiringByHand: hosted.wiringByHand }),
|
|
72
|
+
})
|
|
73
|
+
: planNextHost(builder, {
|
|
74
|
+
inspection,
|
|
75
|
+
packageManager,
|
|
76
|
+
aventaraVersion: input.aventaraVersion,
|
|
77
|
+
externalPackages: [CORE, input.entry.adapterPackage],
|
|
78
|
+
});
|
|
144
79
|
const scaffoldWritesEnv = Object.keys(scaffold.env).length > 0;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
both("package.json", inspection.manifestText, (replace) => mergePackageManifest(inspection.manifestText, {
|
|
157
|
-
dependencies: { ...versions, ...scaffold.dependencies },
|
|
158
|
-
devDependencies: scaffold.devDependencies,
|
|
80
|
+
builder.both("package.json", inspection.manifestText, (replace) => mergePackageManifest(inspection.manifestText, {
|
|
81
|
+
dependencies: {
|
|
82
|
+
[CORE]: input.aventaraVersion,
|
|
83
|
+
...host.dependencies,
|
|
84
|
+
[input.entry.adapterPackage]: input.aventaraVersion,
|
|
85
|
+
...scaffold.dependencies,
|
|
86
|
+
},
|
|
87
|
+
devDependencies: {
|
|
88
|
+
[BUILD]: input.aventaraVersion,
|
|
89
|
+
...scaffold.devDependencies,
|
|
90
|
+
},
|
|
159
91
|
scripts: {
|
|
160
|
-
[PREPARE_SCRIPT]: scaffold.prepare
|
|
92
|
+
[PREPARE_SCRIPT]: `${scaffold.prepare} && ${host.build}`,
|
|
161
93
|
postinstall: `${packageManager} run ${PREPARE_SCRIPT}`,
|
|
162
|
-
...
|
|
163
|
-
`pre${script}`,
|
|
164
|
-
`${packageManager} run ${PREPARE_SCRIPT}`,
|
|
165
|
-
])),
|
|
94
|
+
...host.scripts,
|
|
166
95
|
},
|
|
167
|
-
rewrites:
|
|
168
|
-
? {}
|
|
169
|
-
: {
|
|
170
|
-
...START_SCRIPTS,
|
|
171
|
-
"test:e2e": E2E_SCRIPTS[inspection.moduleKind],
|
|
172
|
-
},
|
|
96
|
+
rewrites: host.rewrites,
|
|
173
97
|
}, replace));
|
|
174
|
-
|
|
175
|
-
|
|
98
|
+
const env = input.read(".env");
|
|
99
|
+
const { envEntry } = host;
|
|
100
|
+
if (scaffoldWritesEnv || (envEntry !== undefined && env !== undefined)) {
|
|
101
|
+
builder.both(".env", env, (replace) => {
|
|
176
102
|
const merged = scaffoldWritesEnv
|
|
177
103
|
? mergeEnvFile(env, scaffold.env, replace)
|
|
178
104
|
: { text: env ?? "", conflicts: [] };
|
|
179
|
-
return
|
|
180
|
-
?
|
|
181
|
-
|
|
105
|
+
return envEntry === undefined
|
|
106
|
+
? merged
|
|
107
|
+
: {
|
|
108
|
+
text: appendEnvEntryIfAbsent(merged.text, envEntry.key, envEntry.value),
|
|
182
109
|
conflicts: merged.conflicts,
|
|
183
|
-
}
|
|
184
|
-
: merged;
|
|
110
|
+
};
|
|
185
111
|
});
|
|
186
112
|
}
|
|
187
113
|
const gitignore = input.read(".gitignore");
|
|
188
|
-
both(".gitignore", gitignore, () => mergeGitignore(gitignore, [
|
|
114
|
+
builder.both(".gitignore", gitignore, () => mergeGitignore(gitignore, [
|
|
189
115
|
...scaffold.gitignore,
|
|
116
|
+
...host.gitignore,
|
|
190
117
|
...(scaffoldWritesEnv ? [".env"] : []),
|
|
191
118
|
]));
|
|
192
119
|
if (packageManager === "pnpm" && scaffold.nativeBuilds.length > 0) {
|
|
193
120
|
const workspace = input.read("pnpm-workspace.yaml");
|
|
194
|
-
both("pnpm-workspace.yaml", workspace, () => mergePnpmWorkspace(workspace, scaffold.nativeBuilds));
|
|
121
|
+
builder.both("pnpm-workspace.yaml", workspace, () => mergePnpmWorkspace(workspace, scaffold.nativeBuilds));
|
|
195
122
|
}
|
|
123
|
+
const skill = planAgentSkill({
|
|
124
|
+
read: input.read,
|
|
125
|
+
coreVersion: input.aventaraVersion,
|
|
126
|
+
force: false,
|
|
127
|
+
});
|
|
128
|
+
keeping.push(...skill.writes);
|
|
129
|
+
replacing.push(...skill.writes);
|
|
130
|
+
warnings.push(...skill.warnings);
|
|
196
131
|
const run = packageManager === "pnpm" ? "pnpm" : "npm";
|
|
197
132
|
return {
|
|
198
133
|
keeping,
|
|
199
134
|
replacing,
|
|
200
135
|
conflicts: [...conflicts],
|
|
201
136
|
warnings,
|
|
202
|
-
instructions,
|
|
137
|
+
instructions: instructions.length === 0 ? undefined : instructions.join("\n"),
|
|
203
138
|
nextSteps: [
|
|
204
139
|
...(input.skipInstall
|
|
205
140
|
? [`Install, which also runs ${PREPARE_SCRIPT}: ${run} install`]
|
|
206
141
|
: []),
|
|
207
142
|
...scaffold.nextSteps,
|
|
208
|
-
...
|
|
209
|
-
|
|
210
|
-
"The start scripts were left as they are: @nestjs/config loads .env, so make sure it does before Prisma connects.",
|
|
211
|
-
]
|
|
212
|
-
: []),
|
|
213
|
-
...corsStep,
|
|
214
|
-
`Start the server: ${run} run start:dev`,
|
|
215
|
-
`In your frontend: ${frontendInitCommand(packageManager, input.aventaraVersion)}`,
|
|
143
|
+
...host.nextSteps,
|
|
144
|
+
`Call the API with curl: see "Calling the API over HTTP" in ${SKILL_DIRECTORY}/SKILL.md`,
|
|
216
145
|
],
|
|
217
146
|
};
|
|
218
147
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { frameworkMajorOf, HOSTS, } from "../catalog/host-entry.interface.js";
|
|
3
4
|
import { lowestMajorOf } from "../catalog/range.reader.js";
|
|
4
5
|
export class AvProjectRefusedError extends Error {
|
|
5
6
|
name = "AvProjectRefusedError";
|
|
6
7
|
}
|
|
7
|
-
const NEST_MAJOR = 12;
|
|
8
8
|
const FIRST_UNSUPPORTED_TYPESCRIPT = 7;
|
|
9
9
|
export const APP_MODULE = "src/app.module.ts";
|
|
10
|
+
const NEXT_APP_DIRECTORY = "src/app";
|
|
11
|
+
const SOURCE_ALIAS = /"@\/\*"\s*:\s*\[\s*"\.\/src\/\*"\s*\]/;
|
|
10
12
|
const LOCKFILES = [
|
|
11
13
|
["package-lock.json", "npm"],
|
|
12
14
|
["pnpm-lock.yaml", "pnpm"],
|
|
@@ -20,36 +22,72 @@ function matchesFamily(pattern, name) {
|
|
|
20
22
|
? name.startsWith(pattern.slice(0, -1))
|
|
21
23
|
: name === pattern;
|
|
22
24
|
}
|
|
25
|
+
function servedProjects() {
|
|
26
|
+
const named = HOSTS.map((host) => `${host.label} ${frameworkMajorOf(host)}`);
|
|
27
|
+
return named.length <= 1
|
|
28
|
+
? named.join("")
|
|
29
|
+
: `${named.slice(0, -1).join(", ")} or ${named[named.length - 1]}`;
|
|
30
|
+
}
|
|
31
|
+
function detectHost(manifest) {
|
|
32
|
+
const found = HOSTS.filter((host) => dependencyRange(manifest, host.framework.package) !== undefined);
|
|
33
|
+
const frameworks = HOSTS.map((host) => host.framework.package);
|
|
34
|
+
if (found.length === 0) {
|
|
35
|
+
throw new AvProjectRefusedError(`aventara init runs in a ${servedProjects()} project, and this package.json depends on ${frameworks.length === 2
|
|
36
|
+
? `neither ${frameworks[0]} nor ${frameworks[1]}`
|
|
37
|
+
: `none of ${frameworks.join(", ")}`}`);
|
|
38
|
+
}
|
|
39
|
+
if (found.length > 1) {
|
|
40
|
+
throw new AvProjectRefusedError(`this package.json depends on both ${found.map((host) => host.framework.package).join(" and ")}, so aventara init cannot tell whether it is a ${found.map((host) => host.label).join(" or a ")} project; Aventara is hosted by one of them`);
|
|
41
|
+
}
|
|
42
|
+
return found[0];
|
|
43
|
+
}
|
|
44
|
+
const HOST_REFUSALS = {
|
|
45
|
+
nest: (directory) => {
|
|
46
|
+
const appModule = path.join(directory, APP_MODULE);
|
|
47
|
+
if (existsSync(appModule) &&
|
|
48
|
+
/\bAventaraModule\b/.test(readFileSync(appModule, "utf8"))) {
|
|
49
|
+
throw new AvProjectRefusedError(`this project is already initialized: ${APP_MODULE} uses AventaraModule`);
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
next: (directory, manifest, host) => {
|
|
53
|
+
if (dependencyRange(manifest, host.package) !== undefined) {
|
|
54
|
+
throw new AvProjectRefusedError(`this project is already initialized: package.json depends on ${host.package}`);
|
|
55
|
+
}
|
|
56
|
+
if (!existsSync(path.join(directory, NEXT_APP_DIRECTORY))) {
|
|
57
|
+
throw new AvProjectRefusedError(`aventara init serves a Next.js app built with the App Router in ${NEXT_APP_DIRECTORY}/, and this project has no ${NEXT_APP_DIRECTORY}/ (the Pages Router, and an app/ outside src/, are not served)`);
|
|
58
|
+
}
|
|
59
|
+
const tsconfig = path.join(directory, "tsconfig.json");
|
|
60
|
+
if (!existsSync(tsconfig) ||
|
|
61
|
+
!SOURCE_ALIAS.test(readFileSync(tsconfig, "utf8"))) {
|
|
62
|
+
throw new AvProjectRefusedError('the scaffold imports its server entry as @/aventara/server, and this project\'s tsconfig.json maps no "@/*" to "./src/*"; add "paths": { "@/*": ["./src/*"] } to its compilerOptions, as create-next-app writes it');
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
23
66
|
export function inspectProject(directory, catalog) {
|
|
24
67
|
const manifestPath = path.join(directory, "package.json");
|
|
25
68
|
if (!existsSync(manifestPath)) {
|
|
26
|
-
throw new AvProjectRefusedError(`aventara init runs in a
|
|
69
|
+
throw new AvProjectRefusedError(`aventara init runs in a ${servedProjects()} project, and there is no package.json in ${directory}`);
|
|
27
70
|
}
|
|
28
71
|
const manifestText = readFileSync(manifestPath, "utf8");
|
|
29
72
|
const manifest = JSON.parse(manifestText);
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
throw new AvProjectRefusedError(`aventara init supports NestJS ${NEST_MAJOR} only, and this project depends on @nestjs/core ${nest}`);
|
|
73
|
+
const host = detectHost(manifest);
|
|
74
|
+
const declared = dependencyRange(manifest, host.framework.package);
|
|
75
|
+
const major = frameworkMajorOf(host);
|
|
76
|
+
if (lowestMajorOf(declared) !== major) {
|
|
77
|
+
throw new AvProjectRefusedError(`aventara init supports ${host.label} ${major} only, and this project depends on ${host.framework.package} ${declared}`);
|
|
36
78
|
}
|
|
37
79
|
const typescript = dependencyRange(manifest, "typescript");
|
|
38
80
|
const typescriptMajor = typescript === undefined ? undefined : lowestMajorOf(typescript);
|
|
39
81
|
if (typescriptMajor !== undefined &&
|
|
40
82
|
typescriptMajor >= FIRST_UNSUPPORTED_TYPESCRIPT) {
|
|
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
|
|
83
|
+
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 5.7 to 6`);
|
|
42
84
|
}
|
|
43
85
|
const lockfiles = LOCKFILES.filter(([file]) => existsSync(path.join(directory, file))).map(([, manager]) => manager);
|
|
44
86
|
const known = lockfiles.filter((manager) => manager !== "yarn");
|
|
45
87
|
if (lockfiles.includes("yarn") && known.length === 0) {
|
|
46
88
|
throw new AvProjectRefusedError("this project has only a yarn.lock, and aventara init supports npm and pnpm");
|
|
47
89
|
}
|
|
48
|
-
|
|
49
|
-
if (existsSync(appModule) &&
|
|
50
|
-
/\bAventaraModule\b/.test(readFileSync(appModule, "utf8"))) {
|
|
51
|
-
throw new AvProjectRefusedError(`this project is already initialized: ${APP_MODULE} uses AventaraModule`);
|
|
52
|
-
}
|
|
90
|
+
HOST_REFUSALS[host.alias](directory, manifest, host);
|
|
53
91
|
const dependencies = [
|
|
54
92
|
...Object.keys(manifest.dependencies ?? {}),
|
|
55
93
|
...Object.keys(manifest.devDependencies ?? {}),
|
|
@@ -64,6 +102,7 @@ export function inspectProject(directory, catalog) {
|
|
|
64
102
|
};
|
|
65
103
|
return {
|
|
66
104
|
directory,
|
|
105
|
+
host,
|
|
67
106
|
manifest,
|
|
68
107
|
manifestText,
|
|
69
108
|
moduleKind: manifest.type === "module" ? "esm" : "cjs",
|
|
@@ -1,22 +1,40 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
export const runCommand = (command, args, options) =>
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
export const runCommand = (command, args, options) => options.interactive === true
|
|
3
|
+
? runInteractive(command, args, options.cwd)
|
|
4
|
+
: runPiped(command, args, options);
|
|
5
|
+
function runInteractive(command, args, cwd) {
|
|
6
|
+
return new Promise((resolve, reject) => {
|
|
7
|
+
const child = spawn(command, args, {
|
|
8
|
+
cwd,
|
|
9
|
+
stdio: "inherit",
|
|
10
|
+
env: process.env,
|
|
11
|
+
});
|
|
12
|
+
child.once("error", reject);
|
|
13
|
+
child.once("close", (code) => {
|
|
14
|
+
resolve({ code: code ?? 1, stdout: "", stderr: "" });
|
|
15
|
+
});
|
|
7
16
|
});
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
17
|
+
}
|
|
18
|
+
function runPiped(command, args, options) {
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const child = spawn(command, args, {
|
|
21
|
+
cwd: options.cwd,
|
|
22
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
23
|
+
env: process.env,
|
|
24
|
+
});
|
|
25
|
+
let stdout = "";
|
|
26
|
+
let stderr = "";
|
|
27
|
+
child.stdout.setEncoding("utf8").on("data", (chunk) => {
|
|
28
|
+
stdout += chunk;
|
|
29
|
+
options.onOutput?.(chunk);
|
|
30
|
+
});
|
|
31
|
+
child.stderr.setEncoding("utf8").on("data", (chunk) => {
|
|
32
|
+
stderr += chunk;
|
|
33
|
+
options.onOutput?.(chunk);
|
|
34
|
+
});
|
|
35
|
+
child.once("error", reject);
|
|
36
|
+
child.once("close", (code) => {
|
|
37
|
+
resolve({ code: code ?? 1, stdout, stderr });
|
|
38
|
+
});
|
|
13
39
|
});
|
|
14
|
-
|
|
15
|
-
stderr += chunk;
|
|
16
|
-
options.onOutput?.(chunk);
|
|
17
|
-
});
|
|
18
|
-
child.once("error", reject);
|
|
19
|
-
child.once("close", (code) => {
|
|
20
|
-
resolve({ code: code ?? 1, stdout, stderr });
|
|
21
|
-
});
|
|
22
|
-
});
|
|
40
|
+
}
|
|
@@ -6,7 +6,7 @@ import { planInit } from "../plan/project.planner.js";
|
|
|
6
6
|
import { detectPackageManager } from "../project/package-manager.detector.js";
|
|
7
7
|
import { AvProjectRefusedError, inspectProject, } from "../project/project.inspector.js";
|
|
8
8
|
import { scanServices } from "../project/service.detector.js";
|
|
9
|
-
import { templatesFor
|
|
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
12
|
import { createStepReporter, } from "./step.reporter.js";
|
|
@@ -39,18 +39,15 @@ export async function initProject(command, io) {
|
|
|
39
39
|
const prepared = inspection.orm.kind === "present"
|
|
40
40
|
? await prepareReuse(command, io, inspection, detectedPackageManager)
|
|
41
41
|
: await prepareFresh(command, io, inspection, detectedPackageManager);
|
|
42
|
-
const { entry, provider,
|
|
42
|
+
const { entry, provider, hosted, packageManager } = prepared;
|
|
43
43
|
const plan = planInit({
|
|
44
44
|
inspection,
|
|
45
45
|
entry,
|
|
46
|
-
|
|
46
|
+
hosted,
|
|
47
47
|
packageManager,
|
|
48
48
|
aventaraVersion: io.aventaraVersion,
|
|
49
49
|
skipInstall: command.skipInstall,
|
|
50
50
|
read,
|
|
51
|
-
...(prepared.wiringByHand === undefined
|
|
52
|
-
? {}
|
|
53
|
-
: { wiringByHand: prepared.wiringByHand }),
|
|
54
51
|
warnings: prepared.warnings,
|
|
55
52
|
});
|
|
56
53
|
await confirmConflicts(plan.conflicts, {
|
|
@@ -105,16 +102,29 @@ async function prepareFresh(command, io, inspection, detectedPackageManager) {
|
|
|
105
102
|
if (entry === undefined || db === undefined || packageManager === undefined) {
|
|
106
103
|
throw new Error("the init questions settled without an ORM, database and package manager");
|
|
107
104
|
}
|
|
105
|
+
const { hosts } = templatesFor(entry);
|
|
108
106
|
return {
|
|
109
107
|
entry,
|
|
110
108
|
provider: db,
|
|
111
109
|
packageManager,
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
hosted: inspection.host.alias === "nest"
|
|
111
|
+
? {
|
|
112
|
+
host: "nest",
|
|
113
|
+
scaffold: hosts.nest.scaffold({
|
|
114
|
+
entry,
|
|
115
|
+
provider: db,
|
|
116
|
+
moduleKind: inspection.moduleKind,
|
|
117
|
+
packageManager,
|
|
118
|
+
}),
|
|
119
|
+
}
|
|
120
|
+
: {
|
|
121
|
+
host: "next",
|
|
122
|
+
scaffold: hosts.next.scaffold({
|
|
123
|
+
entry,
|
|
124
|
+
provider: db,
|
|
125
|
+
packageManager,
|
|
126
|
+
}),
|
|
127
|
+
},
|
|
118
128
|
warnings: [],
|
|
119
129
|
};
|
|
120
130
|
}
|
|
@@ -134,6 +144,10 @@ function installedVersion(directory, name) {
|
|
|
134
144
|
}
|
|
135
145
|
}
|
|
136
146
|
async function prepareReuse(command, io, inspection, detectedPackageManager) {
|
|
147
|
+
if (inspection.host.alias !== "nest") {
|
|
148
|
+
const orm = inspection.orm;
|
|
149
|
+
throw new AvProjectRefusedError(`this ${inspection.host.label} project already uses ${orm.families.join(" and ")} (${orm.packages.join(", ")}), and aventara init sets an ORM up in a ${inspection.host.label} project that has none; it reuses an existing setup in a NestJS project only`);
|
|
150
|
+
}
|
|
137
151
|
const dependencies = [
|
|
138
152
|
...Object.keys(inspection.manifest.dependencies ?? {}),
|
|
139
153
|
...Object.keys(inspection.manifest.devDependencies ?? {}),
|
|
@@ -176,9 +190,9 @@ async function prepareReuse(command, io, inspection, detectedPackageManager) {
|
|
|
176
190
|
services: {
|
|
177
191
|
scan,
|
|
178
192
|
clientClass: templates.clientShape(facts).clientClass,
|
|
179
|
-
newService: templates.newService.service,
|
|
180
|
-
newModule: templates.newService.module,
|
|
181
|
-
newServiceFileExists: existsSync(path.join(io.directory, templates.newService.service.file)),
|
|
193
|
+
newService: templates.hosts.nest.newService.service,
|
|
194
|
+
newModule: templates.hosts.nest.newService.module,
|
|
195
|
+
newServiceFileExists: existsSync(path.join(io.directory, templates.hosts.nest.newService.service.file)),
|
|
182
196
|
label: entry.orm.label,
|
|
183
197
|
},
|
|
184
198
|
},
|
|
@@ -202,28 +216,31 @@ async function prepareReuse(command, io, inspection, detectedPackageManager) {
|
|
|
202
216
|
...(service.clientMember === undefined
|
|
203
217
|
? []
|
|
204
218
|
: [
|
|
205
|
-
`${service.file}#${service.exportName} holds its client in .${service.clientMember} instead of extending ${templates.clientShape(facts).clientClass}, so
|
|
219
|
+
`${service.file}#${service.exportName} holds its client in .${service.clientMember} instead of extending ${templates.clientShape(facts).clientClass}, so app.module.ts passes ${service.exportName}.${service.clientMember} to the adapter`,
|
|
206
220
|
]),
|
|
207
221
|
];
|
|
208
222
|
return {
|
|
209
223
|
entry,
|
|
210
224
|
provider,
|
|
211
225
|
packageManager,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
? {}
|
|
224
|
-
: {
|
|
225
|
-
wiringByHand: `${service.exportName} holds its ${entry.orm.label} client instead of extending it`,
|
|
226
|
+
hosted: {
|
|
227
|
+
host: "nest",
|
|
228
|
+
scaffold: templates.hosts.nest.reuse({
|
|
229
|
+
entry,
|
|
230
|
+
facts: { ...facts, provider, clientOutput: facts.clientOutput },
|
|
231
|
+
moduleKind: inspection.moduleKind,
|
|
232
|
+
packageManager,
|
|
233
|
+
service,
|
|
234
|
+
module,
|
|
235
|
+
writeService: service.isNew,
|
|
236
|
+
artifact,
|
|
226
237
|
}),
|
|
238
|
+
...(service.clientMember === undefined
|
|
239
|
+
? {}
|
|
240
|
+
: {
|
|
241
|
+
wiringByHand: `${service.exportName} holds its ${entry.orm.label} client instead of extending it`,
|
|
242
|
+
}),
|
|
243
|
+
},
|
|
227
244
|
warnings,
|
|
228
245
|
};
|
|
229
246
|
}
|