@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.
@@ -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 ProjectRefusedError extends Error {
5
- name = "ProjectRefusedError";
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 ProjectRefusedError(`aventara init runs in a NestJS ${NEST_MAJOR} project, and there is no package.json in ${directory}`);
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 nest = dependencyRange(manifest, "@nestjs/core");
31
- if (nest === undefined) {
32
- throw new ProjectRefusedError(`aventara init runs in a NestJS ${NEST_MAJOR} project, and this package.json has no @nestjs/core dependency`);
33
- }
34
- if (lowestMajorOf(nest) !== NEST_MAJOR) {
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 ProjectRefusedError(`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`);
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 ProjectRefusedError("this project has only a yarn.lock, and aventara init supports npm and pnpm");
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) => new Promise((resolve, reject) => {
3
- const child = spawn(command, args, {
4
- cwd: options.cwd,
5
- stdio: ["ignore", "pipe", "pipe"],
6
- env: process.env,
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
- let stdout = "";
9
- let stderr = "";
10
- child.stdout.setEncoding("utf8").on("data", (chunk) => {
11
- stdout += chunk;
12
- options.onOutput?.(chunk);
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
- child.stderr.setEncoding("utf8").on("data", (chunk) => {
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 { inspectProject, ProjectRefusedError, } from "../project/project.inspector.js";
7
+ import { AvProjectRefusedError, inspectProject, } from "../project/project.inspector.js";
8
8
  import { scanServices } from "../project/service.detector.js";
9
- import { templatesFor, } from "../templates/template.registry.js";
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 InstallFailedError extends Error {
17
- name = "InstallFailedError";
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, scaffold, packageManager } = prepared;
42
+ const { entry, provider, hosted, packageManager } = prepared;
43
43
  const plan = planInit({
44
44
  inspection,
45
45
  entry,
46
- scaffold,
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 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()}`);
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
- scaffold: templatesFor(entry).scaffold({
113
- entry,
114
- provider: db,
115
- moduleKind: inspection.moduleKind,
116
- packageManager,
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 ProjectRefusedError(verdict.sentence);
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 ProjectRefusedError(problem);
168
+ throw new AvProjectRefusedError(problem);
155
169
  }
156
170
  const provider = facts.provider;
157
171
  if (facts.clientOutput === undefined) {
158
- throw new ProjectRefusedError(`this project's ${facts.generator} generator block in ${facts.schema} has no output, and the generate step reads the client from it; give it one, such as ../src/generated/${entry.orm.family}`);
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 ProjectRefusedError(`${flag} ${given} does not match this project, which already uses ${found}`);
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 src/aventara.config.ts passes ${service.exportName}.${service.clientMember}`,
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
- scaffold: templates.reuse({
213
- entry,
214
- facts: { ...facts, provider, clientOutput: facts.clientOutput },
215
- moduleKind: inspection.moduleKind,
216
- packageManager,
217
- service,
218
- module,
219
- writeService: service.isNew,
220
- artifact,
221
- }),
222
- ...(service.clientMember === undefined
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 { ProjectRefusedError } from "../project/project.inspector.js";
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 { InstallFailedError, initProject, printNextSteps, } from "./init.orchestrator.js";
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
- export const NEST_CLI = "@nestjs/cli@12.0.8";
12
- export const NEST_APP_MODULE_SHA256 = "fe98c4cd2552342272494652287c02e4ef58db3072fcb18513ea831c60f85778";
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 ProjectRefusedError(`aventara new creates ${directory}, which already exists and is not empty; choose another name, or run aventara init inside an existing project`);
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: 2 + (command.skipInstall ? 0 : 1) + (command.skipGit ? 0 : 1),
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
- await progress.step(`Creating the NestJS project (${runner} ${[...prefix, NEST_CLI, "new", name].join(" ")})`, async () => {
61
- const created = await io.runner(runner, args, {
130
+ const generate = async () => {
131
+ const created = await io.runner(generation.command, generation.args, {
62
132
  cwd: io.cwd,
63
- onOutput: progress.output,
133
+ ...(generation.interactive
134
+ ? { interactive: true }
135
+ : { onOutput: progress.output }),
64
136
  });
65
137
  if (created.code !== 0) {
66
- throw new InstallFailedError(`\`${runner} ${args.join(" ")}\` exited with code ${created.code}, and nothing of Aventara's was written; fix what it reports and run aventara new again:\n${created.stderr.trimEnd()}`);
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
- const appModule = path.join(directory, "src", "app.module.ts");
70
- const digest = existsSync(appModule)
71
- ? createHash("sha256").update(readFileSync(appModule)).digest("hex")
72
- : "(absent)";
73
- if (digest !== NEST_APP_MODULE_SHA256) {
74
- throw new InstallFailedError(`nest new's output changed: ${appModule} is not what ${NEST_CLI} writes (sha256 ${digest}), so nothing of Aventara's was written over it; run aventara init inside ${directory} to add Aventara to it`);
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
- export const INITIAL_COMMIT_MESSAGE = "Initial Aventara Scaffold";
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
+ }