@aventara/cli 0.1.0-pilot.4 → 1.0.0-rc.1

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