@aventara/cli 1.0.0-rc.0 → 1.0.0-rc.10
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 +461 -0
- package/README.md +101 -44
- package/dist/apply/conflict.confirmer.js +5 -5
- 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 +29 -16
- package/dist/command/command.parser.js +99 -13
- 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 -167
- package/dist/project/project.inspector.js +56 -17
- package/dist/run/command.runner.js +37 -19
- package/dist/run/init.orchestrator.js +55 -38
- package/dist/run/new.orchestrator.js +105 -30
- package/dist/run/scaffold.committer.js +1 -1
- package/dist/run/skill.orchestrator.js +54 -0
- package/dist/skill/agent-skill.generated.js +7 -0
- package/dist/skill/agent-skill.planner.js +116 -0
- package/dist/templates/docs.links.js +2 -1
- 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 +189 -0
- package/dist/templates/prisma7/prisma7-orm.templates.js +180 -0
- package/dist/templates/prisma7/prisma7.templates.js +7 -365
- package/dist/templates/template.registry.js +2 -2
- package/dist/wizard/answer.resolver.js +5 -5
- package/dist/wizard/readline.prompter.js +3 -3
- package/dist/wizard/wizard.questions.js +11 -11
- package/package.json +3 -3
|
@@ -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
|
-
export class
|
|
5
|
-
name = "
|
|
5
|
+
export class AvProjectRefusedError extends Error {
|
|
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
|
|
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 ProjectRefusedError(`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
|
|
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
|
-
throw new
|
|
47
|
-
}
|
|
48
|
-
const appModule = path.join(directory, APP_MODULE);
|
|
49
|
-
if (existsSync(appModule) &&
|
|
50
|
-
/\bAventaraModule\b/.test(readFileSync(appModule, "utf8"))) {
|
|
51
|
-
throw new ProjectRefusedError(`this project is already initialized: ${APP_MODULE} uses AventaraModule`);
|
|
88
|
+
throw new AvProjectRefusedError("this project has only a yarn.lock, and aventara init supports npm and pnpm");
|
|
52
89
|
}
|
|
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
|
+
}
|
|
@@ -4,17 +4,17 @@ 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
|
-
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";
|
|
13
13
|
export function printNextSteps(stdout, steps) {
|
|
14
14
|
stdout(`Next:\n${steps.map((step, index) => ` ${index + 1}. ${step}`).join("\n")}\n`);
|
|
15
15
|
}
|
|
16
|
-
export class
|
|
17
|
-
name = "
|
|
16
|
+
export class AvInstallFailedError extends Error {
|
|
17
|
+
name = "AvInstallFailedError";
|
|
18
18
|
}
|
|
19
19
|
export async function runInit(command, io) {
|
|
20
20
|
const { progress, nextSteps } = await initProject(command, io);
|
|
@@ -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, {
|
|
@@ -88,7 +85,7 @@ export async function initProject(command, io) {
|
|
|
88
85
|
onOutput: progress.output,
|
|
89
86
|
});
|
|
90
87
|
if (installed.code !== 0) {
|
|
91
|
-
throw new
|
|
88
|
+
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
89
|
}
|
|
93
90
|
});
|
|
94
91
|
}
|
|
@@ -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 ?? {}),
|
|
@@ -144,25 +158,25 @@ async function prepareReuse(command, io, inspection, detectedPackageManager) {
|
|
|
144
158
|
installCommand: `${detectedPackageManager} install`,
|
|
145
159
|
});
|
|
146
160
|
if (verdict.kind === "refused") {
|
|
147
|
-
throw new
|
|
161
|
+
throw new AvProjectRefusedError(verdict.sentence);
|
|
148
162
|
}
|
|
149
163
|
const { entry } = verdict;
|
|
150
164
|
const templates = templatesFor(entry);
|
|
151
165
|
const facts = templates.readProject(io.directory, inspection.manifest);
|
|
152
166
|
const problem = matchOrmSetup(entry, facts);
|
|
153
167
|
if (problem !== undefined) {
|
|
154
|
-
throw new
|
|
168
|
+
throw new AvProjectRefusedError(problem);
|
|
155
169
|
}
|
|
156
170
|
const provider = facts.provider;
|
|
157
171
|
if (facts.clientOutput === undefined) {
|
|
158
|
-
throw new
|
|
172
|
+
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}`);
|
|
159
173
|
}
|
|
160
174
|
for (const [flag, given, found] of [
|
|
161
175
|
["--orm", command.given.orm, entry.alias],
|
|
162
176
|
["--db", command.given.db, provider],
|
|
163
177
|
]) {
|
|
164
178
|
if (given !== undefined && given.trim().toLowerCase() !== found) {
|
|
165
|
-
throw new
|
|
179
|
+
throw new AvProjectRefusedError(`${flag} ${given} does not match this project, which already uses ${found}`);
|
|
166
180
|
}
|
|
167
181
|
}
|
|
168
182
|
const scan = scanServices(io.directory, templates.clientShape(facts));
|
|
@@ -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
|
}
|
|
@@ -1,19 +1,93 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { DEFAULT_HOST, hostOf, } from "../catalog/host-entry.interface.js";
|
|
5
|
+
import { AvCliCommandError, } from "../command/command.parser.js";
|
|
4
6
|
import { detectPackageManager } from "../project/package-manager.detector.js";
|
|
5
|
-
import {
|
|
7
|
+
import { AvProjectRefusedError } from "../project/project.inspector.js";
|
|
6
8
|
import { resolveAnswers } from "../wizard/answer.resolver.js";
|
|
7
|
-
import { NEW_QUESTIONS } from "../wizard/wizard.questions.js";
|
|
8
|
-
import {
|
|
9
|
+
import { NEW_QUESTIONS, } from "../wizard/wizard.questions.js";
|
|
10
|
+
import { AvInstallFailedError, initProject, printNextSteps, } from "./init.orchestrator.js";
|
|
9
11
|
import { commitScaffold, gitSurroundings } from "./scaffold.committer.js";
|
|
10
12
|
import { createStepReporter } from "./step.reporter.js";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
const NEST_CLI = "@nestjs/cli@12.0.8";
|
|
14
|
+
const NEST_APP_MODULE_SHA256 = "fe98c4cd2552342272494652287c02e4ef58db3072fcb18513ea831c60f85778";
|
|
15
|
+
const NEXT_VERSION_VARIABLE = "AVENTARA_NEXT_VERSION";
|
|
16
|
+
const NEXT_GENERATOR = "create-next-app";
|
|
17
|
+
const VERSION = /^[0-9A-Za-z][0-9A-Za-z.+-]*$/;
|
|
18
|
+
const NEXT_LAYOUT_FLAGS = [
|
|
19
|
+
"--ts",
|
|
20
|
+
"--app",
|
|
21
|
+
"--src-dir",
|
|
22
|
+
"--import-alias",
|
|
23
|
+
"@/*",
|
|
24
|
+
];
|
|
13
25
|
function oneOff(packageManager) {
|
|
14
26
|
return packageManager === "pnpm" ? ["pnpm", ["dlx"]] : ["npx", ["-y"]];
|
|
15
27
|
}
|
|
28
|
+
const GENERATIONS = {
|
|
29
|
+
nest: ({ command, name, packageManager }) => {
|
|
30
|
+
if (command.passthrough !== undefined) {
|
|
31
|
+
throw new AvCliCommandError("arguments after -- go to create-next-app, so they need --host next; nest new takes only what aventara new passes it");
|
|
32
|
+
}
|
|
33
|
+
const [runner, prefix] = oneOff(packageManager);
|
|
34
|
+
return {
|
|
35
|
+
title: `Creating the NestJS project (${runner} ${[...prefix, NEST_CLI, "new", name].join(" ")})`,
|
|
36
|
+
command: runner,
|
|
37
|
+
args: [
|
|
38
|
+
...prefix,
|
|
39
|
+
NEST_CLI,
|
|
40
|
+
"new",
|
|
41
|
+
name,
|
|
42
|
+
"--package-manager",
|
|
43
|
+
packageManager,
|
|
44
|
+
"--skip-install",
|
|
45
|
+
"--no-observe",
|
|
46
|
+
...(command.skipGit ? ["--skip-git"] : []),
|
|
47
|
+
],
|
|
48
|
+
interactive: false,
|
|
49
|
+
check: (directory) => {
|
|
50
|
+
const appModule = path.join(directory, "src", "app.module.ts");
|
|
51
|
+
const digest = existsSync(appModule)
|
|
52
|
+
? createHash("sha256").update(readFileSync(appModule)).digest("hex")
|
|
53
|
+
: "(absent)";
|
|
54
|
+
if (digest !== NEST_APP_MODULE_SHA256) {
|
|
55
|
+
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`);
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
next: ({ command, io, name, packageManager }) => {
|
|
61
|
+
const pinned = io.env[NEXT_VERSION_VARIABLE];
|
|
62
|
+
if (pinned !== undefined && pinned !== "" && !VERSION.test(pinned)) {
|
|
63
|
+
throw new AvCliCommandError(`${NEXT_VERSION_VARIABLE} is ${JSON.stringify(pinned)}, which is not a version of ${NEXT_GENERATOR}; set it to one such as 16.4.0, or unset it for the latest`);
|
|
64
|
+
}
|
|
65
|
+
if (!command.yes && !io.prompter.interactive) {
|
|
66
|
+
throw new AvCliCommandError(`${NEXT_GENERATOR} asks its own questions, and nobody can answer them here (stdin is not a terminal); pass --yes to take its defaults, and its own flags after --`);
|
|
67
|
+
}
|
|
68
|
+
const generator = `${NEXT_GENERATOR}@${pinned === undefined || pinned === "" ? "latest" : pinned}`;
|
|
69
|
+
const [runner, prefix] = oneOff(packageManager);
|
|
70
|
+
return {
|
|
71
|
+
title: `Creating the Next.js project (${runner} ${[...prefix, generator, name].join(" ")})`,
|
|
72
|
+
command: runner,
|
|
73
|
+
args: [
|
|
74
|
+
...prefix,
|
|
75
|
+
generator,
|
|
76
|
+
name,
|
|
77
|
+
...NEXT_LAYOUT_FLAGS,
|
|
78
|
+
`--use-${packageManager}`,
|
|
79
|
+
"--skip-install",
|
|
80
|
+
...(command.skipGit ? ["--disable-git"] : []),
|
|
81
|
+
...(command.yes ? ["--yes"] : []),
|
|
82
|
+
...(command.passthrough ?? []),
|
|
83
|
+
],
|
|
84
|
+
interactive: !command.yes,
|
|
85
|
+
check: () => { },
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
};
|
|
16
89
|
export async function runNew(command, io) {
|
|
90
|
+
const host = hostOf((command.host ?? DEFAULT_HOST));
|
|
17
91
|
const answers = await resolveAnswers(NEW_QUESTIONS, {
|
|
18
92
|
given: command.given,
|
|
19
93
|
yes: command.yes,
|
|
@@ -32,47 +106,48 @@ export async function runNew(command, io) {
|
|
|
32
106
|
packageManager === undefined) {
|
|
33
107
|
throw new Error("the new questions settled without every answer");
|
|
34
108
|
}
|
|
109
|
+
const generation = GENERATIONS[host.alias]({
|
|
110
|
+
command,
|
|
111
|
+
io,
|
|
112
|
+
name,
|
|
113
|
+
packageManager,
|
|
114
|
+
});
|
|
35
115
|
const directory = path.join(io.cwd, name);
|
|
36
116
|
if (existsSync(directory) && readdirSync(directory).length > 0) {
|
|
37
|
-
throw new
|
|
117
|
+
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`);
|
|
38
118
|
}
|
|
39
119
|
const surroundings = command.skipGit
|
|
40
120
|
? undefined
|
|
41
121
|
: await gitSurroundings(io.runner, io.cwd);
|
|
42
|
-
const [runner, prefix] = oneOff(packageManager);
|
|
43
|
-
const args = [
|
|
44
|
-
...prefix,
|
|
45
|
-
NEST_CLI,
|
|
46
|
-
"new",
|
|
47
|
-
name,
|
|
48
|
-
"--package-manager",
|
|
49
|
-
packageManager,
|
|
50
|
-
"--skip-install",
|
|
51
|
-
"--no-observe",
|
|
52
|
-
...(command.skipGit ? ["--skip-git"] : []),
|
|
53
|
-
];
|
|
54
122
|
const progress = createStepReporter({
|
|
55
123
|
write: io.stderr,
|
|
56
124
|
mode: io.progressMode,
|
|
57
|
-
total:
|
|
125
|
+
total: (generation.interactive ? 1 : 2) +
|
|
126
|
+
(command.skipInstall ? 0 : 1) +
|
|
127
|
+
(command.skipGit ? 0 : 1),
|
|
58
128
|
verbose: command.verbose === true,
|
|
59
129
|
});
|
|
60
|
-
|
|
61
|
-
const created = await io.runner(
|
|
130
|
+
const generate = async () => {
|
|
131
|
+
const created = await io.runner(generation.command, generation.args, {
|
|
62
132
|
cwd: io.cwd,
|
|
63
|
-
|
|
133
|
+
...(generation.interactive
|
|
134
|
+
? { interactive: true }
|
|
135
|
+
: { onOutput: progress.output }),
|
|
64
136
|
});
|
|
65
137
|
if (created.code !== 0) {
|
|
66
|
-
throw new
|
|
138
|
+
throw new AvInstallFailedError(`\`${generation.command} ${generation.args.join(" ")}\` exited with code ${created.code}, and nothing of Aventara's was written; fix what it reports and run aventara new again${generation.interactive
|
|
139
|
+
? " (its output is above)"
|
|
140
|
+
: `:\n${created.stderr.trimEnd()}`}`);
|
|
67
141
|
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
142
|
+
};
|
|
143
|
+
if (generation.interactive) {
|
|
144
|
+
io.stderr(`${generation.title}: it asks its own questions now.\n`);
|
|
145
|
+
await generate();
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
await progress.step(generation.title, generate);
|
|
75
149
|
}
|
|
150
|
+
generation.check(directory);
|
|
76
151
|
const { nextSteps } = await initProject({
|
|
77
152
|
command: "init",
|
|
78
153
|
given: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { realpathSync } from "node:fs";
|
|
2
|
-
|
|
2
|
+
const INITIAL_COMMIT_MESSAGE = "Initial Aventara Scaffold";
|
|
3
3
|
function isMissingCommand(error) {
|
|
4
4
|
return (error instanceof Error &&
|
|
5
5
|
error.code === "ENOENT");
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { planAgentSkill } from "../skill/agent-skill.planner.js";
|
|
4
|
+
export class AvSkillRefusedError extends Error {
|
|
5
|
+
name = "AvSkillRefusedError";
|
|
6
|
+
}
|
|
7
|
+
function readText(file) {
|
|
8
|
+
try {
|
|
9
|
+
return readFileSync(file, "utf8");
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function runSkill(command, io) {
|
|
16
|
+
if (readText(path.join(io.directory, "package.json")) === undefined) {
|
|
17
|
+
throw new AvSkillRefusedError(`there is no package.json in ${io.directory}: run aventara skill in your project's root`);
|
|
18
|
+
}
|
|
19
|
+
const core = path.join(io.directory, "node_modules", "@aventara", "core");
|
|
20
|
+
const manifest = readText(path.join(core, "package.json"));
|
|
21
|
+
if (manifest === undefined) {
|
|
22
|
+
throw new AvSkillRefusedError("@aventara/core is not installed here: install the project's dependencies first, so the skill matches its version");
|
|
23
|
+
}
|
|
24
|
+
const coreVersion = JSON.parse(manifest)
|
|
25
|
+
.version;
|
|
26
|
+
const errors = readText(path.join(core, "ERRORS.md"));
|
|
27
|
+
const warnings = [];
|
|
28
|
+
if (coreVersion !== io.cliVersion) {
|
|
29
|
+
warnings.push(`this is @aventara/cli ${io.cliVersion} and the project has @aventara/core ${coreVersion}; for guidance written for that version run \`npx @aventara/cli@${coreVersion} skill\``);
|
|
30
|
+
}
|
|
31
|
+
if (errors === undefined) {
|
|
32
|
+
warnings.push(`@aventara/core ${coreVersion} ships no ERRORS.md, so reference/errors.md is this CLI's (${io.cliVersion})`);
|
|
33
|
+
}
|
|
34
|
+
const plan = planAgentSkill({
|
|
35
|
+
read: (file) => readText(path.join(io.directory, file)),
|
|
36
|
+
coreVersion,
|
|
37
|
+
...(errors === undefined ? {} : { errors }),
|
|
38
|
+
force: command.force,
|
|
39
|
+
});
|
|
40
|
+
for (const write of plan.writes) {
|
|
41
|
+
const target = path.join(io.directory, write.path);
|
|
42
|
+
mkdirSync(path.dirname(target), { recursive: true });
|
|
43
|
+
writeFileSync(target, write.content);
|
|
44
|
+
}
|
|
45
|
+
for (const warning of [...warnings, ...plan.warnings]) {
|
|
46
|
+
io.stderr(`aventara: warning: ${warning}.\n`);
|
|
47
|
+
}
|
|
48
|
+
io.stdout(plan.writes.length === 0
|
|
49
|
+
? `aventara: the agent skill for @aventara/core ${coreVersion} is up to date${plan.warnings.length === 0 ? "" : ", apart from what the warnings above name"}.\n`
|
|
50
|
+
: `aventara: wrote the agent skill for @aventara/core ${coreVersion}: ${plan.writes
|
|
51
|
+
.map((write) => `${write.change === "create" ? "created" : "updated"} ${write.path}`)
|
|
52
|
+
.join(", ")}.\n`);
|
|
53
|
+
return 0;
|
|
54
|
+
}
|