@aventara/cli 0.0.0-stage → 0.1.0-pilot.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.
- package/LICENSE +91 -0
- package/LICENSE-ADDITIONAL-PERMISSION.md +9 -0
- package/README.md +120 -2
- package/dist/apply/app-module.anchor.d.ts +30 -0
- package/dist/apply/app-module.anchor.js +185 -0
- package/dist/apply/conflict.confirmer.d.ts +24 -0
- package/dist/apply/conflict.confirmer.js +39 -0
- package/dist/apply/e2e-spec.anchor.d.ts +18 -0
- package/dist/apply/e2e-spec.anchor.js +40 -0
- package/dist/apply/manifest.merger.d.ts +47 -0
- package/dist/apply/manifest.merger.js +139 -0
- package/dist/aventara.bin.d.ts +2 -0
- package/dist/aventara.bin.js +15 -0
- package/dist/catalog/adapter.catalog.generated.d.ts +19 -0
- package/dist/catalog/adapter.catalog.generated.js +35 -0
- package/dist/catalog/catalog-entry.interface.d.ts +46 -0
- package/dist/catalog/catalog-entry.interface.js +1 -0
- package/dist/catalog/catalog.matcher.d.ts +46 -0
- package/dist/catalog/catalog.matcher.js +83 -0
- package/dist/catalog/range.reader.d.ts +17 -0
- package/dist/catalog/range.reader.js +77 -0
- package/dist/cli.d.ts +30 -0
- package/dist/cli.js +96 -0
- package/dist/command/command.parser.d.ts +36 -0
- package/dist/command/command.parser.js +163 -0
- package/dist/node-version.guard.d.ts +8 -0
- package/dist/node-version.guard.js +59 -0
- package/dist/plan/project.planner.d.ts +59 -0
- package/dist/plan/project.planner.js +201 -0
- package/dist/project/package-manager.detector.d.ts +12 -0
- package/dist/project/package-manager.detector.js +14 -0
- package/dist/project/project.inspector.d.ts +38 -0
- package/dist/project/project.inspector.js +84 -0
- package/dist/project/service.detector.d.ts +45 -0
- package/dist/project/service.detector.js +106 -0
- package/dist/project/source.scanner.d.ts +16 -0
- package/dist/project/source.scanner.js +134 -0
- package/dist/run/command.runner.d.ts +15 -0
- package/dist/run/command.runner.js +20 -0
- package/dist/run/init.orchestrator.d.ts +29 -0
- package/dist/run/init.orchestrator.js +222 -0
- package/dist/run/new.orchestrator.d.ts +21 -0
- package/dist/run/new.orchestrator.js +88 -0
- package/dist/templates/prisma7/prisma7.templates.d.ts +34 -0
- package/dist/templates/prisma7/prisma7.templates.js +397 -0
- package/dist/templates/template.registry.d.ts +156 -0
- package/dist/templates/template.registry.js +17 -0
- package/dist/wizard/answer.resolver.d.ts +49 -0
- package/dist/wizard/answer.resolver.js +106 -0
- package/dist/wizard/readline.prompter.d.ts +24 -0
- package/dist/wizard/readline.prompter.js +76 -0
- package/dist/wizard/wizard.questions.d.ts +114 -0
- package/dist/wizard/wizard.questions.js +261 -0
- package/package.json +28 -3
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { PRISMA7_TEMPLATES } from "./prisma7/prisma7.templates.js";
|
|
2
|
+
/**
|
|
3
|
+
* The entrypoint every scaffold mounts the protocol at: written into
|
|
4
|
+
* `src/aventara.config.ts`, and tested by the e2e spec (pilot.1).
|
|
5
|
+
*/
|
|
6
|
+
export const SCAFFOLD_ENTRYPOINT = "/api";
|
|
7
|
+
export const TEMPLATE_REGISTRY = {
|
|
8
|
+
prisma7: PRISMA7_TEMPLATES,
|
|
9
|
+
};
|
|
10
|
+
/** The template set for `entry` — every catalog entry has one (checked at build). */
|
|
11
|
+
export function templatesFor(entry) {
|
|
12
|
+
const set = TEMPLATE_REGISTRY[entry.id];
|
|
13
|
+
if (set === undefined) {
|
|
14
|
+
throw new Error(`the catalog entry ${entry.id} has no template set; the build should have refused it`);
|
|
15
|
+
}
|
|
16
|
+
return set;
|
|
17
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AnyWizardQuestion, QuestionId, WizardAnswers, WizardContext } from "./wizard.questions.js";
|
|
2
|
+
/**
|
|
3
|
+
* R5 — how each question gets its answer, in this order:
|
|
4
|
+
*
|
|
5
|
+
* 1. **its flag**, validated by the question (a bad value is refused, never
|
|
6
|
+
* re-asked);
|
|
7
|
+
* 2. **the only choice there is** (N3): shown on a terminal with no keypress;
|
|
8
|
+
* 3. **`--yes`**: the question's default, when it has one;
|
|
9
|
+
* 4. **the prompt**, when someone can answer (stdin is a terminal);
|
|
10
|
+
* 5. otherwise the question is **unanswered**, and once every question has been
|
|
11
|
+
* tried the run refuses in one sentence naming exactly those flags.
|
|
12
|
+
*
|
|
13
|
+
* `--yes` comes before the prompt because it means "accept each unanswered
|
|
14
|
+
* question's default" (R5 + R2), on a terminal as much as anywhere; a question
|
|
15
|
+
* with no default (`new`'s `<name>`) is still asked on a terminal.
|
|
16
|
+
*/
|
|
17
|
+
/** The seam to a person (B26): `node:readline/promises` on a terminal, a script in specs. */
|
|
18
|
+
export type Prompter = {
|
|
19
|
+
/** Whether someone can answer: stdin is a terminal. */
|
|
20
|
+
readonly interactive: boolean;
|
|
21
|
+
/** Asks once; resolves with what was typed, `""` for Enter alone. */
|
|
22
|
+
readonly ask: (request: PromptRequest) => Promise<string>;
|
|
23
|
+
/** Says one line without asking (N3, and why an answer was not accepted). */
|
|
24
|
+
readonly say: (line: string) => void;
|
|
25
|
+
};
|
|
26
|
+
export type PromptRequest = {
|
|
27
|
+
readonly label: string;
|
|
28
|
+
/** The accepted values, shown with the label; absent for free text. */
|
|
29
|
+
readonly choices?: readonly string[];
|
|
30
|
+
readonly defaultValue?: string;
|
|
31
|
+
};
|
|
32
|
+
/** Nobody can answer and some questions are unanswered. A refusal: one sentence. */
|
|
33
|
+
export declare class UnansweredQuestionsError extends Error {
|
|
34
|
+
readonly name = "UnansweredQuestionsError";
|
|
35
|
+
}
|
|
36
|
+
export type AnswerSources = {
|
|
37
|
+
/** Raw answers from the command line, by question. */
|
|
38
|
+
readonly given: Readonly<Partial<Record<QuestionId, string>>>;
|
|
39
|
+
readonly yes: boolean;
|
|
40
|
+
readonly prompter: Prompter;
|
|
41
|
+
readonly context: Omit<WizardContext, "answered">;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Settles `questions` in order, each later question seeing the earlier answers.
|
|
45
|
+
*
|
|
46
|
+
* @throws WizardAnswerError when a flag's value is not an answer.
|
|
47
|
+
* @throws UnansweredQuestionsError when nobody can answer and something is unanswered.
|
|
48
|
+
*/
|
|
49
|
+
export declare function resolveAnswers(questions: readonly AnyWizardQuestion[], sources: AnswerSources): Promise<Partial<WizardAnswers>>;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { WizardAnswerError } from "./wizard.questions.js";
|
|
2
|
+
/** Nobody can answer and some questions are unanswered. A refusal: one sentence. */
|
|
3
|
+
export class UnansweredQuestionsError extends Error {
|
|
4
|
+
name = "UnansweredQuestionsError";
|
|
5
|
+
}
|
|
6
|
+
/** `--db <sqlite|postgresql>`, or `<name>` for the positional. */
|
|
7
|
+
function flagSpelling(question, context) {
|
|
8
|
+
if (!question.flag.startsWith("--")) {
|
|
9
|
+
return question.flag;
|
|
10
|
+
}
|
|
11
|
+
const choices = question.choices(context);
|
|
12
|
+
return choices === undefined
|
|
13
|
+
? `${question.flag} ${question.placeholder ?? "<value>"}`
|
|
14
|
+
: `${question.flag} <${choices.map((choice) => choice.value).join("|")}>`;
|
|
15
|
+
}
|
|
16
|
+
function listed(items) {
|
|
17
|
+
return items.length <= 1
|
|
18
|
+
? items.join("")
|
|
19
|
+
: `${items.slice(0, -1).join(", ")} and ${items[items.length - 1]}`;
|
|
20
|
+
}
|
|
21
|
+
/** The refusal sentence: the unanswered flags, exactly, and `--yes` for those with a default. */
|
|
22
|
+
function unansweredSentence(missing, yes) {
|
|
23
|
+
const defaulted = missing.filter((item) => item.defaulted);
|
|
24
|
+
const yesClause = !yes && defaulted.length > 0
|
|
25
|
+
? `, or pass --yes to accept the default${defaulted.length === 1 ? "" : "s"} for ${listed(defaulted.map((item) => item.spelling.split(" ")[0]))}`
|
|
26
|
+
: "";
|
|
27
|
+
return `stdin is not a terminal, so nothing can be asked: pass ${listed(missing.map((item) => item.spelling))}${yesClause}`;
|
|
28
|
+
}
|
|
29
|
+
async function prompt(question, context, prompter) {
|
|
30
|
+
const choices = question.choices(context)?.map((choice) => choice.value);
|
|
31
|
+
const defaultValue = question.defaultFor(context);
|
|
32
|
+
for (;;) {
|
|
33
|
+
const typed = (await prompter.ask({
|
|
34
|
+
label: question.label,
|
|
35
|
+
...(choices === undefined ? {} : { choices }),
|
|
36
|
+
...(defaultValue === undefined ? {} : { defaultValue }),
|
|
37
|
+
})).trim();
|
|
38
|
+
const raw = typed === "" ? defaultValue : typed;
|
|
39
|
+
if (raw === undefined) {
|
|
40
|
+
prompter.say(`${question.label} is required.`);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
return question.parse(raw, context);
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (!(error instanceof WizardAnswerError)) {
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
prompter.say(`${error.message}.`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Settles `questions` in order, each later question seeing the earlier answers.
|
|
56
|
+
*
|
|
57
|
+
* @throws WizardAnswerError when a flag's value is not an answer.
|
|
58
|
+
* @throws UnansweredQuestionsError when nobody can answer and something is unanswered.
|
|
59
|
+
*/
|
|
60
|
+
export async function resolveAnswers(questions, sources) {
|
|
61
|
+
const answered = {};
|
|
62
|
+
const missing = [];
|
|
63
|
+
for (const question of questions) {
|
|
64
|
+
const context = {
|
|
65
|
+
...sources.context,
|
|
66
|
+
answered: answered,
|
|
67
|
+
};
|
|
68
|
+
// A question that depends on one left unanswered cannot be asked yet.
|
|
69
|
+
if (question.after !== undefined &&
|
|
70
|
+
answered[question.after] === undefined) {
|
|
71
|
+
missing.push({ spelling: question.flag, defaulted: false });
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const given = sources.given[question.id];
|
|
75
|
+
if (given !== undefined) {
|
|
76
|
+
answered[question.id] = question.parse(given, context);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const choices = question.choices(context);
|
|
80
|
+
if (choices?.length === 1) {
|
|
81
|
+
const only = question.parse(choices[0].value, context);
|
|
82
|
+
answered[question.id] = only;
|
|
83
|
+
if (sources.prompter.interactive) {
|
|
84
|
+
sources.prompter.say(`${question.label}: ${choices[0].label} — the only choice available.`);
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
const defaultValue = question.defaultFor(context);
|
|
89
|
+
if (sources.yes && defaultValue !== undefined) {
|
|
90
|
+
answered[question.id] = question.parse(defaultValue, context);
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if (sources.prompter.interactive) {
|
|
94
|
+
answered[question.id] = await prompt(question, context, sources.prompter);
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
missing.push({
|
|
98
|
+
spelling: flagSpelling(question, context),
|
|
99
|
+
defaulted: defaultValue !== undefined,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (missing.length > 0) {
|
|
103
|
+
throw new UnansweredQuestionsError(unansweredSentence(missing, sources.yes));
|
|
104
|
+
}
|
|
105
|
+
return answered;
|
|
106
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Readable, Writable } from "node:stream";
|
|
2
|
+
import type { Prompter } from "./answer.resolver.js";
|
|
3
|
+
/**
|
|
4
|
+
* P6, B26 — the {@link Prompter} over `node:readline`: no prompt dependency.
|
|
5
|
+
*
|
|
6
|
+
* Lines are **queued** as they arrive and handed to questions in order. A
|
|
7
|
+
* terminal delivers whatever was typed ahead of a question — and a script piping
|
|
8
|
+
* answers into a pseudo-terminal delivers them all at once — and readline drops
|
|
9
|
+
* a line nobody is waiting for. Measured: with `readline/promises`' `question`,
|
|
10
|
+
* three answers piped into `script(1)` reached the first question only.
|
|
11
|
+
*/
|
|
12
|
+
/** Input ended while a question was open: the person closed the wizard. A refusal. */
|
|
13
|
+
export declare class WizardCancelledError extends Error {
|
|
14
|
+
readonly name = "WizardCancelledError";
|
|
15
|
+
}
|
|
16
|
+
export type ReadlinePrompter = Prompter & {
|
|
17
|
+
readonly close: () => void;
|
|
18
|
+
};
|
|
19
|
+
export declare function createReadlinePrompter(streams: {
|
|
20
|
+
readonly input: Readable & {
|
|
21
|
+
readonly isTTY?: boolean;
|
|
22
|
+
};
|
|
23
|
+
readonly output: Writable;
|
|
24
|
+
}): ReadlinePrompter;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { createInterface } from "node:readline";
|
|
2
|
+
/**
|
|
3
|
+
* P6, B26 — the {@link Prompter} over `node:readline`: no prompt dependency.
|
|
4
|
+
*
|
|
5
|
+
* Lines are **queued** as they arrive and handed to questions in order. A
|
|
6
|
+
* terminal delivers whatever was typed ahead of a question — and a script piping
|
|
7
|
+
* answers into a pseudo-terminal delivers them all at once — and readline drops
|
|
8
|
+
* a line nobody is waiting for. Measured: with `readline/promises`' `question`,
|
|
9
|
+
* three answers piped into `script(1)` reached the first question only.
|
|
10
|
+
*/
|
|
11
|
+
/** Input ended while a question was open: the person closed the wizard. A refusal. */
|
|
12
|
+
export class WizardCancelledError extends Error {
|
|
13
|
+
name = "WizardCancelledError";
|
|
14
|
+
}
|
|
15
|
+
export function createReadlinePrompter(streams) {
|
|
16
|
+
let session;
|
|
17
|
+
let ended = false;
|
|
18
|
+
const lines = [];
|
|
19
|
+
const waiting = [];
|
|
20
|
+
const cancelled = () => new WizardCancelledError("the wizard was closed before it was answered");
|
|
21
|
+
const open = () => {
|
|
22
|
+
if (session === undefined) {
|
|
23
|
+
session = createInterface({
|
|
24
|
+
input: streams.input,
|
|
25
|
+
output: streams.output,
|
|
26
|
+
});
|
|
27
|
+
session.on("line", (line) => {
|
|
28
|
+
const next = waiting.shift();
|
|
29
|
+
if (next === undefined) {
|
|
30
|
+
lines.push(line);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
next.resolve(line);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
session.once("close", () => {
|
|
37
|
+
ended = true;
|
|
38
|
+
for (const next of waiting.splice(0)) {
|
|
39
|
+
next.reject(cancelled());
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return session;
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
interactive: streams.input.isTTY === true,
|
|
47
|
+
ask: (request) => {
|
|
48
|
+
const readline = open();
|
|
49
|
+
readline.setPrompt(request.label +
|
|
50
|
+
(request.choices === undefined
|
|
51
|
+
? ""
|
|
52
|
+
: ` (${request.choices.join(", ")})`) +
|
|
53
|
+
(request.defaultValue === undefined
|
|
54
|
+
? ""
|
|
55
|
+
: ` [${request.defaultValue}]`) +
|
|
56
|
+
": ");
|
|
57
|
+
readline.prompt();
|
|
58
|
+
const typed = lines.shift();
|
|
59
|
+
if (typed !== undefined) {
|
|
60
|
+
return Promise.resolve(typed);
|
|
61
|
+
}
|
|
62
|
+
if (ended) {
|
|
63
|
+
return Promise.reject(cancelled());
|
|
64
|
+
}
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
waiting.push({ resolve, reject });
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
say: (line) => {
|
|
70
|
+
streams.output.write(`${line}\n`);
|
|
71
|
+
},
|
|
72
|
+
close: () => {
|
|
73
|
+
session?.close();
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import type { AdapterCatalog, CatalogEntry } from "../catalog/catalog-entry.interface.js";
|
|
2
|
+
import { type ExportReference, type ServiceScan } from "../project/service.detector.js";
|
|
3
|
+
/**
|
|
4
|
+
* R5 — every question the two commands ask, in **one table**: its flag, its
|
|
5
|
+
* label, its choices and its default. The command-line grammar, the TTY prompt,
|
|
6
|
+
* the `--yes` defaults and the non-interactive refusal sentence are all derived
|
|
7
|
+
* from it, so a question cannot have a flag the parser does not know or a
|
|
8
|
+
* default the refusal does not mention.
|
|
9
|
+
*
|
|
10
|
+
* Choices that are an adapter's facts — which ORMs and majors exist, which
|
|
11
|
+
* databases one supports — come from the generated catalog (R14), never from a
|
|
12
|
+
* literal here.
|
|
13
|
+
*/
|
|
14
|
+
/** Q16-5 (a). Yarn is not supported (U1). */
|
|
15
|
+
export declare const PACKAGE_MANAGERS: readonly ["npm", "pnpm"];
|
|
16
|
+
export type PackageManager = (typeof PACKAGE_MANAGERS)[number];
|
|
17
|
+
/** What the wizard settles. */
|
|
18
|
+
export type WizardAnswers = {
|
|
19
|
+
/** `new`'s project directory (Q16-3). */
|
|
20
|
+
readonly name: string;
|
|
21
|
+
/** The ORM and major, as the catalog entry that serves it (R14). */
|
|
22
|
+
readonly orm: CatalogEntry;
|
|
23
|
+
/** One of `orm`'s providers (R10). */
|
|
24
|
+
readonly db: string;
|
|
25
|
+
readonly packageManager: PackageManager;
|
|
26
|
+
/** `init` over an existing ORM setup: the developer's service (R6). */
|
|
27
|
+
readonly service: ServiceChoice;
|
|
28
|
+
/** The Nest module that provides and exports it. */
|
|
29
|
+
readonly module: ExportReference;
|
|
30
|
+
};
|
|
31
|
+
/** The service `AventaraModule` is given, or the template's new one to be written. */
|
|
32
|
+
export type ServiceChoice = ExportReference & {
|
|
33
|
+
/** Set when the service holds its client in a member instead of extending it. */
|
|
34
|
+
readonly clientMember?: string;
|
|
35
|
+
/** The template's new service file, written because the project has none. */
|
|
36
|
+
readonly isNew: boolean;
|
|
37
|
+
};
|
|
38
|
+
export type QuestionId = keyof WizardAnswers;
|
|
39
|
+
/** What the scan of the developer's source found, for the service questions (R6). */
|
|
40
|
+
export type ServiceContext = {
|
|
41
|
+
readonly scan: ServiceScan;
|
|
42
|
+
/** The ORM's client class, as the scan looked for it. */
|
|
43
|
+
readonly clientClass: string;
|
|
44
|
+
/** The service and module init writes when the project has none. */
|
|
45
|
+
readonly newService: ExportReference;
|
|
46
|
+
readonly newModule: ExportReference;
|
|
47
|
+
/** Whether a file already stands where the new service would be written. */
|
|
48
|
+
readonly newServiceFileExists: boolean;
|
|
49
|
+
/** How the ORM is named to the person, e.g. `Prisma`. */
|
|
50
|
+
readonly label: string;
|
|
51
|
+
};
|
|
52
|
+
/** What a question may consult: the catalog, what was detected, what is already answered. */
|
|
53
|
+
export type WizardContext = {
|
|
54
|
+
readonly catalog: AdapterCatalog;
|
|
55
|
+
/** Q16-5's default: the lockfile, else the launching package manager (B11), else npm. */
|
|
56
|
+
readonly detectedPackageManager: PackageManager;
|
|
57
|
+
readonly answered: Partial<WizardAnswers>;
|
|
58
|
+
/** Present for `init` over an existing ORM setup. */
|
|
59
|
+
readonly services?: ServiceContext;
|
|
60
|
+
};
|
|
61
|
+
export type Choice = {
|
|
62
|
+
/** What the flag takes and the prompt accepts, e.g. `prisma@7`. */
|
|
63
|
+
readonly value: string;
|
|
64
|
+
/** How it is shown, e.g. `Prisma 7`. */
|
|
65
|
+
readonly label: string;
|
|
66
|
+
};
|
|
67
|
+
/** An answer the question cannot accept. A refusal: one sentence. */
|
|
68
|
+
export declare class WizardAnswerError extends Error {
|
|
69
|
+
readonly name = "WizardAnswerError";
|
|
70
|
+
}
|
|
71
|
+
export type WizardQuestion<K extends QuestionId> = {
|
|
72
|
+
readonly id: K;
|
|
73
|
+
/** How the command line answers it: a `--flag <value>`, or the positional `<name>`. */
|
|
74
|
+
readonly flag: string;
|
|
75
|
+
/** The prompt's label. */
|
|
76
|
+
readonly label: string;
|
|
77
|
+
/** How the usage text shows its value; `<value>` when absent. */
|
|
78
|
+
readonly placeholder?: string;
|
|
79
|
+
/** A question whose choices depend on an earlier answer is not asked before it. */
|
|
80
|
+
readonly after?: QuestionId;
|
|
81
|
+
/** The answers on offer, or `undefined` for free text. */
|
|
82
|
+
readonly choices: (context: WizardContext) => readonly Choice[] | undefined;
|
|
83
|
+
/** The answer `--yes` (or Enter at the prompt) takes; `undefined` when there is none. */
|
|
84
|
+
readonly defaultFor: (context: WizardContext) => string | undefined;
|
|
85
|
+
/** @throws WizardAnswerError when `raw` is not an answer to this question. */
|
|
86
|
+
readonly parse: (raw: string, context: WizardContext) => WizardAnswers[K];
|
|
87
|
+
/** How a settled answer is shown back. */
|
|
88
|
+
readonly describe: (answer: WizardAnswers[K]) => string;
|
|
89
|
+
};
|
|
90
|
+
export type AnyWizardQuestion = {
|
|
91
|
+
readonly [K in QuestionId]: WizardQuestion<K>;
|
|
92
|
+
}[QuestionId];
|
|
93
|
+
export declare const NAME_QUESTION: WizardQuestion<"name">;
|
|
94
|
+
/**
|
|
95
|
+
* R14 — the ORM, from the catalog: `--orm <family>@<major>`, or `--orm <family>`
|
|
96
|
+
* for the family's highest major (Q16-31).
|
|
97
|
+
*/
|
|
98
|
+
export declare const ORM_QUESTION: WizardQuestion<"orm">;
|
|
99
|
+
/** R10 — the database, from the chosen entry's providers; the first is the default. */
|
|
100
|
+
export declare const DB_QUESTION: WizardQuestion<"db">;
|
|
101
|
+
/** Q16-5 — npm or pnpm; the default is detected. */
|
|
102
|
+
export declare const PACKAGE_MANAGER_QUESTION: WizardQuestion<"packageManager">;
|
|
103
|
+
/** R6, Q16-11 — the developer's service; a new one is written only when there is none. */
|
|
104
|
+
export declare const SERVICE_QUESTION: WizardQuestion<"service">;
|
|
105
|
+
/** The module that provides and exports the chosen service (or is @Global). */
|
|
106
|
+
export declare const MODULE_QUESTION: WizardQuestion<"module">;
|
|
107
|
+
/** `aventara new`'s questions, in the order they are asked. */
|
|
108
|
+
export declare const NEW_QUESTIONS: readonly [WizardQuestion<"name">, WizardQuestion<"orm">, WizardQuestion<"db">, WizardQuestion<"packageManager">];
|
|
109
|
+
/** `aventara init`'s questions when the project has no ORM (S6), in order. */
|
|
110
|
+
export declare const INIT_QUESTIONS: readonly [WizardQuestion<"orm">, WizardQuestion<"db">, WizardQuestion<"packageManager">];
|
|
111
|
+
/** `aventara init`'s questions over an existing ORM setup (S7): the ORM and database are read, not asked. */
|
|
112
|
+
export declare const REUSE_QUESTIONS: readonly [WizardQuestion<"service">, WizardQuestion<"module">, WizardQuestion<"packageManager">];
|
|
113
|
+
/** Every flag `aventara init` takes for a question, either way. */
|
|
114
|
+
export declare const INIT_FLAG_QUESTIONS: readonly [WizardQuestion<"orm">, WizardQuestion<"db">, WizardQuestion<"service">, WizardQuestion<"module">, WizardQuestion<"packageManager">];
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { modulesProviding, } from "../project/service.detector.js";
|
|
2
|
+
/**
|
|
3
|
+
* R5 — every question the two commands ask, in **one table**: its flag, its
|
|
4
|
+
* label, its choices and its default. The command-line grammar, the TTY prompt,
|
|
5
|
+
* the `--yes` defaults and the non-interactive refusal sentence are all derived
|
|
6
|
+
* from it, so a question cannot have a flag the parser does not know or a
|
|
7
|
+
* default the refusal does not mention.
|
|
8
|
+
*
|
|
9
|
+
* Choices that are an adapter's facts — which ORMs and majors exist, which
|
|
10
|
+
* databases one supports — come from the generated catalog (R14), never from a
|
|
11
|
+
* literal here.
|
|
12
|
+
*/
|
|
13
|
+
/** Q16-5 (a). Yarn is not supported (U1). */
|
|
14
|
+
export const PACKAGE_MANAGERS = ["npm", "pnpm"];
|
|
15
|
+
/** An answer the question cannot accept. A refusal: one sentence. */
|
|
16
|
+
export class WizardAnswerError extends Error {
|
|
17
|
+
name = "WizardAnswerError";
|
|
18
|
+
}
|
|
19
|
+
function ormLabel(entry) {
|
|
20
|
+
return `${entry.orm.label} ${entry.orm.major}`;
|
|
21
|
+
}
|
|
22
|
+
function available(catalog) {
|
|
23
|
+
return catalog
|
|
24
|
+
.map((entry) => `${ormLabel(entry)} (${entry.adapterPackage})`)
|
|
25
|
+
.join(", ");
|
|
26
|
+
}
|
|
27
|
+
/** Ordered for the wizard: by family as the catalog lists them, majors newest first. */
|
|
28
|
+
function ormChoices(catalog) {
|
|
29
|
+
const families = [...new Set(catalog.map((entry) => entry.orm.family))];
|
|
30
|
+
return families.flatMap((family) => catalog
|
|
31
|
+
.filter((entry) => entry.orm.family === family)
|
|
32
|
+
.sort((left, right) => right.orm.major - left.orm.major));
|
|
33
|
+
}
|
|
34
|
+
const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
35
|
+
export const NAME_QUESTION = {
|
|
36
|
+
id: "name",
|
|
37
|
+
flag: "<name>",
|
|
38
|
+
label: "Project name",
|
|
39
|
+
choices: () => undefined,
|
|
40
|
+
defaultFor: () => undefined,
|
|
41
|
+
parse: (raw) => {
|
|
42
|
+
const name = raw.trim();
|
|
43
|
+
if (!NAME.test(name)) {
|
|
44
|
+
throw new WizardAnswerError(`${JSON.stringify(raw)} is not a project name: use letters, digits, ".", "_" and "-", starting with a letter or digit`);
|
|
45
|
+
}
|
|
46
|
+
return name;
|
|
47
|
+
},
|
|
48
|
+
describe: (name) => name,
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* R14 — the ORM, from the catalog: `--orm <family>@<major>`, or `--orm <family>`
|
|
52
|
+
* for the family's highest major (Q16-31).
|
|
53
|
+
*/
|
|
54
|
+
export const ORM_QUESTION = {
|
|
55
|
+
id: "orm",
|
|
56
|
+
flag: "--orm",
|
|
57
|
+
label: "ORM",
|
|
58
|
+
choices: (context) => ormChoices(context.catalog).map((entry) => ({
|
|
59
|
+
value: `${entry.orm.family}@${entry.orm.major}`,
|
|
60
|
+
label: ormLabel(entry),
|
|
61
|
+
})),
|
|
62
|
+
defaultFor: (context) => context.catalog.length === 1
|
|
63
|
+
? `${context.catalog[0]?.orm.family}@${context.catalog[0]?.orm.major}`
|
|
64
|
+
: undefined,
|
|
65
|
+
parse: (raw, context) => {
|
|
66
|
+
const [family, major, ...rest] = raw.trim().toLowerCase().split("@");
|
|
67
|
+
const offered = ormChoices(context.catalog).filter((entry) => entry.orm.family === family);
|
|
68
|
+
if (rest.length > 0 || offered.length === 0) {
|
|
69
|
+
throw new WizardAnswerError(`there is no Aventara adapter for the ORM ${JSON.stringify(raw)}; available: ${available(context.catalog)}`);
|
|
70
|
+
}
|
|
71
|
+
if (major === undefined) {
|
|
72
|
+
return offered[0];
|
|
73
|
+
}
|
|
74
|
+
const entry = offered.find((candidate) => String(candidate.orm.major) === major);
|
|
75
|
+
if (entry === undefined) {
|
|
76
|
+
throw new WizardAnswerError(`there is no Aventara adapter for ${offered[0]?.orm.label} ${major}; available: ${available(context.catalog)}`);
|
|
77
|
+
}
|
|
78
|
+
return entry;
|
|
79
|
+
},
|
|
80
|
+
describe: ormLabel,
|
|
81
|
+
};
|
|
82
|
+
function requireOrm(context) {
|
|
83
|
+
const orm = context.answered.orm;
|
|
84
|
+
if (orm === undefined) {
|
|
85
|
+
throw new Error("the database question is asked after the ORM question");
|
|
86
|
+
}
|
|
87
|
+
return orm;
|
|
88
|
+
}
|
|
89
|
+
/** R10 — the database, from the chosen entry's providers; the first is the default. */
|
|
90
|
+
export const DB_QUESTION = {
|
|
91
|
+
id: "db",
|
|
92
|
+
flag: "--db",
|
|
93
|
+
label: "Database",
|
|
94
|
+
after: "orm",
|
|
95
|
+
choices: (context) => requireOrm(context).providers.map((provider) => ({
|
|
96
|
+
value: provider,
|
|
97
|
+
label: provider,
|
|
98
|
+
})),
|
|
99
|
+
defaultFor: (context) => requireOrm(context).providers[0],
|
|
100
|
+
parse: (raw, context) => {
|
|
101
|
+
const orm = requireOrm(context);
|
|
102
|
+
const db = raw.trim().toLowerCase();
|
|
103
|
+
if (!orm.providers.includes(db)) {
|
|
104
|
+
throw new WizardAnswerError(`the ${ormLabel(orm)} adapter does not support the database ${JSON.stringify(raw)}; choose ${orm.providers.join(" or ")}`);
|
|
105
|
+
}
|
|
106
|
+
return db;
|
|
107
|
+
},
|
|
108
|
+
describe: (db) => db,
|
|
109
|
+
};
|
|
110
|
+
/** Q16-5 — npm or pnpm; the default is detected. */
|
|
111
|
+
export const PACKAGE_MANAGER_QUESTION = {
|
|
112
|
+
id: "packageManager",
|
|
113
|
+
flag: "--package-manager",
|
|
114
|
+
label: "Package manager",
|
|
115
|
+
choices: () => PACKAGE_MANAGERS.map((manager) => ({ value: manager, label: manager })),
|
|
116
|
+
defaultFor: (context) => context.detectedPackageManager,
|
|
117
|
+
parse: (raw) => {
|
|
118
|
+
const manager = PACKAGE_MANAGERS.find((candidate) => candidate === raw.trim().toLowerCase());
|
|
119
|
+
if (manager === undefined) {
|
|
120
|
+
throw new WizardAnswerError(`${JSON.stringify(raw)} is not a supported package manager; choose ${PACKAGE_MANAGERS.join(" or ")}`);
|
|
121
|
+
}
|
|
122
|
+
return manager;
|
|
123
|
+
},
|
|
124
|
+
describe: (manager) => manager,
|
|
125
|
+
};
|
|
126
|
+
function reference(raw, flag) {
|
|
127
|
+
const hash = raw.lastIndexOf("#");
|
|
128
|
+
const exportName = raw.slice(hash + 1).trim();
|
|
129
|
+
const file = raw.slice(0, hash).trim().replace(/^\.\//, "");
|
|
130
|
+
if (hash <= 0 || !/^[A-Za-z_$][\w$]*$/.test(exportName)) {
|
|
131
|
+
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)}`);
|
|
132
|
+
}
|
|
133
|
+
return { file, exportName };
|
|
134
|
+
}
|
|
135
|
+
const spelled = (ref) => `${ref.file}#${ref.exportName}`;
|
|
136
|
+
function requireServices(context) {
|
|
137
|
+
if (context.services === undefined) {
|
|
138
|
+
throw new Error("the service questions are asked only over an existing ORM setup");
|
|
139
|
+
}
|
|
140
|
+
return context.services;
|
|
141
|
+
}
|
|
142
|
+
/** The services on offer: what the scan found, or — when it found none — the template's new file. */
|
|
143
|
+
function serviceOptions(services) {
|
|
144
|
+
const found = [
|
|
145
|
+
...services.scan.candidates.map((candidate) => ({
|
|
146
|
+
...candidate,
|
|
147
|
+
isNew: false,
|
|
148
|
+
})),
|
|
149
|
+
...services.scan.compositions.map(({ member, ...candidate }) => ({
|
|
150
|
+
...candidate,
|
|
151
|
+
clientMember: member,
|
|
152
|
+
isNew: false,
|
|
153
|
+
})),
|
|
154
|
+
];
|
|
155
|
+
return found.length > 0 || services.newServiceFileExists
|
|
156
|
+
? found
|
|
157
|
+
: [{ ...services.newService, isNew: true }];
|
|
158
|
+
}
|
|
159
|
+
/** R6, Q16-11 — the developer's service; a new one is written only when there is none. */
|
|
160
|
+
export const SERVICE_QUESTION = {
|
|
161
|
+
id: "service",
|
|
162
|
+
flag: "--prisma-service",
|
|
163
|
+
placeholder: "<path#Export>",
|
|
164
|
+
label: "Service to give Aventara (file#Export)",
|
|
165
|
+
// With no service found, the offer to write one is a question with a default
|
|
166
|
+
// (asked on a terminal, taken by --yes), not a single choice taken silently.
|
|
167
|
+
choices: (context) => {
|
|
168
|
+
const options = serviceOptions(requireServices(context));
|
|
169
|
+
return options.every((option) => option.isNew)
|
|
170
|
+
? undefined
|
|
171
|
+
: options.map((option) => ({
|
|
172
|
+
value: spelled(option),
|
|
173
|
+
label: option.isNew
|
|
174
|
+
? `write ${option.file}`
|
|
175
|
+
: option.clientMember === undefined
|
|
176
|
+
? spelled(option)
|
|
177
|
+
: `${spelled(option)} (holds its client in .${option.clientMember})`,
|
|
178
|
+
}));
|
|
179
|
+
},
|
|
180
|
+
defaultFor: (context) => {
|
|
181
|
+
const options = serviceOptions(requireServices(context));
|
|
182
|
+
return options.length === 1
|
|
183
|
+
? spelled(options[0])
|
|
184
|
+
: undefined;
|
|
185
|
+
},
|
|
186
|
+
parse: (raw, context) => {
|
|
187
|
+
const services = requireServices(context);
|
|
188
|
+
const wanted = reference(raw, "--prisma-service");
|
|
189
|
+
const options = serviceOptions(services);
|
|
190
|
+
const chosen = options.find((option) => option.file === wanted.file && option.exportName === wanted.exportName);
|
|
191
|
+
if (chosen === undefined) {
|
|
192
|
+
throw new WizardAnswerError(`${spelled(wanted)} is not a service found in src/ (a class extending ${services.clientClass}, or one holding it); found: ${options
|
|
193
|
+
.filter((option) => !option.isNew)
|
|
194
|
+
.map(spelled)
|
|
195
|
+
.join(", ") || "none"}`);
|
|
196
|
+
}
|
|
197
|
+
return chosen;
|
|
198
|
+
},
|
|
199
|
+
describe: spelled,
|
|
200
|
+
};
|
|
201
|
+
/** The module that provides and exports the chosen service (or is @Global). */
|
|
202
|
+
export const MODULE_QUESTION = {
|
|
203
|
+
id: "module",
|
|
204
|
+
flag: "--prisma-module",
|
|
205
|
+
placeholder: "<path#Export>",
|
|
206
|
+
label: "Its Nest module (file#Export)",
|
|
207
|
+
after: "service",
|
|
208
|
+
choices: (context) => {
|
|
209
|
+
const services = requireServices(context);
|
|
210
|
+
const service = context.answered.service;
|
|
211
|
+
const modules = service.isNew
|
|
212
|
+
? [services.newModule]
|
|
213
|
+
: modulesProviding(services.scan, service);
|
|
214
|
+
return modules.map((module) => ({
|
|
215
|
+
value: spelled(module),
|
|
216
|
+
label: spelled(module),
|
|
217
|
+
}));
|
|
218
|
+
},
|
|
219
|
+
defaultFor: () => undefined,
|
|
220
|
+
parse: (raw, context) => {
|
|
221
|
+
const services = requireServices(context);
|
|
222
|
+
const service = context.answered.service;
|
|
223
|
+
const wanted = reference(raw, "--prisma-module");
|
|
224
|
+
const modules = service.isNew
|
|
225
|
+
? [services.newModule]
|
|
226
|
+
: modulesProviding(services.scan, service);
|
|
227
|
+
const chosen = modules.find((module) => module.file === wanted.file && module.exportName === wanted.exportName);
|
|
228
|
+
if (chosen === undefined) {
|
|
229
|
+
throw new WizardAnswerError(`${spelled(wanted)} is not a module that provides and exports ${service.exportName}; found: ${modules.map(spelled).join(", ") || "none"}`);
|
|
230
|
+
}
|
|
231
|
+
return { file: chosen.file, exportName: chosen.exportName };
|
|
232
|
+
},
|
|
233
|
+
describe: spelled,
|
|
234
|
+
};
|
|
235
|
+
/** `aventara new`'s questions, in the order they are asked. */
|
|
236
|
+
export const NEW_QUESTIONS = [
|
|
237
|
+
NAME_QUESTION,
|
|
238
|
+
ORM_QUESTION,
|
|
239
|
+
DB_QUESTION,
|
|
240
|
+
PACKAGE_MANAGER_QUESTION,
|
|
241
|
+
];
|
|
242
|
+
/** `aventara init`'s questions when the project has no ORM (S6), in order. */
|
|
243
|
+
export const INIT_QUESTIONS = [
|
|
244
|
+
ORM_QUESTION,
|
|
245
|
+
DB_QUESTION,
|
|
246
|
+
PACKAGE_MANAGER_QUESTION,
|
|
247
|
+
];
|
|
248
|
+
/** `aventara init`'s questions over an existing ORM setup (S7): the ORM and database are read, not asked. */
|
|
249
|
+
export const REUSE_QUESTIONS = [
|
|
250
|
+
SERVICE_QUESTION,
|
|
251
|
+
MODULE_QUESTION,
|
|
252
|
+
PACKAGE_MANAGER_QUESTION,
|
|
253
|
+
];
|
|
254
|
+
/** Every flag `aventara init` takes for a question, either way. */
|
|
255
|
+
export const INIT_FLAG_QUESTIONS = [
|
|
256
|
+
ORM_QUESTION,
|
|
257
|
+
DB_QUESTION,
|
|
258
|
+
SERVICE_QUESTION,
|
|
259
|
+
MODULE_QUESTION,
|
|
260
|
+
PACKAGE_MANAGER_QUESTION,
|
|
261
|
+
];
|