ai-check-template 0.2.0-alpha.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 +201 -0
- package/README-ja.md +151 -0
- package/README.md +149 -0
- package/bin/ai-check-template.mjs +7 -0
- package/docs/cli.md +348 -0
- package/package-templates/.claude/README.md +83 -0
- package/package-templates/.claude/rules/test-rules.md +46 -0
- package/package-templates/.claude/settings.hook-fragment.json +25 -0
- package/package-templates/README.md +56 -0
- package/package-templates/ci-examples/README.md +134 -0
- package/package-templates/ci-examples/github-actions/ai-check-fast.yml +49 -0
- package/package-templates/ci-examples/github-actions/ai-check.yml +58 -0
- package/package-templates/ci-examples/github-actions/ai-quality-call.yml +26 -0
- package/package-templates/ci-examples/github-actions/ai-quality-reusable.yml +113 -0
- package/package-templates/docs/philosophy/formal-name-match.md +182 -0
- package/package-templates/docs/philosophy/given-when-then.md +206 -0
- package/package-templates/docs/philosophy/qa-techniques.md +235 -0
- package/package-templates/docs/philosophy/test-pyramid.md +171 -0
- package/package-templates/docs/test-design-template.md +173 -0
- package/package-templates/package.scripts.fragment.json +6 -0
- package/package-templates/profiles/README.md +89 -0
- package/package-templates/profiles/expo-rn/README.md +80 -0
- package/package-templates/profiles/node-cli/README.md +93 -0
- package/package-templates/profiles/react-nextjs/README.md +82 -0
- package/package-templates/profiles/react-vanilla/README.md +73 -0
- package/package-templates/profiles/supabase-rls/README.md +89 -0
- package/package-templates/prompts/README.md +94 -0
- package/package-templates/prompts/boundary-value.md +94 -0
- package/package-templates/prompts/decision-table.md +82 -0
- package/package-templates/prompts/diagnostic-repair.md +149 -0
- package/package-templates/prompts/plan-first.md +122 -0
- package/package-templates/prompts/rls-permission.md +94 -0
- package/package-templates/prompts/state-transition.md +81 -0
- package/package-templates/scripts/README.md +78 -0
- package/package-templates/scripts/ai-check-fast.sh +20 -0
- package/package-templates/scripts/ai-check.sh +22 -0
- package/package.json +47 -0
- package/src/cli/ci-workflows.mjs +104 -0
- package/src/cli/claude-hooks.mjs +94 -0
- package/src/cli/dependency-installer.mjs +164 -0
- package/src/cli/doctor.mjs +392 -0
- package/src/cli/index.mjs +80 -0
- package/src/cli/init.mjs +433 -0
- package/src/cli/install-state.mjs +242 -0
- package/src/cli/package-manager.mjs +78 -0
- package/src/cli/profile-diagnostics.mjs +160 -0
- package/src/cli/profile-docs.mjs +31 -0
- package/src/cli/profile-scripts.mjs +96 -0
- package/src/cli/profile.mjs +59 -0
- package/src/cli/update.mjs +537 -0
- package/src/cli/utils.mjs +75 -0
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { CliError, pathExists } from "./utils.mjs";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_PACKAGE_MANAGER = "pnpm";
|
|
6
|
+
|
|
7
|
+
const VALID_PACKAGE_MANAGERS = new Set(["pnpm", "npm", "yarn", "bun"]);
|
|
8
|
+
|
|
9
|
+
const LOCKFILE_PACKAGE_MANAGERS = [
|
|
10
|
+
["pnpm-lock.yaml", "pnpm"],
|
|
11
|
+
["package-lock.json", "npm"],
|
|
12
|
+
["npm-shrinkwrap.json", "npm"],
|
|
13
|
+
["yarn.lock", "yarn"],
|
|
14
|
+
["bun.lock", "bun"],
|
|
15
|
+
["bun.lockb", "bun"],
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function validatePackageManager(packageManager) {
|
|
19
|
+
if (!VALID_PACKAGE_MANAGERS.has(packageManager)) {
|
|
20
|
+
throw new CliError(
|
|
21
|
+
`--package-manager must be one of: ${[...VALID_PACKAGE_MANAGERS].join(", ")}`,
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return packageManager;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function detectPackageManager(targetDir) {
|
|
29
|
+
const packageManager = await packageManagerFromPackageJson(targetDir);
|
|
30
|
+
if (packageManager) {
|
|
31
|
+
return packageManager;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
for (const [lockfile, detectedPackageManager] of LOCKFILE_PACKAGE_MANAGERS) {
|
|
35
|
+
if (await pathExists(path.join(targetDir, lockfile))) {
|
|
36
|
+
return detectedPackageManager;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return DEFAULT_PACKAGE_MANAGER;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function scriptCommand(packageManager, scriptName) {
|
|
44
|
+
const validated = validatePackageManager(packageManager);
|
|
45
|
+
|
|
46
|
+
if (validated === "npm") {
|
|
47
|
+
return `npm run ${scriptName}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (validated === "bun") {
|
|
51
|
+
return `bun run ${scriptName}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return `${validated} ${scriptName}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function packageManagerFromPackageJson(targetDir) {
|
|
58
|
+
const packageJsonPath = path.join(targetDir, "package.json");
|
|
59
|
+
|
|
60
|
+
if (!(await pathExists(packageJsonPath))) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let packageJson;
|
|
65
|
+
try {
|
|
66
|
+
packageJson = JSON.parse(await fs.readFile(packageJsonPath, "utf8"));
|
|
67
|
+
} catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
const value = packageJson.packageManager;
|
|
71
|
+
|
|
72
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const [name] = value.split("@");
|
|
77
|
+
return validatePackageManager(name);
|
|
78
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const CHECK_SCRIPT_NAMES = ["ai:check", "ai:check:fast"];
|
|
2
|
+
|
|
3
|
+
const SCRIPT_REFERENCE_PATTERNS = [
|
|
4
|
+
/\b(?:pnpm|yarn)\s+([A-Za-z0-9:_-]+)/g,
|
|
5
|
+
/\b(?:npm|bun)\s+run\s+([A-Za-z0-9:_-]+)/g,
|
|
6
|
+
];
|
|
7
|
+
|
|
8
|
+
const PACKAGE_MANAGER_SUBCOMMANDS = new Set([
|
|
9
|
+
"add",
|
|
10
|
+
"audit",
|
|
11
|
+
"cache",
|
|
12
|
+
"config",
|
|
13
|
+
"create",
|
|
14
|
+
"dlx",
|
|
15
|
+
"exec",
|
|
16
|
+
"help",
|
|
17
|
+
"init",
|
|
18
|
+
"install",
|
|
19
|
+
"link",
|
|
20
|
+
"outdated",
|
|
21
|
+
"pack",
|
|
22
|
+
"publish",
|
|
23
|
+
"remove",
|
|
24
|
+
"run",
|
|
25
|
+
"store",
|
|
26
|
+
"unlink",
|
|
27
|
+
"update",
|
|
28
|
+
"upgrade",
|
|
29
|
+
"version",
|
|
30
|
+
"why",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
export function diagnoseProfileScripts(profile, packageJson) {
|
|
34
|
+
const scripts = packageJson?.scripts && typeof packageJson.scripts === "object"
|
|
35
|
+
? packageJson.scripts
|
|
36
|
+
: {};
|
|
37
|
+
const warnings = [];
|
|
38
|
+
const base = profile?.base;
|
|
39
|
+
const addons = new Set(profile?.addons ?? []);
|
|
40
|
+
|
|
41
|
+
if (base === "react-nextjs") {
|
|
42
|
+
if (!hasScriptOrCommand(scripts, "doctor")) {
|
|
43
|
+
warnings.push(profileWarning("React Next.js profile recommends a React Doctor check."));
|
|
44
|
+
}
|
|
45
|
+
if (!hasScriptOrCommand(scripts, "test:e2e:smoke")) {
|
|
46
|
+
warnings.push(profileWarning("React Next.js profile recommends a Playwright smoke E2E script."));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (base === "react-vanilla" && hasCommandToken(scripts, "next lint")) {
|
|
51
|
+
warnings.push(profileWarning("React vanilla profile should avoid Next.js-specific lint scripts."));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (base === "expo-rn") {
|
|
55
|
+
if (hasCommandToken(scripts, "playwright")) {
|
|
56
|
+
warnings.push(profileWarning("Expo React Native profile recommends Maestro or Detox instead of Playwright."));
|
|
57
|
+
}
|
|
58
|
+
if (hasCommandToken(scripts, "react-doctor")) {
|
|
59
|
+
warnings.push(profileWarning("Expo React Native profile does not support React Doctor as a merge gate."));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (base === "node-cli") {
|
|
64
|
+
if (hasScriptOrCommand(scripts, "test:e2e") || hasCommandToken(scripts, "playwright")) {
|
|
65
|
+
warnings.push(profileWarning("Node CLI profile usually replaces UI E2E checks with CLI integration tests."));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (addons.has("supabase-rls")) {
|
|
70
|
+
if (!hasScriptName(scripts, "test:db")) {
|
|
71
|
+
warnings.push(addonWarning("Supabase RLS addon recommends a test:db script for pgTAP or DB-level tests."));
|
|
72
|
+
}
|
|
73
|
+
if (!hasScriptName(scripts, "test:integration:rls")) {
|
|
74
|
+
warnings.push(addonWarning("Supabase RLS addon recommends a test:integration:rls script."));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
warnings.push(...missingReferencedScriptWarnings(scripts));
|
|
79
|
+
|
|
80
|
+
return warnings;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function profileWarning(message) {
|
|
84
|
+
return {
|
|
85
|
+
code: "profile-advice",
|
|
86
|
+
path: "package.json",
|
|
87
|
+
message,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function addonWarning(message) {
|
|
92
|
+
return {
|
|
93
|
+
code: "profile-addon-advice",
|
|
94
|
+
path: "package.json",
|
|
95
|
+
message,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function scriptWarning(message) {
|
|
100
|
+
return {
|
|
101
|
+
code: "script-advice",
|
|
102
|
+
path: "package.json",
|
|
103
|
+
message,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function hasScriptName(scripts, name) {
|
|
108
|
+
return Object.hasOwn(scripts, name);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function hasScriptOrCommand(scripts, token) {
|
|
112
|
+
return hasScriptName(scripts, token) || hasCommandToken(scripts, token);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function hasCommandToken(scripts, token) {
|
|
116
|
+
const normalizedToken = token.toLowerCase();
|
|
117
|
+
return Object.values(scripts).some(
|
|
118
|
+
(command) => typeof command === "string" && command.toLowerCase().includes(normalizedToken),
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function missingReferencedScriptWarnings(scripts) {
|
|
123
|
+
return [...missingReferencedScripts(scripts)].map((scriptName) => (
|
|
124
|
+
scriptWarning(`ai:check references missing package script: ${scriptName}`)
|
|
125
|
+
));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function missingReferencedScripts(scripts) {
|
|
129
|
+
const referenced = new Set();
|
|
130
|
+
|
|
131
|
+
for (const scriptName of CHECK_SCRIPT_NAMES) {
|
|
132
|
+
for (const reference of referencedScriptsInCommand(scripts[scriptName])) {
|
|
133
|
+
referenced.add(reference);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return new Set([...referenced].filter((scriptName) => !hasScriptName(scripts, scriptName)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function referencedScriptsInCommand(command) {
|
|
141
|
+
if (typeof command !== "string") {
|
|
142
|
+
return [];
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const references = [];
|
|
146
|
+
for (const pattern of SCRIPT_REFERENCE_PATTERNS) {
|
|
147
|
+
for (const match of command.matchAll(pattern)) {
|
|
148
|
+
const scriptName = match[1];
|
|
149
|
+
if (!isPackageManagerSubcommand(scriptName)) {
|
|
150
|
+
references.push(scriptName);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return references;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function isPackageManagerSubcommand(scriptName) {
|
|
159
|
+
return PACKAGE_MANAGER_SUBCOMMANDS.has(scriptName);
|
|
160
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { parseProfiles } from "./profile.mjs";
|
|
2
|
+
import { fromTemplates } from "./utils.mjs";
|
|
3
|
+
|
|
4
|
+
const TARGET_ROOT = "docs/ai-check-template";
|
|
5
|
+
|
|
6
|
+
const COMMON_DOC_FILES = [
|
|
7
|
+
["docs/test-design-template.md", `${TARGET_ROOT}/docs/test-design-template.md`],
|
|
8
|
+
["docs/philosophy/formal-name-match.md", `${TARGET_ROOT}/docs/philosophy/formal-name-match.md`],
|
|
9
|
+
["docs/philosophy/given-when-then.md", `${TARGET_ROOT}/docs/philosophy/given-when-then.md`],
|
|
10
|
+
["docs/philosophy/qa-techniques.md", `${TARGET_ROOT}/docs/philosophy/qa-techniques.md`],
|
|
11
|
+
["docs/philosophy/test-pyramid.md", `${TARGET_ROOT}/docs/philosophy/test-pyramid.md`],
|
|
12
|
+
["prompts/diagnostic-repair.md", `${TARGET_ROOT}/prompts/diagnostic-repair.md`],
|
|
13
|
+
["profiles/README.md", `${TARGET_ROOT}/profiles/README.md`],
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export function getProfileDocFiles(input = "react-nextjs") {
|
|
17
|
+
const profile = typeof input === "string" ? parseProfiles(input) : input;
|
|
18
|
+
const files = [...COMMON_DOC_FILES];
|
|
19
|
+
|
|
20
|
+
for (const profileName of [profile.base, ...(profile.addons ?? [])]) {
|
|
21
|
+
files.push([
|
|
22
|
+
`profiles/${profileName}/README.md`,
|
|
23
|
+
`${TARGET_ROOT}/profiles/${profileName}/README.md`,
|
|
24
|
+
]);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return files.map(([sourceRelativePath, targetRelativePath]) => ({
|
|
28
|
+
sourcePath: fromTemplates(...sourceRelativePath.split("/")),
|
|
29
|
+
relativePath: targetRelativePath,
|
|
30
|
+
}));
|
|
31
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { parseProfiles } from "./profile.mjs";
|
|
2
|
+
import { DEFAULT_PACKAGE_MANAGER, scriptCommand } from "./package-manager.mjs";
|
|
3
|
+
|
|
4
|
+
const BASE_PROFILE_SCRIPTS = {
|
|
5
|
+
"react-nextjs": {
|
|
6
|
+
"ai:check": "pnpm typecheck && pnpm lint && pnpm doctor && pnpm deadcode && pnpm test && pnpm test:e2e:smoke",
|
|
7
|
+
"ai:check:fast": "pnpm typecheck && pnpm lint && pnpm test:unit",
|
|
8
|
+
doctor: "npx -y react-doctor@latest . --fail-on warning",
|
|
9
|
+
deadcode: "knip",
|
|
10
|
+
},
|
|
11
|
+
"react-vanilla": {
|
|
12
|
+
"ai:check": "pnpm typecheck && pnpm lint && pnpm deadcode && pnpm test",
|
|
13
|
+
"ai:check:fast": "pnpm typecheck && pnpm lint && pnpm test:unit",
|
|
14
|
+
deadcode: "knip",
|
|
15
|
+
},
|
|
16
|
+
"expo-rn": {
|
|
17
|
+
"ai:check": "pnpm typecheck && pnpm lint && pnpm deadcode && pnpm test && pnpm test:e2e:smoke",
|
|
18
|
+
"ai:check:fast": "pnpm typecheck && pnpm lint && pnpm test:unit",
|
|
19
|
+
deadcode: "knip",
|
|
20
|
+
"test:e2e:smoke": "maestro test .maestro/smoke.yaml",
|
|
21
|
+
},
|
|
22
|
+
"node-cli": {
|
|
23
|
+
"ai:check": "pnpm typecheck && pnpm lint && pnpm deadcode && pnpm test",
|
|
24
|
+
"ai:check:fast": "pnpm typecheck && pnpm lint && pnpm test:unit",
|
|
25
|
+
deadcode: "knip",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ADDON_PROFILE_SCRIPTS = {
|
|
30
|
+
"supabase-rls": {
|
|
31
|
+
"test:db": "supabase test db",
|
|
32
|
+
"test:integration:rls": "vitest run --dir tests/rls",
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const ADDON_CHECK_STEPS = {
|
|
37
|
+
"supabase-rls": ["test:db", "test:integration:rls"],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const COMMON_SUPPORT_SCRIPTS = {
|
|
41
|
+
typecheck: "tsc --noEmit",
|
|
42
|
+
lint: "eslint .",
|
|
43
|
+
test: "vitest run",
|
|
44
|
+
"test:unit": "vitest run --dir tests/unit",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const BASE_PROFILE_SUPPORT_SCRIPTS = {
|
|
48
|
+
"react-nextjs": {
|
|
49
|
+
...COMMON_SUPPORT_SCRIPTS,
|
|
50
|
+
"test:e2e:smoke": "playwright test --grep smoke",
|
|
51
|
+
},
|
|
52
|
+
"react-vanilla": COMMON_SUPPORT_SCRIPTS,
|
|
53
|
+
"expo-rn": COMMON_SUPPORT_SCRIPTS,
|
|
54
|
+
"node-cli": COMMON_SUPPORT_SCRIPTS,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export function getProfileScripts(input = "react-nextjs", options = {}) {
|
|
58
|
+
const profile = typeof input === "string" ? parseProfiles(input) : input;
|
|
59
|
+
const packageManager = options.packageManager ?? DEFAULT_PACKAGE_MANAGER;
|
|
60
|
+
const scripts = { ...BASE_PROFILE_SCRIPTS[profile.base] };
|
|
61
|
+
|
|
62
|
+
for (const addon of profile.addons) {
|
|
63
|
+
Object.assign(scripts, ADDON_PROFILE_SCRIPTS[addon] ?? {});
|
|
64
|
+
for (const step of ADDON_CHECK_STEPS[addon] ?? []) {
|
|
65
|
+
scripts["ai:check"] = appendScriptStep(scripts["ai:check"], scriptCommand(packageManager, step));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return renderPackageManagerScripts(scripts, packageManager);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function getProfileSupportScripts(input = "react-nextjs") {
|
|
73
|
+
const profile = typeof input === "string" ? parseProfiles(input) : input;
|
|
74
|
+
return { ...BASE_PROFILE_SUPPORT_SCRIPTS[profile.base] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function appendScriptStep(command, step) {
|
|
78
|
+
const parts = command.split(" && ").map((part) => part.trim());
|
|
79
|
+
if (parts.includes(step)) {
|
|
80
|
+
return command;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return [...parts, step].join(" && ");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderPackageManagerScripts(scripts, packageManager) {
|
|
87
|
+
return Object.fromEntries(
|
|
88
|
+
Object.entries(scripts).map(([name, command]) => [name, renderScriptCommand(command, packageManager)]),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function renderScriptCommand(command, packageManager) {
|
|
93
|
+
return command.replace(/\bpnpm ([a-zA-Z0-9:_-]+)/g, (_, scriptName) => (
|
|
94
|
+
scriptCommand(packageManager, scriptName)
|
|
95
|
+
));
|
|
96
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { CliError } from "./utils.mjs";
|
|
2
|
+
|
|
3
|
+
const BASE_PROFILES = new Set([
|
|
4
|
+
"react-nextjs",
|
|
5
|
+
"react-vanilla",
|
|
6
|
+
"expo-rn",
|
|
7
|
+
"node-cli",
|
|
8
|
+
]);
|
|
9
|
+
|
|
10
|
+
const ADDON_PROFILES = new Set(["supabase-rls"]);
|
|
11
|
+
|
|
12
|
+
export const supportedProfiles = [
|
|
13
|
+
...BASE_PROFILES,
|
|
14
|
+
...ADDON_PROFILES,
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function parseProfiles(input = "react-nextjs") {
|
|
18
|
+
const names = input
|
|
19
|
+
.split(/[,+]/)
|
|
20
|
+
.map((value) => value.trim())
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
|
|
23
|
+
if (names.length === 0) {
|
|
24
|
+
throw invalidProfile(input);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const uniqueNames = new Set(names);
|
|
28
|
+
if (uniqueNames.size !== names.length) {
|
|
29
|
+
throw new CliError(`Duplicate profile in --profile: ${input}`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const unknown = names.filter(
|
|
33
|
+
(name) => !BASE_PROFILES.has(name) && !ADDON_PROFILES.has(name),
|
|
34
|
+
);
|
|
35
|
+
if (unknown.length > 0) {
|
|
36
|
+
throw invalidProfile(unknown.join(", "));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const baseProfiles = names.filter((name) => BASE_PROFILES.has(name));
|
|
40
|
+
if (baseProfiles.length !== 1) {
|
|
41
|
+
throw new CliError(
|
|
42
|
+
`--profile must include exactly one base profile. Supported base profiles: ${[
|
|
43
|
+
...BASE_PROFILES,
|
|
44
|
+
].join(", ")}`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
base: baseProfiles[0],
|
|
50
|
+
addons: names.filter((name) => ADDON_PROFILES.has(name)),
|
|
51
|
+
all: names,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function invalidProfile(value) {
|
|
56
|
+
return new CliError(
|
|
57
|
+
`Invalid profile: ${value}. Supported profiles: ${supportedProfiles.join(", ")}`,
|
|
58
|
+
);
|
|
59
|
+
}
|