@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,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The merges `aventara init` makes into files a project already has —
|
|
3
|
+
* `package.json`, `.env`, `.gitignore`, `pnpm-workspace.yaml` — each answering
|
|
4
|
+
* the merged text and the **conflicts**: values already there that differ from
|
|
5
|
+
* what would be written (the `generateAt` rule's "content the tool did not
|
|
6
|
+
* produce"). A value already equal to what would be written is no change (P3);
|
|
7
|
+
* a conflict is replaced only once confirmed (`conflict.confirmer.ts`), so each
|
|
8
|
+
* merge is computed with conflicts both kept and replaced.
|
|
9
|
+
*/
|
|
10
|
+
function sorted(record) {
|
|
11
|
+
return Object.fromEntries(Object.entries(record).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
|
|
12
|
+
}
|
|
13
|
+
/** `package.json`, in npm's own layout: two-space JSON, dependencies sorted. */
|
|
14
|
+
export function mergePackageManifest(text, changes, replaceConflicts) {
|
|
15
|
+
const manifest = JSON.parse(text);
|
|
16
|
+
const conflicts = [];
|
|
17
|
+
for (const field of ["dependencies", "devDependencies"]) {
|
|
18
|
+
const current = {
|
|
19
|
+
...(manifest[field] ?? {}),
|
|
20
|
+
};
|
|
21
|
+
for (const [name, version] of Object.entries(changes[field])) {
|
|
22
|
+
const existing = current[name];
|
|
23
|
+
if (existing !== undefined && existing !== version) {
|
|
24
|
+
conflicts.push(`package.json's ${field}["${name}"] (${existing})`);
|
|
25
|
+
if (!replaceConflicts) {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
current[name] = version;
|
|
30
|
+
}
|
|
31
|
+
manifest[field] = sorted(current);
|
|
32
|
+
}
|
|
33
|
+
const scripts = {
|
|
34
|
+
...(manifest.scripts ?? {}),
|
|
35
|
+
};
|
|
36
|
+
for (const [name, command] of Object.entries(changes.scripts)) {
|
|
37
|
+
const existing = scripts[name];
|
|
38
|
+
if (existing !== undefined && existing !== command) {
|
|
39
|
+
conflicts.push(`package.json's scripts["${name}"]`);
|
|
40
|
+
if (!replaceConflicts) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
scripts[name] = command;
|
|
45
|
+
}
|
|
46
|
+
for (const [name, { from, to }] of Object.entries(changes.rewrites)) {
|
|
47
|
+
const existing = scripts[name];
|
|
48
|
+
if (existing === undefined || existing === to) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
if (existing !== from) {
|
|
52
|
+
conflicts.push(`package.json's scripts["${name}"]`);
|
|
53
|
+
if (!replaceConflicts) {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
scripts[name] = to;
|
|
58
|
+
}
|
|
59
|
+
manifest.scripts = scripts;
|
|
60
|
+
return { text: `${JSON.stringify(manifest, null, 2)}\n`, conflicts };
|
|
61
|
+
}
|
|
62
|
+
const ENV_LINE = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(.*?)\s*$/;
|
|
63
|
+
function unquoted(value) {
|
|
64
|
+
const quote = value[0];
|
|
65
|
+
return (quote === '"' || quote === "'") && value.endsWith(quote)
|
|
66
|
+
? value.slice(1, -1)
|
|
67
|
+
: value;
|
|
68
|
+
}
|
|
69
|
+
/** `.env`: a missing key is appended as `KEY="value"`, after a final newline (B5). */
|
|
70
|
+
export function mergeEnvFile(text, entries, replaceConflicts) {
|
|
71
|
+
const lines = text === undefined || text === ""
|
|
72
|
+
? []
|
|
73
|
+
: text.replace(/\n$/, "").split("\n");
|
|
74
|
+
const conflicts = [];
|
|
75
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
76
|
+
const at = lines.findIndex((line) => ENV_LINE.exec(line)?.[1] === key);
|
|
77
|
+
const line = `${key}="${value}"`;
|
|
78
|
+
if (at === -1) {
|
|
79
|
+
lines.push(line);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const existing = unquoted(ENV_LINE.exec(lines[at])?.[2] ?? "");
|
|
83
|
+
if (existing === value) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
conflicts.push(`.env's ${key}`);
|
|
87
|
+
if (replaceConflicts) {
|
|
88
|
+
lines[at] = line;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return { text: `${lines.join("\n")}\n`, conflicts };
|
|
92
|
+
}
|
|
93
|
+
/** `.gitignore`: missing lines appended, nothing else touched. */
|
|
94
|
+
export function mergeGitignore(text, additions) {
|
|
95
|
+
const lines = text === undefined || text === ""
|
|
96
|
+
? []
|
|
97
|
+
: text.replace(/\n$/, "").split("\n");
|
|
98
|
+
const missing = additions.filter((line) => !lines.some((present) => present.trim() === line));
|
|
99
|
+
return {
|
|
100
|
+
text: missing.length === 0 && text !== undefined
|
|
101
|
+
? text
|
|
102
|
+
: `${[...lines, ...missing].join("\n")}\n`,
|
|
103
|
+
conflicts: [],
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* `pnpm-workspace.yaml`'s `allowBuilds:` (B9: pnpm 12 refuses install scripts
|
|
108
|
+
* until they are allowed — what `pnpm approve-builds` writes). Keys missing
|
|
109
|
+
* under an existing `allowBuilds:` are added there; otherwise the block is
|
|
110
|
+
* appended.
|
|
111
|
+
*/
|
|
112
|
+
export function mergePnpmWorkspace(text, packages) {
|
|
113
|
+
const quoted = (name) => /^[A-Za-z0-9_-][A-Za-z0-9._-]*$/.test(name) ? name : `"${name}"`;
|
|
114
|
+
const lines = text === undefined || text === ""
|
|
115
|
+
? []
|
|
116
|
+
: text.replace(/\n$/, "").split("\n");
|
|
117
|
+
const header = lines.findIndex((line) => /^allowBuilds:\s*$/.test(line));
|
|
118
|
+
const present = new Set();
|
|
119
|
+
if (header !== -1) {
|
|
120
|
+
for (let at = header + 1; at < lines.length && /^\s+\S/.test(lines[at]); at += 1) {
|
|
121
|
+
const name = /^\s+["']?([^"':]+)["']?\s*:/.exec(lines[at])?.[1];
|
|
122
|
+
if (name !== undefined) {
|
|
123
|
+
present.add(name);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const missing = packages.filter((name) => !present.has(name));
|
|
128
|
+
if (missing.length === 0 && text !== undefined) {
|
|
129
|
+
return { text, conflicts: [] };
|
|
130
|
+
}
|
|
131
|
+
const entries = missing.map((name) => ` ${quoted(name)}: true`);
|
|
132
|
+
if (header === -1) {
|
|
133
|
+
lines.push("allowBuilds:", ...entries);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
lines.splice(header + 1, 0, ...entries);
|
|
137
|
+
}
|
|
138
|
+
return { text: `${lines.join("\n")}\n`, conflicts: [] };
|
|
139
|
+
}
|
|
@@ -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, commandUsage, 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(command.topic === undefined ? USAGE : commandUsage(command.topic));
|
|
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,36 @@
|
|
|
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
|
+
/** `--help`; with `topic`, `aventara <topic> --help` (pilot.1). */
|
|
22
|
+
{
|
|
23
|
+
readonly command: "help";
|
|
24
|
+
readonly topic?: ScaffoldCommandName;
|
|
25
|
+
} | {
|
|
26
|
+
readonly command: "version";
|
|
27
|
+
} | ScaffoldCommand;
|
|
28
|
+
/** The command line cannot be understood. A refusal: one sentence, exit 1. */
|
|
29
|
+
export declare class CliCommandError extends Error {
|
|
30
|
+
readonly name = "CliCommandError";
|
|
31
|
+
}
|
|
32
|
+
/** `aventara <command> --help` (pilot.1): that command's usage alone. */
|
|
33
|
+
export declare function commandUsage(command: ScaffoldCommandName): string;
|
|
34
|
+
export declare const USAGE: string;
|
|
35
|
+
/** @throws CliCommandError when `argv` is not a command this CLI has. */
|
|
36
|
+
export declare function parseCliCommand(argv: readonly string[]): CliCommand;
|