@aventara/cli 0.0.0-stage → 0.1.0-pilot.0
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 +88 -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/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 +31 -0
- package/dist/command/command.parser.js +138 -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 +52 -0
- package/dist/plan/project.planner.js +178 -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 +396 -0
- package/dist/templates/template.registry.d.ts +151 -0
- package/dist/templates/template.registry.js +12 -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,15 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { refuseUnsupportedNode } from "./node-version.guard.js";
|
|
3
|
+
/**
|
|
4
|
+
* `aventara`'s entry, what the manifest's `bin` names (F-855). It always runs — no
|
|
5
|
+
* `import.meta.main` — and checks this Node against the package's
|
|
6
|
+
* `engines.node` before it loads anything else: the program is imported only
|
|
7
|
+
* once the guard admits this Node, so an older Node meets one sentence and exit
|
|
8
|
+
* 1, never a silent exit 0 or a parse error from a module it cannot run.
|
|
9
|
+
*
|
|
10
|
+
* A promise chain rather than a top-level `await`, which Node 12 cannot parse:
|
|
11
|
+
* this file is read by the Nodes the package does not support.
|
|
12
|
+
*/
|
|
13
|
+
if (!refuseUnsupportedNode("aventara", new URL("../package.json", import.meta.url))) {
|
|
14
|
+
void import("./cli.js").then((program) => program.runFromProcess());
|
|
15
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export declare const ADAPTER_CATALOG: readonly [{
|
|
2
|
+
readonly id: "prisma7";
|
|
3
|
+
readonly adapterPackage: "@aventara/prisma7-adapter";
|
|
4
|
+
readonly orm: {
|
|
5
|
+
readonly family: "prisma";
|
|
6
|
+
readonly label: "Prisma";
|
|
7
|
+
readonly package: "@prisma/client";
|
|
8
|
+
readonly major: 7;
|
|
9
|
+
};
|
|
10
|
+
readonly range: "^7.10.0";
|
|
11
|
+
readonly familyPackages: readonly ["prisma", "@prisma/client", "@prisma/prisma7", "@prisma/orm-*"];
|
|
12
|
+
readonly providers: readonly ["sqlite", "postgresql"];
|
|
13
|
+
readonly drivers: {
|
|
14
|
+
readonly sqlite: "@prisma/adapter-better-sqlite3";
|
|
15
|
+
readonly postgresql: "@prisma/adapter-pg";
|
|
16
|
+
};
|
|
17
|
+
readonly generators: readonly ["prisma-client"];
|
|
18
|
+
readonly bin: "aventara-prisma7-generate";
|
|
19
|
+
}];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
// biome-ignore-all format: generated output; these bytes are the catalog
|
|
2
|
+
// biome-ignore-all lint: generated output
|
|
3
|
+
/* !!! Generated by @aventara/cli's adapter-catalog generator from each adapter's manifest. Do not edit. !!! */
|
|
4
|
+
/* Regenerate with `pnpm --filter @aventara/cli build` (its `prebuild`). */
|
|
5
|
+
export const ADAPTER_CATALOG = [
|
|
6
|
+
{
|
|
7
|
+
"id": "prisma7",
|
|
8
|
+
"adapterPackage": "@aventara/prisma7-adapter",
|
|
9
|
+
"orm": {
|
|
10
|
+
"family": "prisma",
|
|
11
|
+
"label": "Prisma",
|
|
12
|
+
"package": "@prisma/client",
|
|
13
|
+
"major": 7
|
|
14
|
+
},
|
|
15
|
+
"range": "^7.10.0",
|
|
16
|
+
"familyPackages": [
|
|
17
|
+
"prisma",
|
|
18
|
+
"@prisma/client",
|
|
19
|
+
"@prisma/prisma7",
|
|
20
|
+
"@prisma/orm-*"
|
|
21
|
+
],
|
|
22
|
+
"providers": [
|
|
23
|
+
"sqlite",
|
|
24
|
+
"postgresql"
|
|
25
|
+
],
|
|
26
|
+
"drivers": {
|
|
27
|
+
"sqlite": "@prisma/adapter-better-sqlite3",
|
|
28
|
+
"postgresql": "@prisma/adapter-pg"
|
|
29
|
+
},
|
|
30
|
+
"generators": [
|
|
31
|
+
"prisma-client"
|
|
32
|
+
],
|
|
33
|
+
"bin": "aventara-prisma7-generate"
|
|
34
|
+
}
|
|
35
|
+
];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { ADAPTER_CATALOG } from "./adapter.catalog.generated.js";
|
|
2
|
+
/**
|
|
3
|
+
* One entry per Aventara adapter package that exists (R13, R14): what the CLI
|
|
4
|
+
* may offer and what it must refuse, every value traced to that adapter's own
|
|
5
|
+
* manifest — its `aventara.adapter` declaration, its `peerDependencies` and its
|
|
6
|
+
* `bin` (Q16-27). The entries are **generated** at this package's build by
|
|
7
|
+
* `scripts/adapter-catalog.generator.ts` into `adapter.catalog.generated.ts`
|
|
8
|
+
* and never edited (P9). The CLI holds no list of ORMs, majors, providers or
|
|
9
|
+
* drivers of its own.
|
|
10
|
+
*/
|
|
11
|
+
export type CatalogEntry = {
|
|
12
|
+
/** The adapter package's ORM stem, e.g. `prisma7` (Q16-33): the wizard's value and the templates' key. */
|
|
13
|
+
readonly id: string;
|
|
14
|
+
/** The adapter package a project installs, e.g. `@aventara/prisma7-adapter`. */
|
|
15
|
+
readonly adapterPackage: string;
|
|
16
|
+
readonly orm: {
|
|
17
|
+
/** `aventara.adapter.orm`, e.g. `prisma`: the family the wizard groups majors under. */
|
|
18
|
+
readonly family: string;
|
|
19
|
+
/** `aventara.adapter.ormLabel`, e.g. `Prisma`: how the wizard names the family. */
|
|
20
|
+
readonly label: string;
|
|
21
|
+
/** `aventara.adapter.ormPackage`, e.g. `@prisma/client`: whose installed version selects the entry. */
|
|
22
|
+
readonly package: string;
|
|
23
|
+
/** The one major `range` admits. */
|
|
24
|
+
readonly major: number;
|
|
25
|
+
};
|
|
26
|
+
/** `peerDependencies[orm.package]`: the ORM versions the adapter supports (D8). */
|
|
27
|
+
readonly range: string;
|
|
28
|
+
/** Packages that identify the ORM family in a project (D10, Q16-27). */
|
|
29
|
+
readonly familyPackages: readonly string[];
|
|
30
|
+
/** Database providers, in the adapter's order; the first is the default (R10). */
|
|
31
|
+
readonly providers: readonly string[];
|
|
32
|
+
/** Provider → the driver adapter package for it. */
|
|
33
|
+
readonly drivers: Readonly<Record<string, string>>;
|
|
34
|
+
/** The ORM generators whose output the adapter reads (B27). */
|
|
35
|
+
readonly generators: readonly string[];
|
|
36
|
+
/** The adapter's generate step: its one `bin` (Q16-30). */
|
|
37
|
+
readonly bin: string;
|
|
38
|
+
};
|
|
39
|
+
export type AdapterCatalog = readonly CatalogEntry[];
|
|
40
|
+
/** The generated catalog's literal type: what the template registry is checked against. */
|
|
41
|
+
export type Catalog = typeof ADAPTER_CATALOG;
|
|
42
|
+
export type EntryId = Catalog[number]["id"];
|
|
43
|
+
/** The providers one entry declares, as literals. */
|
|
44
|
+
export type ProviderOf<I extends EntryId> = Extract<Catalog[number], {
|
|
45
|
+
readonly id: I;
|
|
46
|
+
}>["providers"][number];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AdapterCatalog, CatalogEntry } from "./catalog-entry.interface.js";
|
|
2
|
+
/**
|
|
3
|
+
* R7, R13, R14, D10, P10 — which catalog entry serves a project that already
|
|
4
|
+
* has an ORM, decided from what is **installed** and what each adapter
|
|
5
|
+
* **declares**; this module holds no ORM, version or provider of its own.
|
|
6
|
+
*
|
|
7
|
+
* 1. The ORM family: the catalog entries whose family packages the project
|
|
8
|
+
* depends on (a pattern ending in `*` is a prefix).
|
|
9
|
+
* 2. Its major: the installed version of the family's ORM package, never the
|
|
10
|
+
* range in `package.json` (P10: `^7` can resolve to anything in 7, and a
|
|
11
|
+
* stale lockfile to 6.x). Every other family package installed must be of
|
|
12
|
+
* that same major — a Prisma 8 CLI or an `@prisma/orm-*` beside a Prisma 7
|
|
13
|
+
* client is a project mid-migration, refused (D10).
|
|
14
|
+
* 3. The entry for that major, or a refusal naming what exists.
|
|
15
|
+
* 4. The installed version inside the entry's declared range.
|
|
16
|
+
* 5. Then what the project's ORM setup declares — provider, driver, generator —
|
|
17
|
+
* against the entry's providers, drivers and generators.
|
|
18
|
+
*
|
|
19
|
+
* Every refusal is one sentence naming what was found and what the catalog
|
|
20
|
+
* offers. Nothing has been written when one is raised.
|
|
21
|
+
*/
|
|
22
|
+
export type MatchVerdict = {
|
|
23
|
+
readonly kind: "matched";
|
|
24
|
+
readonly entry: CatalogEntry;
|
|
25
|
+
} | {
|
|
26
|
+
readonly kind: "refused";
|
|
27
|
+
readonly sentence: string;
|
|
28
|
+
};
|
|
29
|
+
export type InstalledOrm = {
|
|
30
|
+
/** The project's dependency names (dependencies and devDependencies). */
|
|
31
|
+
readonly dependencies: readonly string[];
|
|
32
|
+
/** The installed version of a package, or `undefined` when it is not installed. */
|
|
33
|
+
readonly installedVersion: (name: string) => string | undefined;
|
|
34
|
+
/** The command that installs the project's dependencies. */
|
|
35
|
+
readonly installCommand: string;
|
|
36
|
+
};
|
|
37
|
+
/** What the project's ORM setup states, read by the entry's templates. */
|
|
38
|
+
export type OrmSetup = {
|
|
39
|
+
readonly provider: string | undefined;
|
|
40
|
+
readonly generator: string | undefined;
|
|
41
|
+
/** The driver-adapter packages the project depends on. */
|
|
42
|
+
readonly drivers: readonly string[];
|
|
43
|
+
};
|
|
44
|
+
export declare function matchInstalledOrm(catalog: AdapterCatalog, project: InstalledOrm): MatchVerdict;
|
|
45
|
+
/** Step 5: the project's provider, driver and generator against what `entry` declares. */
|
|
46
|
+
export declare function matchOrmSetup(entry: CatalogEntry, setup: OrmSetup): string | undefined;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { satisfiesRange } from "./range.reader.js";
|
|
2
|
+
function matches(pattern, name) {
|
|
3
|
+
return pattern.endsWith("*")
|
|
4
|
+
? name.startsWith(pattern.slice(0, -1))
|
|
5
|
+
: name === pattern;
|
|
6
|
+
}
|
|
7
|
+
function majorOf(version) {
|
|
8
|
+
const major = /^(\d+)\./.exec(version)?.[1];
|
|
9
|
+
return major === undefined ? undefined : Number(major);
|
|
10
|
+
}
|
|
11
|
+
function available(catalog) {
|
|
12
|
+
return catalog
|
|
13
|
+
.map((entry) => `${entry.orm.label} ${entry.orm.major} (${entry.adapterPackage})`)
|
|
14
|
+
.join(", ");
|
|
15
|
+
}
|
|
16
|
+
export function matchInstalledOrm(catalog, project) {
|
|
17
|
+
const family = catalog.filter((entry) => entry.familyPackages.some((pattern) => project.dependencies.some((name) => matches(pattern, name))));
|
|
18
|
+
const first = family[0];
|
|
19
|
+
if (first === undefined) {
|
|
20
|
+
throw new Error("matchInstalledOrm is asked only about a project with an ORM");
|
|
21
|
+
}
|
|
22
|
+
const label = first.orm.label;
|
|
23
|
+
const ormPackage = first.orm.package;
|
|
24
|
+
const members = project.dependencies.filter((name) => first.familyPackages.some((pattern) => matches(pattern, name)));
|
|
25
|
+
const installed = members.map((name) => ({
|
|
26
|
+
name,
|
|
27
|
+
version: project.installedVersion(name),
|
|
28
|
+
}));
|
|
29
|
+
const missing = installed.filter((member) => member.version === undefined);
|
|
30
|
+
if (missing.length > 0) {
|
|
31
|
+
return {
|
|
32
|
+
kind: "refused",
|
|
33
|
+
sentence: `this project depends on ${missing.map((member) => member.name).join(", ")}, which ${missing.length === 1 ? "is" : "are"} not installed, so its ${label} version cannot be read; install first: ${project.installCommand}`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const client = installed.find((member) => member.name === ormPackage);
|
|
37
|
+
const clientMajor = client?.version === undefined ? undefined : majorOf(client.version);
|
|
38
|
+
// D10: one major across the family. The first package off the client's major
|
|
39
|
+
// (or any, when there is no client) names the project's other major.
|
|
40
|
+
const off = installed.find((member) => member.name !== ormPackage &&
|
|
41
|
+
majorOf(member.version) !== clientMajor);
|
|
42
|
+
const decided = off ?? client;
|
|
43
|
+
if (decided === undefined) {
|
|
44
|
+
return {
|
|
45
|
+
kind: "refused",
|
|
46
|
+
sentence: `this project depends on ${members.join(", ")} but not on ${ormPackage}; available: ${available(catalog)}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const major = majorOf(decided.version);
|
|
50
|
+
const entry = family.find((candidate) => candidate.orm.major === major);
|
|
51
|
+
if (off !== undefined || entry === undefined) {
|
|
52
|
+
return {
|
|
53
|
+
kind: "refused",
|
|
54
|
+
sentence: `this project uses ${label} ${major} (${decided.name} ${decided.version}); there is no Aventara adapter for ${label} ${major}; available: ${available(catalog)}`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
if (!satisfiesRange(client?.version, entry.range)) {
|
|
58
|
+
return {
|
|
59
|
+
kind: "refused",
|
|
60
|
+
sentence: `this project has ${ormPackage} ${client?.version}, outside the ${entry.range} the ${label} ${entry.orm.major} adapter (${entry.adapterPackage}) supports; upgrade ${ormPackage}, the ${label} CLI and the driver adapter to that range`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return { kind: "matched", entry };
|
|
64
|
+
}
|
|
65
|
+
/** Step 5: the project's provider, driver and generator against what `entry` declares. */
|
|
66
|
+
export function matchOrmSetup(entry, setup) {
|
|
67
|
+
const name = `the ${entry.orm.label} ${entry.orm.major} adapter`;
|
|
68
|
+
if (setup.provider === undefined ||
|
|
69
|
+
!entry.providers.includes(setup.provider)) {
|
|
70
|
+
return `this project's schema uses the database ${JSON.stringify(setup.provider ?? "(none)")}, and ${name} supports ${entry.providers.join(" and ")}`;
|
|
71
|
+
}
|
|
72
|
+
if (setup.generator === undefined ||
|
|
73
|
+
!entry.generators.includes(setup.generator)) {
|
|
74
|
+
return `this project's schema generates its client with ${JSON.stringify(setup.generator ?? "(no generator)")}, and ${name} reads only the output of ${entry.generators.join(" and ")}; switch the generator block to it`;
|
|
75
|
+
}
|
|
76
|
+
const driver = entry.drivers[setup.provider];
|
|
77
|
+
if (!setup.drivers.includes(driver)) {
|
|
78
|
+
return setup.drivers.length === 0
|
|
79
|
+
? `this project has no driver adapter for ${setup.provider}, which ${entry.orm.label} ${entry.orm.major} requires; ${name} supports ${driver}`
|
|
80
|
+
: `this project drives ${setup.provider} through ${setup.drivers.join(", ")}, and ${name} supports ${driver}`;
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a declared version range for the two facts the CLI needs from one:
|
|
3
|
+
* its lowest version (D1, D9: what `init` pins when it installs the ORM — the
|
|
4
|
+
* adapter's measured floor) and its lowest major (a project's declared Nest and
|
|
5
|
+
* TypeScript ranges, checked before anything is written).
|
|
6
|
+
*/
|
|
7
|
+
/** `^7.10.0` → `7.10.0`; `>=7.10.0 <8` → `7.10.0`. */
|
|
8
|
+
export declare function lowestVersionOf(range: string): string;
|
|
9
|
+
/** `^12.0.1` → 12; `~6.0.2` → 6; a range with no number (`latest`, `*`) → `undefined`. */
|
|
10
|
+
export declare function lowestMajorOf(range: string): number | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Whether an installed `version` is in a declared `range` (R7: the adapter's
|
|
13
|
+
* declaration decides support). Reads the comparator forms an adapter's peer
|
|
14
|
+
* range uses; a pre-release never satisfies (semver's rule for a range whose
|
|
15
|
+
* comparators carry none). Any other form is refused rather than guessed.
|
|
16
|
+
*/
|
|
17
|
+
export declare function satisfiesRange(version: string, range: string): boolean;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a declared version range for the two facts the CLI needs from one:
|
|
3
|
+
* its lowest version (D1, D9: what `init` pins when it installs the ORM — the
|
|
4
|
+
* adapter's measured floor) and its lowest major (a project's declared Nest and
|
|
5
|
+
* TypeScript ranges, checked before anything is written).
|
|
6
|
+
*/
|
|
7
|
+
const VERSION = /(\d+)(?:\.(\d+))?(?:\.(\d+))?/;
|
|
8
|
+
/** `^7.10.0` → `7.10.0`; `>=7.10.0 <8` → `7.10.0`. */
|
|
9
|
+
export function lowestVersionOf(range) {
|
|
10
|
+
const found = VERSION.exec(range);
|
|
11
|
+
if (found === null) {
|
|
12
|
+
throw new Error(`the range ${JSON.stringify(range)} states no version`);
|
|
13
|
+
}
|
|
14
|
+
return `${found[1]}.${found[2] ?? 0}.${found[3] ?? 0}`;
|
|
15
|
+
}
|
|
16
|
+
/** `^12.0.1` → 12; `~6.0.2` → 6; a range with no number (`latest`, `*`) → `undefined`. */
|
|
17
|
+
export function lowestMajorOf(range) {
|
|
18
|
+
const found = VERSION.exec(range);
|
|
19
|
+
return found === null ? undefined : Number(found[1]);
|
|
20
|
+
}
|
|
21
|
+
const RELEASE = /^(\d+)\.(\d+)\.(\d+)$/;
|
|
22
|
+
const COMPARATOR = /^(\^|>=|>|<=|<|=)?(\d+)(?:\.(\d+))?(?:\.(\d+))?$/;
|
|
23
|
+
/**
|
|
24
|
+
* Whether an installed `version` is in a declared `range` (R7: the adapter's
|
|
25
|
+
* declaration decides support). Reads the comparator forms an adapter's peer
|
|
26
|
+
* range uses; a pre-release never satisfies (semver's rule for a range whose
|
|
27
|
+
* comparators carry none). Any other form is refused rather than guessed.
|
|
28
|
+
*/
|
|
29
|
+
export function satisfiesRange(version, range) {
|
|
30
|
+
const release = RELEASE.exec(version);
|
|
31
|
+
if (release === null) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
const candidate = [
|
|
35
|
+
Number(release[1]),
|
|
36
|
+
Number(release[2]),
|
|
37
|
+
Number(release[3]),
|
|
38
|
+
];
|
|
39
|
+
const compare = (bound) => {
|
|
40
|
+
for (let index = 0; index < 3; index += 1) {
|
|
41
|
+
const difference = candidate[index] - bound[index];
|
|
42
|
+
if (difference !== 0) {
|
|
43
|
+
return difference;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return 0;
|
|
47
|
+
};
|
|
48
|
+
return range
|
|
49
|
+
.trim()
|
|
50
|
+
.split(/\s+/)
|
|
51
|
+
.every((comparator) => {
|
|
52
|
+
const parsed = COMPARATOR.exec(comparator);
|
|
53
|
+
if (parsed === null || (parsed[1] === "^" && parsed[2] === "0")) {
|
|
54
|
+
throw new Error(`the range ${JSON.stringify(range)} is not one the CLI reads`);
|
|
55
|
+
}
|
|
56
|
+
const bound = [
|
|
57
|
+
Number(parsed[2]),
|
|
58
|
+
Number(parsed[3] ?? 0),
|
|
59
|
+
Number(parsed[4] ?? 0),
|
|
60
|
+
];
|
|
61
|
+
const order = compare(bound);
|
|
62
|
+
switch (parsed[1]) {
|
|
63
|
+
case "^":
|
|
64
|
+
return order >= 0 && candidate[0] === bound[0];
|
|
65
|
+
case ">=":
|
|
66
|
+
return order >= 0;
|
|
67
|
+
case ">":
|
|
68
|
+
return order > 0;
|
|
69
|
+
case "<=":
|
|
70
|
+
return order <= 0;
|
|
71
|
+
case "<":
|
|
72
|
+
return order < 0;
|
|
73
|
+
default:
|
|
74
|
+
return order === 0;
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { AdapterCatalog } from "./catalog/catalog-entry.interface.js";
|
|
2
|
+
import { type CommandRunner } from "./run/command.runner.js";
|
|
3
|
+
import { type Prompter } from "./wizard/answer.resolver.js";
|
|
4
|
+
/**
|
|
5
|
+
* `aventara` — the bin (R1): `new` and `init`. A refusal is one sentence on
|
|
6
|
+
* stderr and exit 1, never a stack; anything else is a defect and keeps its
|
|
7
|
+
* stack.
|
|
8
|
+
*
|
|
9
|
+
* `init` initializes the project in the working directory
|
|
10
|
+
* (`run/init.orchestrator.ts`); `new` creates one with the pinned `nest new` and
|
|
11
|
+
* then runs the same pipeline over it (`run/new.orchestrator.ts`).
|
|
12
|
+
*/
|
|
13
|
+
export type CliIo = {
|
|
14
|
+
/** The working directory: where `init` initializes and `new` creates. */
|
|
15
|
+
readonly cwd: string;
|
|
16
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
17
|
+
readonly stdout: (text: string) => void;
|
|
18
|
+
readonly stderr: (text: string) => void;
|
|
19
|
+
readonly prompter: Prompter;
|
|
20
|
+
readonly runner?: CommandRunner;
|
|
21
|
+
readonly catalog?: AdapterCatalog;
|
|
22
|
+
};
|
|
23
|
+
export declare function runCli(argv: readonly string[], io: CliIo): Promise<number>;
|
|
24
|
+
/**
|
|
25
|
+
* Runs `aventara` over this process — its arguments, its terminal, its exit
|
|
26
|
+
* code. Called by the bin's entry (`aventara.bin.ts`) once the Node guard has
|
|
27
|
+
* admitted this Node; importing this module runs nothing, which lets `runCli`
|
|
28
|
+
* be tested in process.
|
|
29
|
+
*/
|
|
30
|
+
export declare function runFromProcess(): Promise<void>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { ConflictsNotConfirmedError } from "./apply/conflict.confirmer.js";
|
|
3
|
+
import { ADAPTER_CATALOG } from "./catalog/adapter.catalog.generated.js";
|
|
4
|
+
import { CliCommandError, parseCliCommand, USAGE, } from "./command/command.parser.js";
|
|
5
|
+
import { ProjectRefusedError } from "./project/project.inspector.js";
|
|
6
|
+
import { runCommand } from "./run/command.runner.js";
|
|
7
|
+
import { InstallFailedError, runInit } from "./run/init.orchestrator.js";
|
|
8
|
+
import { runNew } from "./run/new.orchestrator.js";
|
|
9
|
+
import { UnansweredQuestionsError, } from "./wizard/answer.resolver.js";
|
|
10
|
+
import { createReadlinePrompter, WizardCancelledError, } from "./wizard/readline.prompter.js";
|
|
11
|
+
import { WizardAnswerError } from "./wizard/wizard.questions.js";
|
|
12
|
+
const REFUSALS = [
|
|
13
|
+
CliCommandError,
|
|
14
|
+
WizardAnswerError,
|
|
15
|
+
UnansweredQuestionsError,
|
|
16
|
+
WizardCancelledError,
|
|
17
|
+
ProjectRefusedError,
|
|
18
|
+
ConflictsNotConfirmedError,
|
|
19
|
+
InstallFailedError,
|
|
20
|
+
];
|
|
21
|
+
const MANIFEST = new URL("../package.json", import.meta.url);
|
|
22
|
+
function ownVersion() {
|
|
23
|
+
return JSON.parse(readFileSync(MANIFEST, "utf8"))
|
|
24
|
+
.version;
|
|
25
|
+
}
|
|
26
|
+
export async function runCli(argv, io) {
|
|
27
|
+
try {
|
|
28
|
+
const command = parseCliCommand(argv);
|
|
29
|
+
if (command.command === "help") {
|
|
30
|
+
io.stdout(USAGE);
|
|
31
|
+
return 0;
|
|
32
|
+
}
|
|
33
|
+
if (command.command === "version") {
|
|
34
|
+
io.stdout(`${ownVersion()}\n`);
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
if (command.command === "init") {
|
|
38
|
+
return await runInit(command, {
|
|
39
|
+
directory: io.cwd,
|
|
40
|
+
env: io.env,
|
|
41
|
+
stdout: io.stdout,
|
|
42
|
+
stderr: io.stderr,
|
|
43
|
+
prompter: io.prompter,
|
|
44
|
+
runner: io.runner ?? runCommand,
|
|
45
|
+
catalog: io.catalog ?? ADAPTER_CATALOG,
|
|
46
|
+
aventaraVersion: ownVersion(),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return await runNew(command, {
|
|
50
|
+
cwd: io.cwd,
|
|
51
|
+
env: io.env,
|
|
52
|
+
stdout: io.stdout,
|
|
53
|
+
stderr: io.stderr,
|
|
54
|
+
prompter: io.prompter,
|
|
55
|
+
runner: io.runner ?? runCommand,
|
|
56
|
+
catalog: io.catalog ?? ADAPTER_CATALOG,
|
|
57
|
+
aventaraVersion: ownVersion(),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (REFUSALS.some((refusal) => error instanceof refusal)) {
|
|
62
|
+
io.stderr(`aventara: ${error.message}.\n`);
|
|
63
|
+
return 1;
|
|
64
|
+
}
|
|
65
|
+
io.stderr(`${String(error instanceof Error ? error.stack : error)}\n`);
|
|
66
|
+
return 1;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Runs `aventara` over this process — its arguments, its terminal, its exit
|
|
71
|
+
* code. Called by the bin's entry (`aventara.bin.ts`) once the Node guard has
|
|
72
|
+
* admitted this Node; importing this module runs nothing, which lets `runCli`
|
|
73
|
+
* be tested in process.
|
|
74
|
+
*/
|
|
75
|
+
export async function runFromProcess() {
|
|
76
|
+
const prompter = createReadlinePrompter({
|
|
77
|
+
input: process.stdin,
|
|
78
|
+
output: process.stdout,
|
|
79
|
+
});
|
|
80
|
+
try {
|
|
81
|
+
process.exitCode = await runCli(process.argv.slice(2), {
|
|
82
|
+
cwd: process.cwd(),
|
|
83
|
+
env: process.env,
|
|
84
|
+
stdout: (text) => {
|
|
85
|
+
process.stdout.write(text);
|
|
86
|
+
},
|
|
87
|
+
stderr: (text) => {
|
|
88
|
+
process.stderr.write(text);
|
|
89
|
+
},
|
|
90
|
+
prompter,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
prompter.close();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type QuestionId } from "../wizard/wizard.questions.js";
|
|
2
|
+
/**
|
|
3
|
+
* The `aventara` command line (R1, R2, Q16): `new <name>` and `init`, each
|
|
4
|
+
* question's flag from the wizard's table, and the options that are not
|
|
5
|
+
* questions. Unknown, misplaced or repeated arguments are refused in one
|
|
6
|
+
* sentence; a flag's VALUE is the question's to judge, because what is valid
|
|
7
|
+
* (`--db`'s providers) depends on the catalog and on earlier answers.
|
|
8
|
+
*/
|
|
9
|
+
export type ScaffoldCommandName = "new" | "init";
|
|
10
|
+
export type ScaffoldCommand = {
|
|
11
|
+
readonly command: ScaffoldCommandName;
|
|
12
|
+
/** Raw answers by question, from flags and `new`'s positional `<name>`. */
|
|
13
|
+
readonly given: Readonly<Partial<Record<QuestionId, string>>>;
|
|
14
|
+
readonly skipInstall: boolean;
|
|
15
|
+
/** `new` only: passed through to `nest new` (Q16-7). */
|
|
16
|
+
readonly skipGit: boolean;
|
|
17
|
+
/** Accepts each unanswered question's default and confirms overwrites (R5). */
|
|
18
|
+
readonly yes: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type CliCommand = {
|
|
21
|
+
readonly command: "help";
|
|
22
|
+
} | {
|
|
23
|
+
readonly command: "version";
|
|
24
|
+
} | ScaffoldCommand;
|
|
25
|
+
/** The command line cannot be understood. A refusal: one sentence, exit 1. */
|
|
26
|
+
export declare class CliCommandError extends Error {
|
|
27
|
+
readonly name = "CliCommandError";
|
|
28
|
+
}
|
|
29
|
+
export declare const USAGE: string;
|
|
30
|
+
/** @throws CliCommandError when `argv` is not a command this CLI has. */
|
|
31
|
+
export declare function parseCliCommand(argv: readonly string[]): CliCommand;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { INIT_FLAG_QUESTIONS, NEW_QUESTIONS, } from "../wizard/wizard.questions.js";
|
|
2
|
+
/** The command line cannot be understood. A refusal: one sentence, exit 1. */
|
|
3
|
+
export class CliCommandError extends Error {
|
|
4
|
+
name = "CliCommandError";
|
|
5
|
+
}
|
|
6
|
+
/** The options that are not wizard questions. */
|
|
7
|
+
const OPTIONS = [
|
|
8
|
+
{
|
|
9
|
+
flag: "--skip-install",
|
|
10
|
+
key: "skipInstall",
|
|
11
|
+
commands: ["new", "init"],
|
|
12
|
+
summary: "write the files, and print the install command instead of running it",
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
flag: "--skip-git",
|
|
16
|
+
key: "skipGit",
|
|
17
|
+
commands: ["new"],
|
|
18
|
+
summary: "create no git repository (passed to nest new)",
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
flag: "--yes",
|
|
22
|
+
alias: "-y",
|
|
23
|
+
key: "yes",
|
|
24
|
+
commands: ["new", "init"],
|
|
25
|
+
summary: "accept every unanswered question's default, and confirm overwriting existing content",
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
const QUESTIONS = {
|
|
29
|
+
new: NEW_QUESTIONS,
|
|
30
|
+
init: INIT_FLAG_QUESTIONS,
|
|
31
|
+
};
|
|
32
|
+
const HELP = "run `aventara --help` for usage";
|
|
33
|
+
function usageLines(command) {
|
|
34
|
+
const rows = [
|
|
35
|
+
...QUESTIONS[command]
|
|
36
|
+
.filter((question) => question.flag.startsWith("--"))
|
|
37
|
+
.map((question) => [
|
|
38
|
+
`${question.flag} ${question.placeholder ?? "<value>"}`,
|
|
39
|
+
`${question.label}; asked when omitted`,
|
|
40
|
+
]),
|
|
41
|
+
...OPTIONS.filter((option) => option.commands.includes(command)).map((option) => [
|
|
42
|
+
`${option.alias === undefined ? "" : `${option.alias}, `}${option.flag}`,
|
|
43
|
+
option.summary,
|
|
44
|
+
]),
|
|
45
|
+
];
|
|
46
|
+
const width = Math.max(...rows.map(([left]) => left.length));
|
|
47
|
+
return rows
|
|
48
|
+
.map(([left, right]) => ` ${left.padEnd(width)} ${right}`)
|
|
49
|
+
.join("\n");
|
|
50
|
+
}
|
|
51
|
+
export const USAGE = `aventara — scaffold an Aventara server on NestJS.
|
|
52
|
+
|
|
53
|
+
Usage:
|
|
54
|
+
aventara new <name> [options] Create a NestJS project with Aventara (nest new, then init).
|
|
55
|
+
${usageLines("new")}
|
|
56
|
+
|
|
57
|
+
aventara init [options] Add Aventara to the NestJS 12 project in this directory.
|
|
58
|
+
${usageLines("init")}
|
|
59
|
+
|
|
60
|
+
aventara --help Print this and exit 0.
|
|
61
|
+
aventara --version Print this CLI's version and exit 0.
|
|
62
|
+
|
|
63
|
+
On a terminal every unanswered question is asked; anywhere else, pass its flag
|
|
64
|
+
or --yes, or the run stops before writing anything.
|
|
65
|
+
`;
|
|
66
|
+
/** @throws CliCommandError when `argv` is not a command this CLI has. */
|
|
67
|
+
export function parseCliCommand(argv) {
|
|
68
|
+
const [command, ...rest] = argv;
|
|
69
|
+
if (command === undefined) {
|
|
70
|
+
throw new CliCommandError(`a command is required: new or init; ${HELP}`);
|
|
71
|
+
}
|
|
72
|
+
if (command === "--help" || command === "-h") {
|
|
73
|
+
return { command: "help" };
|
|
74
|
+
}
|
|
75
|
+
if (command === "--version") {
|
|
76
|
+
return { command: "version" };
|
|
77
|
+
}
|
|
78
|
+
if (command !== "new" && command !== "init") {
|
|
79
|
+
throw new CliCommandError(`unknown command ${JSON.stringify(command)}: the commands are new and init; ${HELP}`);
|
|
80
|
+
}
|
|
81
|
+
const questions = QUESTIONS[command];
|
|
82
|
+
const given = {};
|
|
83
|
+
const options = {};
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const positional = questions.find((question) => !question.flag.startsWith("--"));
|
|
86
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
87
|
+
const argument = rest[index];
|
|
88
|
+
if (!argument.startsWith("-")) {
|
|
89
|
+
if (positional === undefined || given[positional.id] !== undefined) {
|
|
90
|
+
throw new CliCommandError(`unexpected argument ${JSON.stringify(argument)}: aventara ${command} takes ${positional === undefined ? "no" : "one"} positional argument; ${HELP}`);
|
|
91
|
+
}
|
|
92
|
+
given[positional.id] = argument;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const equals = argument.indexOf("=");
|
|
96
|
+
const flag = equals === -1 ? argument : argument.slice(0, equals);
|
|
97
|
+
const inline = equals === -1 ? undefined : argument.slice(equals + 1);
|
|
98
|
+
const question = questions.find((candidate) => candidate.flag === flag);
|
|
99
|
+
const option = OPTIONS.find((candidate) => candidate.flag === flag || candidate.alias === flag);
|
|
100
|
+
const canonical = question?.flag ?? option?.flag ?? flag;
|
|
101
|
+
if (question === undefined && option === undefined) {
|
|
102
|
+
const elsewhere = ["new", "init"].filter((other) => QUESTIONS[other].some((candidate) => candidate.flag === flag));
|
|
103
|
+
throw new CliCommandError(elsewhere.length === 0
|
|
104
|
+
? `unknown flag ${flag}; ${HELP}`
|
|
105
|
+
: `${flag} is a flag of aventara ${elsewhere.join(" and ")}, not aventara ${command}; ${HELP}`);
|
|
106
|
+
}
|
|
107
|
+
if (option !== undefined && !option.commands.includes(command)) {
|
|
108
|
+
throw new CliCommandError(`${option.flag} is a flag of aventara ${option.commands.join(" and ")}, not aventara ${command}; ${HELP}`);
|
|
109
|
+
}
|
|
110
|
+
if (seen.has(canonical)) {
|
|
111
|
+
throw new CliCommandError(`${canonical} was given twice; ${HELP}`);
|
|
112
|
+
}
|
|
113
|
+
seen.add(canonical);
|
|
114
|
+
if (question === undefined) {
|
|
115
|
+
if (inline !== undefined) {
|
|
116
|
+
throw new CliCommandError(`${canonical} takes no value; ${HELP}`);
|
|
117
|
+
}
|
|
118
|
+
options[option.key] = true;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const value = inline ?? rest[index + 1];
|
|
122
|
+
if (value === undefined ||
|
|
123
|
+
(inline === undefined && value.startsWith("-"))) {
|
|
124
|
+
throw new CliCommandError(`${canonical} takes a value; ${HELP}`);
|
|
125
|
+
}
|
|
126
|
+
if (inline === undefined) {
|
|
127
|
+
index += 1;
|
|
128
|
+
}
|
|
129
|
+
given[question.id] = value;
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
command,
|
|
133
|
+
given,
|
|
134
|
+
skipInstall: options.skipInstall === true,
|
|
135
|
+
skipGit: options.skipGit === true,
|
|
136
|
+
yes: options.yes === true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The sentence a bin answers on `version`, or `undefined` when `range` admits it. */
|
|
2
|
+
export declare function nodeVersionRefusal(bin: string, packageName: string, range: string, version: string): string | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Reads the manifest at `manifestUrl`; on a Node its `engines.node` does not
|
|
5
|
+
* admit, writes the sentence to stderr, sets exit code 1 and answers `true` —
|
|
6
|
+
* the bin then loads nothing else.
|
|
7
|
+
*/
|
|
8
|
+
export declare function refuseUnsupportedNode(bin: string, manifestUrl: URL): boolean;
|