@aventara/cli 1.0.0-rc.8 → 1.0.0-rc.9
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 +39 -0
- package/README.md +41 -3
- package/dist/catalog/host-entry.interface.js +10 -0
- package/dist/catalog/host.catalog.generated.js +32 -0
- package/dist/command/command.parser.js +55 -3
- 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 +81 -166
- package/dist/project/project.inspector.js +52 -13
- package/dist/run/command.runner.js +37 -19
- package/dist/run/init.orchestrator.js +46 -29
- package/dist/run/new.orchestrator.js +100 -25
- package/dist/skill/agent-skill.generated.js +2 -2
- package/dist/templates/next-server.entry.js +5 -0
- package/dist/templates/prisma7/prisma7-nest.templates.js +242 -0
- package/dist/templates/prisma7/prisma7-next.templates.js +182 -0
- package/dist/templates/prisma7/prisma7-orm.templates.js +180 -0
- package/dist/templates/prisma7/prisma7.templates.js +7 -411
- package/package.json +2 -2
|
@@ -1,46 +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
|
-
import {
|
|
8
|
-
|
|
9
|
-
const PREPARED_SCRIPTS = [
|
|
10
|
-
"build",
|
|
11
|
-
"start",
|
|
12
|
-
"start:dev",
|
|
13
|
-
"start:debug",
|
|
14
|
-
];
|
|
15
|
-
const START_SCRIPTS = {
|
|
16
|
-
start: { from: "nest start", to: "nest start --env-file .env" },
|
|
17
|
-
"start:dev": {
|
|
18
|
-
from: "nest start --watch",
|
|
19
|
-
to: "nest start --watch --env-file .env",
|
|
20
|
-
},
|
|
21
|
-
"start:debug": {
|
|
22
|
-
from: "nest start --debug --watch",
|
|
23
|
-
to: "nest start --debug --watch --env-file .env",
|
|
24
|
-
},
|
|
25
|
-
"start:prod": {
|
|
26
|
-
from: "node dist/main",
|
|
27
|
-
to: "node --env-file-if-exists=.env dist/main",
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
const E2E_SCRIPTS = {
|
|
31
|
-
esm: {
|
|
32
|
-
from: "vitest run --config ./vitest.config.e2e.ts",
|
|
33
|
-
to: "node --env-file-if-exists=.env ./node_modules/vitest/vitest.mjs run --config ./vitest.config.e2e.ts",
|
|
34
|
-
},
|
|
35
|
-
cjs: {
|
|
36
|
-
from: "node --experimental-vm-modules ./node_modules/jest/bin/jest.js --config ./test/jest-e2e.json",
|
|
37
|
-
to: "node --env-file-if-exists=.env --experimental-vm-modules ./node_modules/jest/bin/jest.js --config ./test/jest-e2e.json",
|
|
38
|
-
},
|
|
39
|
-
};
|
|
40
|
-
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";
|
|
41
6
|
const CORE = "@aventara/core";
|
|
42
7
|
const BUILD = "@aventara/build";
|
|
43
|
-
const BUILD_BIN = "aventara-build";
|
|
44
8
|
function writeOf(path, before, after) {
|
|
45
9
|
return before === after
|
|
46
10
|
? []
|
|
@@ -52,152 +16,109 @@ function writeOf(path, before, after) {
|
|
|
52
16
|
},
|
|
53
17
|
];
|
|
54
18
|
}
|
|
55
|
-
export function frontendInitCommand(packageManager, cliVersion) {
|
|
56
|
-
const tag = /^\d+\.\d+\.\d+-([0-9A-Za-z-]+)/.exec(cliVersion)?.[1];
|
|
57
|
-
const runner = packageManager === "pnpm" ? "pnpm dlx" : "npx";
|
|
58
|
-
return `${runner} @aventara/client${tag === undefined ? "" : `@${tag}`} init`;
|
|
59
|
-
}
|
|
60
19
|
export function planInit(input) {
|
|
61
|
-
const { inspection,
|
|
20
|
+
const { inspection, hosted, packageManager } = input;
|
|
21
|
+
const { scaffold } = hosted;
|
|
62
22
|
const conflicts = new Set();
|
|
63
23
|
const keeping = [];
|
|
64
24
|
const replacing = [];
|
|
65
25
|
const warnings = [...(input.warnings ?? [])];
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
},
|
|
74
59
|
};
|
|
75
60
|
for (const file of scaffold.files) {
|
|
76
|
-
|
|
77
|
-
if (before === undefined) {
|
|
78
|
-
keeping.push({
|
|
79
|
-
path: file.path,
|
|
80
|
-
content: file.content,
|
|
81
|
-
change: "create",
|
|
82
|
-
});
|
|
83
|
-
replacing.push({
|
|
84
|
-
path: file.path,
|
|
85
|
-
content: file.content,
|
|
86
|
-
change: "create",
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
else if (before !== file.content) {
|
|
90
|
-
conflicts.add(file.path);
|
|
91
|
-
replacing.push({
|
|
92
|
-
path: file.path,
|
|
93
|
-
content: file.content,
|
|
94
|
-
change: "replace",
|
|
95
|
-
});
|
|
96
|
-
}
|
|
61
|
+
builder.file(file.path, file.content);
|
|
97
62
|
}
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
let instructions;
|
|
115
|
-
if (edit.kind === "edited") {
|
|
116
|
-
keeping.push(...writeOf(APP_MODULE, appModule, edit.text));
|
|
117
|
-
replacing.push(...writeOf(APP_MODULE, appModule, edit.text));
|
|
118
|
-
const spec = input.read(E2E_SPEC);
|
|
119
|
-
const tested = spec === undefined ? undefined : spliceContractTest(spec, SCAFFOLD_API);
|
|
120
|
-
if (tested !== undefined) {
|
|
121
|
-
keeping.push(...writeOf(E2E_SPEC, spec, tested));
|
|
122
|
-
replacing.push(...writeOf(E2E_SPEC, spec, tested));
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
instructions = wiringInstructions(wiring);
|
|
127
|
-
warnings.push(`${APP_MODULE} was not edited: ${edit.reason}; add AventaraModule by hand, as printed below`);
|
|
128
|
-
}
|
|
129
|
-
const main = input.read(MAIN_FILE);
|
|
130
|
-
const cors = main === undefined
|
|
131
|
-
? { kind: "not-anchored", reason: `there is no ${MAIN_FILE}` }
|
|
132
|
-
: spliceMainCors(main);
|
|
133
|
-
if (cors.kind === "edited") {
|
|
134
|
-
keeping.push(...writeOf(MAIN_FILE, main, cors.text));
|
|
135
|
-
replacing.push(...writeOf(MAIN_FILE, main, cors.text));
|
|
136
|
-
}
|
|
137
|
-
else if (cors.kind === "configured") {
|
|
138
|
-
warnings.push(`${MAIN_FILE} already configures CORS, so it was left as it is; a browser frontend on another origin is answered only if that configuration allows it`);
|
|
139
|
-
}
|
|
140
|
-
else if (cors.kind === "not-anchored") {
|
|
141
|
-
instructions = [instructions, mainCorsInstructions()]
|
|
142
|
-
.filter((text) => text !== undefined)
|
|
143
|
-
.join("\n");
|
|
144
|
-
warnings.push(`${MAIN_FILE} was not edited: ${cors.reason}; add CORS by hand, as printed below`);
|
|
145
|
-
}
|
|
146
|
-
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
|
+
});
|
|
147
79
|
const scaffoldWritesEnv = Object.keys(scaffold.env).length > 0;
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
[CORE]: input.aventaraVersion,
|
|
156
|
-
[NEST_HOST]: input.aventaraVersion,
|
|
157
|
-
[input.entry.adapterPackage]: input.aventaraVersion,
|
|
158
|
-
};
|
|
159
|
-
both("package.json", inspection.manifestText, (replace) => mergePackageManifest(inspection.manifestText, {
|
|
160
|
-
dependencies: { ...versions, ...scaffold.dependencies },
|
|
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
|
+
},
|
|
161
87
|
devDependencies: {
|
|
162
88
|
[BUILD]: input.aventaraVersion,
|
|
163
89
|
...scaffold.devDependencies,
|
|
164
90
|
},
|
|
165
91
|
scripts: {
|
|
166
|
-
[PREPARE_SCRIPT]: `${scaffold.prepare} && ${
|
|
92
|
+
[PREPARE_SCRIPT]: `${scaffold.prepare} && ${host.build}`,
|
|
167
93
|
postinstall: `${packageManager} run ${PREPARE_SCRIPT}`,
|
|
168
|
-
...
|
|
169
|
-
`pre${script}`,
|
|
170
|
-
`${packageManager} run ${PREPARE_SCRIPT}`,
|
|
171
|
-
])),
|
|
94
|
+
...host.scripts,
|
|
172
95
|
},
|
|
173
|
-
rewrites:
|
|
174
|
-
? {}
|
|
175
|
-
: {
|
|
176
|
-
...START_SCRIPTS,
|
|
177
|
-
"test:e2e": E2E_SCRIPTS[inspection.moduleKind],
|
|
178
|
-
},
|
|
96
|
+
rewrites: host.rewrites,
|
|
179
97
|
}, replace));
|
|
180
|
-
|
|
181
|
-
|
|
98
|
+
const env = input.read(".env");
|
|
99
|
+
const { envEntry } = host;
|
|
100
|
+
if (scaffoldWritesEnv || (envEntry !== undefined && env !== undefined)) {
|
|
101
|
+
builder.both(".env", env, (replace) => {
|
|
182
102
|
const merged = scaffoldWritesEnv
|
|
183
103
|
? mergeEnvFile(env, scaffold.env, replace)
|
|
184
104
|
: { text: env ?? "", conflicts: [] };
|
|
185
|
-
return
|
|
186
|
-
?
|
|
187
|
-
|
|
105
|
+
return envEntry === undefined
|
|
106
|
+
? merged
|
|
107
|
+
: {
|
|
108
|
+
text: appendEnvEntryIfAbsent(merged.text, envEntry.key, envEntry.value),
|
|
188
109
|
conflicts: merged.conflicts,
|
|
189
|
-
}
|
|
190
|
-
: merged;
|
|
110
|
+
};
|
|
191
111
|
});
|
|
192
112
|
}
|
|
193
113
|
const gitignore = input.read(".gitignore");
|
|
194
|
-
both(".gitignore", gitignore, () => mergeGitignore(gitignore, [
|
|
114
|
+
builder.both(".gitignore", gitignore, () => mergeGitignore(gitignore, [
|
|
195
115
|
...scaffold.gitignore,
|
|
116
|
+
...host.gitignore,
|
|
196
117
|
...(scaffoldWritesEnv ? [".env"] : []),
|
|
197
118
|
]));
|
|
198
119
|
if (packageManager === "pnpm" && scaffold.nativeBuilds.length > 0) {
|
|
199
120
|
const workspace = input.read("pnpm-workspace.yaml");
|
|
200
|
-
both("pnpm-workspace.yaml", workspace, () => mergePnpmWorkspace(workspace, scaffold.nativeBuilds));
|
|
121
|
+
builder.both("pnpm-workspace.yaml", workspace, () => mergePnpmWorkspace(workspace, scaffold.nativeBuilds));
|
|
201
122
|
}
|
|
202
123
|
const skill = planAgentSkill({
|
|
203
124
|
read: input.read,
|
|
@@ -213,20 +134,14 @@ export function planInit(input) {
|
|
|
213
134
|
replacing,
|
|
214
135
|
conflicts: [...conflicts],
|
|
215
136
|
warnings,
|
|
216
|
-
instructions,
|
|
137
|
+
instructions: instructions.length === 0 ? undefined : instructions.join("\n"),
|
|
217
138
|
nextSteps: [
|
|
218
139
|
...(input.skipInstall
|
|
219
140
|
? [`Install, which also runs ${PREPARE_SCRIPT}: ${run} install`]
|
|
220
141
|
: []),
|
|
221
142
|
...scaffold.nextSteps,
|
|
222
|
-
...
|
|
223
|
-
|
|
224
|
-
"The start scripts were left as they are: @nestjs/config loads .env, so make sure it does before Prisma connects.",
|
|
225
|
-
]
|
|
226
|
-
: []),
|
|
227
|
-
...corsStep,
|
|
228
|
-
`Start the server: ${run} run start:dev`,
|
|
229
|
-
`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`,
|
|
230
145
|
],
|
|
231
146
|
};
|
|
232
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,19 +22,59 @@ 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);
|
|
@@ -45,11 +87,7 @@ export function inspectProject(directory, catalog) {
|
|
|
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
|
},
|
|
@@ -209,21 +223,24 @@ async function prepareReuse(command, io, inspection, detectedPackageManager) {
|
|
|
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
|
}
|