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,164 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { validatePackageManager } from "./package-manager.mjs";
|
|
3
|
+
import { parseProfiles } from "./profile.mjs";
|
|
4
|
+
import { CliError, readJson } from "./utils.mjs";
|
|
5
|
+
|
|
6
|
+
const DEPENDENCY_SECTIONS = [
|
|
7
|
+
"dependencies",
|
|
8
|
+
"devDependencies",
|
|
9
|
+
"peerDependencies",
|
|
10
|
+
"optionalDependencies",
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
const COMMON_DEV_DEPENDENCIES = [
|
|
14
|
+
"typescript",
|
|
15
|
+
"eslint",
|
|
16
|
+
"vitest",
|
|
17
|
+
"knip",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
const BASE_PROFILE_DEV_DEPENDENCIES = {
|
|
21
|
+
"react-nextjs": [
|
|
22
|
+
...COMMON_DEV_DEPENDENCIES,
|
|
23
|
+
"@playwright/test",
|
|
24
|
+
],
|
|
25
|
+
"react-vanilla": COMMON_DEV_DEPENDENCIES,
|
|
26
|
+
"expo-rn": COMMON_DEV_DEPENDENCIES,
|
|
27
|
+
"node-cli": COMMON_DEV_DEPENDENCIES,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
const ADDON_PROFILE_DEV_DEPENDENCIES = {
|
|
31
|
+
"supabase-rls": [],
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export async function planDependencyInstall(packageJsonPath, profileInput, packageManagerInput) {
|
|
35
|
+
const packageJson = await readJson(packageJsonPath);
|
|
36
|
+
const packageManager = validatePackageManager(packageManagerInput);
|
|
37
|
+
const dependencies = getProfileDevDependencies(profileInput);
|
|
38
|
+
const missingDependencies = dependencies.filter((dependency) => !isDependencyDeclared(packageJson, dependency));
|
|
39
|
+
const command = buildInstallCommand(packageManager, missingDependencies);
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
packageManager,
|
|
43
|
+
dependencies,
|
|
44
|
+
missingDependencies,
|
|
45
|
+
...command,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function getProfileDevDependencies(input = "react-nextjs") {
|
|
50
|
+
const profile = typeof input === "string" ? parseProfiles(input) : input;
|
|
51
|
+
const dependencies = new Set(BASE_PROFILE_DEV_DEPENDENCIES[profile.base] ?? []);
|
|
52
|
+
|
|
53
|
+
for (const addon of profile.addons ?? []) {
|
|
54
|
+
for (const dependency of ADDON_PROFILE_DEV_DEPENDENCIES[addon] ?? []) {
|
|
55
|
+
dependencies.add(dependency);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return [...dependencies];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function dependencyInstallOperation(plan, options = {}) {
|
|
63
|
+
const { dryRun = false, path = "package.json" } = options;
|
|
64
|
+
|
|
65
|
+
if (plan.missingDependencies.length === 0) {
|
|
66
|
+
return {
|
|
67
|
+
action: "keep",
|
|
68
|
+
path,
|
|
69
|
+
detail: "dev dependencies already declared",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
action: dryRun ? "would-install" : "install",
|
|
75
|
+
path,
|
|
76
|
+
detail: `dev dependencies ${plan.missingDependencies.join(" ")}`,
|
|
77
|
+
command: plan.commandText,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function preflightDependencyInstaller(plan, cwd) {
|
|
82
|
+
if (plan.missingDependencies.length === 0) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const result = spawnSync(plan.command, ["--version"], {
|
|
87
|
+
cwd,
|
|
88
|
+
encoding: "utf8",
|
|
89
|
+
shell: false,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (result.error) {
|
|
93
|
+
throw new CliError(`Package manager command not found for --install-deps: ${plan.command}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (result.status !== 0) {
|
|
97
|
+
throw new CliError(
|
|
98
|
+
`Package manager preflight failed for --install-deps: ${plan.command} --version\n${formatSpawnOutput(result)}`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function runDependencyInstall(plan, cwd) {
|
|
104
|
+
if (plan.missingDependencies.length === 0) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const result = spawnSync(plan.command, plan.args, {
|
|
109
|
+
cwd,
|
|
110
|
+
encoding: "utf8",
|
|
111
|
+
shell: false,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
if (result.error) {
|
|
115
|
+
throw new CliError(`Dependency install command failed to start: ${plan.commandText}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (result.status !== 0) {
|
|
119
|
+
throw new CliError(`Dependency install command failed: ${plan.commandText}\n${formatSpawnOutput(result)}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function buildInstallCommand(packageManager, dependencies) {
|
|
124
|
+
const command = validatePackageManager(packageManager);
|
|
125
|
+
const args = installArgs(command, dependencies);
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
command,
|
|
129
|
+
args,
|
|
130
|
+
commandText: [command, ...args].join(" "),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function installArgs(packageManager, dependencies) {
|
|
135
|
+
if (dependencies.length === 0) {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (packageManager === "pnpm") {
|
|
140
|
+
return ["add", "-D", ...dependencies];
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (packageManager === "npm") {
|
|
144
|
+
return ["install", "--save-dev", ...dependencies];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (packageManager === "yarn") {
|
|
148
|
+
return ["add", "--dev", ...dependencies];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return ["add", "--dev", ...dependencies];
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isDependencyDeclared(packageJson, dependency) {
|
|
155
|
+
return DEPENDENCY_SECTIONS.some((section) => (
|
|
156
|
+
packageJson[section] &&
|
|
157
|
+
typeof packageJson[section] === "object" &&
|
|
158
|
+
Object.hasOwn(packageJson[section], dependency)
|
|
159
|
+
));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function formatSpawnOutput(result) {
|
|
163
|
+
return [result.stderr, result.stdout].filter(Boolean).join("\n").trim();
|
|
164
|
+
}
|
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
ciWorkflowFiles,
|
|
5
|
+
ciWorkflowRelativePath,
|
|
6
|
+
inactiveCiWorkflowFiles,
|
|
7
|
+
isManagedCiWorkflowContent,
|
|
8
|
+
renderedCiWorkflow,
|
|
9
|
+
} from "./ci-workflows.mjs";
|
|
10
|
+
import {
|
|
11
|
+
effectiveOptionsSummary,
|
|
12
|
+
installationSummary,
|
|
13
|
+
installStateIssue,
|
|
14
|
+
loadInstallState,
|
|
15
|
+
resolveEffectiveOptions,
|
|
16
|
+
validateCiMode,
|
|
17
|
+
} from "./install-state.mjs";
|
|
18
|
+
import { DEFAULT_PACKAGE_MANAGER, detectPackageManager, validatePackageManager } from "./package-manager.mjs";
|
|
19
|
+
import { diagnoseProfileScripts } from "./profile-diagnostics.mjs";
|
|
20
|
+
import { getProfileScripts } from "./profile-scripts.mjs";
|
|
21
|
+
import {
|
|
22
|
+
CliError,
|
|
23
|
+
fromTemplates,
|
|
24
|
+
pathExists,
|
|
25
|
+
readJson,
|
|
26
|
+
resolveTarget,
|
|
27
|
+
writeLine,
|
|
28
|
+
} from "./utils.mjs";
|
|
29
|
+
|
|
30
|
+
const DOCTOR_USAGE = `ai-check-template doctor
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
ai-check-template doctor --target <dir> [options]
|
|
34
|
+
|
|
35
|
+
Options:
|
|
36
|
+
--target <dir> Target project directory. Defaults to the current directory.
|
|
37
|
+
--profile <name> Profile to check. Defaults to install state or react-nextjs.
|
|
38
|
+
--package-manager <name> Package manager: pnpm, npm, yarn, or bun. Defaults to install state or target detection.
|
|
39
|
+
--ci <mode> CI mode to check: direct, reusable, or none. Defaults to direct.
|
|
40
|
+
--claude-hooks Check Claude rule and hook settings.
|
|
41
|
+
--strict Treat warnings as failures.
|
|
42
|
+
--json Print machine-readable JSON output.`;
|
|
43
|
+
|
|
44
|
+
export async function runDoctor(argv, io = {}) {
|
|
45
|
+
const options = parseDoctorArgs(argv, io.cwd ?? process.cwd());
|
|
46
|
+
|
|
47
|
+
if (options.help) {
|
|
48
|
+
writeLine(io.stdout, DOCTOR_USAGE);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const targetDir = await normalizeTargetDir(options.target);
|
|
53
|
+
options.packageManager = options.explicit.packageManager
|
|
54
|
+
? options.packageManager
|
|
55
|
+
: await detectPackageManager(targetDir);
|
|
56
|
+
const installState = await loadInstallState(targetDir);
|
|
57
|
+
const effectiveOptions = resolveEffectiveOptions(options, installState);
|
|
58
|
+
const result = await diagnoseTarget(targetDir, effectiveOptions);
|
|
59
|
+
const stateIssue = installStateIssue(installState);
|
|
60
|
+
const issues = stateIssue ? [stateIssue, ...result.issues] : result.issues;
|
|
61
|
+
const failed = issues.length > 0 || (options.strict && result.warnings.length > 0);
|
|
62
|
+
const output = {
|
|
63
|
+
status: failed ? "fail" : "pass",
|
|
64
|
+
target: targetDir,
|
|
65
|
+
strict: options.strict,
|
|
66
|
+
installation: installationSummary(installState),
|
|
67
|
+
effectiveOptions: effectiveOptionsSummary(effectiveOptions),
|
|
68
|
+
warnings: result.warnings,
|
|
69
|
+
issues,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
if (options.json) {
|
|
73
|
+
writeLine(io.stdout, JSON.stringify(output, null, 2));
|
|
74
|
+
} else {
|
|
75
|
+
writeHumanOutput(io.stdout, output);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (output.status === "fail") {
|
|
79
|
+
throw new CliError(
|
|
80
|
+
`doctor found ${output.issues.length} issue(s) and ${output.warnings.length} warning(s)`,
|
|
81
|
+
1,
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseDoctorArgs(argv, cwd) {
|
|
87
|
+
const options = {
|
|
88
|
+
target: cwd,
|
|
89
|
+
profile: "react-nextjs",
|
|
90
|
+
packageManager: DEFAULT_PACKAGE_MANAGER,
|
|
91
|
+
ci: "direct",
|
|
92
|
+
claudeHooks: false,
|
|
93
|
+
strict: false,
|
|
94
|
+
json: false,
|
|
95
|
+
help: false,
|
|
96
|
+
explicit: {
|
|
97
|
+
profile: false,
|
|
98
|
+
packageManager: false,
|
|
99
|
+
ci: false,
|
|
100
|
+
claudeHooks: false,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
105
|
+
const arg = argv[index];
|
|
106
|
+
|
|
107
|
+
if (arg === "--help" || arg === "-h") {
|
|
108
|
+
options.help = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (arg === "--claude-hooks") {
|
|
113
|
+
options.claudeHooks = true;
|
|
114
|
+
options.explicit.claudeHooks = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (arg === "--json") {
|
|
119
|
+
options.json = true;
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (arg === "--strict") {
|
|
124
|
+
options.strict = true;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (arg.startsWith("--target=")) {
|
|
129
|
+
options.target = resolveTarget(arg.slice("--target=".length), cwd);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (arg === "--target") {
|
|
134
|
+
options.target = resolveTarget(readFlagValue(argv, (index += 1), arg), cwd);
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (arg.startsWith("--profile=")) {
|
|
139
|
+
options.profile = arg.slice("--profile=".length);
|
|
140
|
+
options.explicit.profile = true;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (arg === "--profile") {
|
|
145
|
+
options.profile = readFlagValue(argv, (index += 1), arg);
|
|
146
|
+
options.explicit.profile = true;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (arg.startsWith("--package-manager=")) {
|
|
151
|
+
options.packageManager = validatePackageManager(arg.slice("--package-manager=".length));
|
|
152
|
+
options.explicit.packageManager = true;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (arg === "--package-manager") {
|
|
157
|
+
options.packageManager = validatePackageManager(readFlagValue(argv, (index += 1), arg));
|
|
158
|
+
options.explicit.packageManager = true;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (arg.startsWith("--ci=")) {
|
|
163
|
+
options.ci = arg.slice("--ci=".length);
|
|
164
|
+
options.explicit.ci = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (arg === "--ci") {
|
|
169
|
+
options.ci = readFlagValue(argv, (index += 1), arg);
|
|
170
|
+
options.explicit.ci = true;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
throw new CliError(`Unknown doctor option: ${arg}\n\n${DOCTOR_USAGE}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
validateCiMode(options.ci);
|
|
178
|
+
|
|
179
|
+
return options;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function readFlagValue(argv, index, flagName) {
|
|
183
|
+
const value = argv[index];
|
|
184
|
+
|
|
185
|
+
if (!value || value.startsWith("--")) {
|
|
186
|
+
throw new CliError(`Missing value for ${flagName}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return value;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function normalizeTargetDir(target) {
|
|
193
|
+
const resolved = path.resolve(target);
|
|
194
|
+
|
|
195
|
+
try {
|
|
196
|
+
return await fs.realpath(resolved);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
throw new CliError(`Target directory does not exist: ${resolved}\n${error.message}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function diagnoseTarget(targetDir, options) {
|
|
203
|
+
const issues = [];
|
|
204
|
+
let warnings = [];
|
|
205
|
+
const packageJsonPath = path.join(targetDir, "package.json");
|
|
206
|
+
|
|
207
|
+
if (!(await pathExists(packageJsonPath))) {
|
|
208
|
+
return {
|
|
209
|
+
warnings,
|
|
210
|
+
issues: [
|
|
211
|
+
issue("missing-file", "package.json", "Target project must contain package.json"),
|
|
212
|
+
],
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let packageJson;
|
|
217
|
+
try {
|
|
218
|
+
packageJson = await readJson(packageJsonPath);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
issues.push(issue("invalid-json", "package.json", error.message));
|
|
221
|
+
return { issues, warnings };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
checkPackageScripts(packageJson, options.profile, issues, options.packageManager);
|
|
225
|
+
await checkTemplateFile(targetDir, fromTemplates("scripts", "ai-check.sh"), "scripts/ai-check.sh", issues);
|
|
226
|
+
await checkTemplateFile(targetDir, fromTemplates("scripts", "ai-check-fast.sh"), "scripts/ai-check-fast.sh", issues);
|
|
227
|
+
await checkCi(targetDir, options.ci, options.packageManager, issues);
|
|
228
|
+
const ciWarnings = await diagnoseInactiveCi(targetDir, options.ci);
|
|
229
|
+
|
|
230
|
+
if (options.claudeHooks) {
|
|
231
|
+
await checkTemplateFile(
|
|
232
|
+
targetDir,
|
|
233
|
+
fromTemplates(".claude", "rules", "test-rules.md"),
|
|
234
|
+
".claude/rules/test-rules.md",
|
|
235
|
+
issues,
|
|
236
|
+
);
|
|
237
|
+
await checkClaudeSettings(targetDir, issues);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
warnings = [...diagnoseProfileScripts(options.profile, packageJson), ...ciWarnings];
|
|
241
|
+
|
|
242
|
+
return { issues, warnings };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function checkPackageScripts(packageJson, profile, issues, packageManager) {
|
|
246
|
+
const expectedScripts = getProfileScripts(profile, { packageManager });
|
|
247
|
+
const scripts = packageJson.scripts ?? {};
|
|
248
|
+
|
|
249
|
+
for (const [name, expected] of Object.entries(expectedScripts)) {
|
|
250
|
+
if (!scripts[name]) {
|
|
251
|
+
issues.push(issue("missing-script", "package.json", `Missing package script: ${name}`));
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (scripts[name] !== expected) {
|
|
256
|
+
issues.push(issue("drift", "package.json", `Package script differs: ${name}`));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function checkCi(targetDir, ciMode, packageManager, issues) {
|
|
262
|
+
for (const fileName of ciWorkflowFiles(ciMode)) {
|
|
263
|
+
await checkExpectedFileContent(
|
|
264
|
+
targetDir,
|
|
265
|
+
await renderedCiWorkflow(fileName, packageManager),
|
|
266
|
+
ciWorkflowRelativePath(fileName),
|
|
267
|
+
issues,
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function diagnoseInactiveCi(targetDir, ciMode) {
|
|
273
|
+
const warnings = [];
|
|
274
|
+
|
|
275
|
+
for (const fileName of inactiveCiWorkflowFiles(ciMode)) {
|
|
276
|
+
const relativePath = ciWorkflowRelativePath(fileName);
|
|
277
|
+
const matchesManagedTemplate = await matchesManagedCiWorkflow(targetDir, fileName, relativePath);
|
|
278
|
+
|
|
279
|
+
if (matchesManagedTemplate) {
|
|
280
|
+
warnings.push(
|
|
281
|
+
warning(
|
|
282
|
+
"ci-advice",
|
|
283
|
+
relativePath,
|
|
284
|
+
`Managed CI workflow is inactive for ci mode: ${ciMode}`,
|
|
285
|
+
),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return warnings;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function checkExpectedFileContent(targetDir, expected, relativePath, issues) {
|
|
294
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
295
|
+
|
|
296
|
+
if (!(await pathExists(targetPath))) {
|
|
297
|
+
issues.push(issue("missing-file", normalizeRelative(relativePath), "Expected template file is missing"));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const actual = await fs.readFile(targetPath, "utf8");
|
|
302
|
+
|
|
303
|
+
if (actual !== expected) {
|
|
304
|
+
issues.push(issue("drift", normalizeRelative(relativePath), "Template-managed file differs"));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
async function checkTemplateFile(targetDir, expectedPath, relativePath, issues) {
|
|
309
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
310
|
+
|
|
311
|
+
if (!(await pathExists(targetPath))) {
|
|
312
|
+
issues.push(issue("missing-file", normalizeRelative(relativePath), "Expected template file is missing"));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
const [actual, expected] = await Promise.all([
|
|
317
|
+
fs.readFile(targetPath, "utf8"),
|
|
318
|
+
fs.readFile(expectedPath, "utf8"),
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
if (actual !== expected) {
|
|
322
|
+
issues.push(issue("drift", normalizeRelative(relativePath), "Template-managed file differs"));
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function matchesManagedCiWorkflow(targetDir, fileName, relativePath) {
|
|
327
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
328
|
+
|
|
329
|
+
if (!(await pathExists(targetPath))) {
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return isManagedCiWorkflowContent(fileName, await fs.readFile(targetPath, "utf8"));
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function checkClaudeSettings(targetDir, issues) {
|
|
337
|
+
const relativePath = ".claude/settings.json";
|
|
338
|
+
const targetPath = path.join(targetDir, relativePath);
|
|
339
|
+
|
|
340
|
+
if (!(await pathExists(targetPath))) {
|
|
341
|
+
issues.push(issue("missing-file", relativePath, "Claude settings file is missing"));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let settings;
|
|
346
|
+
try {
|
|
347
|
+
settings = await readJson(targetPath);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
issues.push(issue("invalid-json", relativePath, error.message));
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const requiredHooks = ["PostToolUse", "Stop"];
|
|
354
|
+
for (const hookName of requiredHooks) {
|
|
355
|
+
if (!settings.hooks?.[hookName]) {
|
|
356
|
+
issues.push(issue("missing-hook", relativePath, `Missing Claude hook: ${hookName}`));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function issue(code, filePath, message) {
|
|
362
|
+
return { code, path: normalizeRelative(filePath), message };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function warning(code, filePath, message) {
|
|
366
|
+
return { code, path: normalizeRelative(filePath), message };
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function normalizeRelative(filePath) {
|
|
370
|
+
return filePath.split(path.sep).join("/");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function writeHumanOutput(stream, output) {
|
|
374
|
+
writeLine(stream, `ai-check-template doctor ${output.status}`);
|
|
375
|
+
writeLine(stream, `target: ${output.target}`);
|
|
376
|
+
writeLine(stream, `install-state: ${output.installation.source}`);
|
|
377
|
+
writeLine(stream, `profile: ${output.effectiveOptions.profile}`);
|
|
378
|
+
writeLine(stream, `package-manager: ${output.effectiveOptions.packageManager}`);
|
|
379
|
+
writeLine(stream, `ci: ${output.effectiveOptions.ci}`);
|
|
380
|
+
writeLine(stream, `claude-hooks: ${output.effectiveOptions.claudeHooks}`);
|
|
381
|
+
writeLine(stream, `strict: ${output.strict}`);
|
|
382
|
+
|
|
383
|
+
writeLine(stream, `issues: ${output.issues.length}`);
|
|
384
|
+
for (const currentIssue of output.issues) {
|
|
385
|
+
writeLine(stream, `- ${currentIssue.code}: ${currentIssue.path} (${currentIssue.message})`);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
writeLine(stream, `warnings: ${output.warnings.length}`);
|
|
389
|
+
for (const currentWarning of output.warnings) {
|
|
390
|
+
writeLine(stream, `- ${currentWarning.code}: ${currentWarning.path} (${currentWarning.message})`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { runDoctor } from "./doctor.mjs";
|
|
2
|
+
import { runInit } from "./init.mjs";
|
|
3
|
+
import { runUpdate } from "./update.mjs";
|
|
4
|
+
import { CliError, writeLine } from "./utils.mjs";
|
|
5
|
+
|
|
6
|
+
const USAGE = `ai-check-template
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
ai-check-template --help
|
|
10
|
+
ai-check-template init [options]
|
|
11
|
+
ai-check-template doctor [options]
|
|
12
|
+
ai-check-template update [options]
|
|
13
|
+
|
|
14
|
+
Commands:
|
|
15
|
+
doctor Diagnose an existing ai-check-template installation.
|
|
16
|
+
init Copy ai-check templates into an existing project.
|
|
17
|
+
update Update template-managed files in an existing installation.
|
|
18
|
+
|
|
19
|
+
Init options:
|
|
20
|
+
--target <dir> Target project directory. Defaults to the current directory.
|
|
21
|
+
--profile <name> Profile name. Defaults to react-nextjs.
|
|
22
|
+
--package-manager <name> Package manager: pnpm, npm, yarn, or bun.
|
|
23
|
+
--ci <mode> CI mode: direct, reusable, or none. Defaults to direct.
|
|
24
|
+
--claude-hooks Copy Claude hook rule and merge hook settings.
|
|
25
|
+
--install-deps Install missing dev dependencies for generated package scripts.
|
|
26
|
+
--dry-run Print planned operations without writing files.
|
|
27
|
+
--yes Confirm non-interactive writes.
|
|
28
|
+
--overwrite Replace conflicting files/scripts.
|
|
29
|
+
|
|
30
|
+
Doctor options:
|
|
31
|
+
--target <dir> Target project directory. Defaults to the current directory.
|
|
32
|
+
--package-manager <name> Package manager: pnpm, npm, yarn, or bun.
|
|
33
|
+
--ci <mode> CI mode to check: direct, reusable, or none. Defaults to direct.
|
|
34
|
+
--claude-hooks Check Claude rule and hook settings.
|
|
35
|
+
--strict Treat warnings as failures.
|
|
36
|
+
--json Print machine-readable JSON output.
|
|
37
|
+
|
|
38
|
+
Update options:
|
|
39
|
+
--target <dir> Target project directory. Defaults to the current directory.
|
|
40
|
+
--package-manager <name> Package manager: pnpm, npm, yarn, or bun.
|
|
41
|
+
--ci <mode> CI mode to update: direct, reusable, or none. Defaults to direct.
|
|
42
|
+
--claude-hooks Update Claude rule and hook settings.
|
|
43
|
+
--install-deps Install missing dev dependencies for generated package scripts.
|
|
44
|
+
--dry-run Print planned operations without writing files.
|
|
45
|
+
--yes Confirm non-interactive writes.
|
|
46
|
+
--json Print machine-readable JSON output.
|
|
47
|
+
|
|
48
|
+
Examples:
|
|
49
|
+
ai-check-template init --target . --profile react-nextjs --yes
|
|
50
|
+
ai-check-template init --target . --profile react-nextjs+supabase-rls --ci reusable --dry-run
|
|
51
|
+
ai-check-template doctor --target . --ci direct --json
|
|
52
|
+
ai-check-template update --target . --ci direct --dry-run`;
|
|
53
|
+
|
|
54
|
+
export async function main(argv = process.argv.slice(2), io = {}) {
|
|
55
|
+
const args = [...argv];
|
|
56
|
+
|
|
57
|
+
if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
|
|
58
|
+
writeLine(io.stdout, USAGE);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const command = args.shift();
|
|
63
|
+
|
|
64
|
+
if (command === "init") {
|
|
65
|
+
await runInit(args, io);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (command === "doctor") {
|
|
70
|
+
await runDoctor(args, io);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (command === "update") {
|
|
75
|
+
await runUpdate(args, io);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
throw new CliError(`Unknown command: ${command}\n\n${USAGE}`, 1);
|
|
80
|
+
}
|