@neosianexus/quality 1.0.0-beta.5
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/README.md +551 -0
- package/bin/cli.js +1099 -0
- package/biome.json +639 -0
- package/commitlint.config.js +134 -0
- package/knip.config.json +7 -0
- package/package.json +115 -0
- package/tsconfig.base.json +57 -0
- package/tsconfig.nextjs.json +29 -0
- package/tsconfig.react.json +12 -0
- package/vscode/extensions.json +11 -0
- package/vscode/settings.json +49 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,1099 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// bin/cli.ts
|
|
4
|
+
import { defineCommand as defineCommand3, runCommand as runCommand2, runMain } from "citty";
|
|
5
|
+
|
|
6
|
+
// bin/commands/init.ts
|
|
7
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync2 } from "fs";
|
|
8
|
+
import { join as join4 } from "path";
|
|
9
|
+
import * as p from "@clack/prompts";
|
|
10
|
+
import { defineCommand } from "citty";
|
|
11
|
+
import pc from "picocolors";
|
|
12
|
+
|
|
13
|
+
// bin/utils/constants.ts
|
|
14
|
+
var VERSION = "1.0.0-beta.3";
|
|
15
|
+
var PACKAGE_NAME = "@neosianexus/quality";
|
|
16
|
+
|
|
17
|
+
// bin/utils/detect.ts
|
|
18
|
+
import { existsSync, readFileSync } from "fs";
|
|
19
|
+
import { join } from "path";
|
|
20
|
+
function detectPackageManager(cwd) {
|
|
21
|
+
if (existsSync(join(cwd, "bun.lockb")) || existsSync(join(cwd, "bun.lock"))) {
|
|
22
|
+
return "bun";
|
|
23
|
+
}
|
|
24
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml"))) {
|
|
25
|
+
return "pnpm";
|
|
26
|
+
}
|
|
27
|
+
if (existsSync(join(cwd, "yarn.lock"))) {
|
|
28
|
+
return "yarn";
|
|
29
|
+
}
|
|
30
|
+
if (existsSync(join(cwd, "package-lock.json"))) {
|
|
31
|
+
return "npm";
|
|
32
|
+
}
|
|
33
|
+
return "bun";
|
|
34
|
+
}
|
|
35
|
+
function readPackageJson(cwd) {
|
|
36
|
+
const packageJsonPath = join(cwd, "package.json");
|
|
37
|
+
if (!existsSync(packageJsonPath)) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const content = readFileSync(packageJsonPath, "utf-8");
|
|
42
|
+
return JSON.parse(content);
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function detectProjectType(cwd) {
|
|
48
|
+
const packageJson = readPackageJson(cwd);
|
|
49
|
+
if (!packageJson) {
|
|
50
|
+
return "base";
|
|
51
|
+
}
|
|
52
|
+
const deps = {
|
|
53
|
+
...packageJson.dependencies,
|
|
54
|
+
...packageJson.devDependencies
|
|
55
|
+
};
|
|
56
|
+
if (deps.next) {
|
|
57
|
+
return "nextjs";
|
|
58
|
+
}
|
|
59
|
+
if (deps.react) {
|
|
60
|
+
return "react";
|
|
61
|
+
}
|
|
62
|
+
return "base";
|
|
63
|
+
}
|
|
64
|
+
function isGitRepository(cwd) {
|
|
65
|
+
return existsSync(join(cwd, ".git"));
|
|
66
|
+
}
|
|
67
|
+
function getPackageManagerCommands(pm) {
|
|
68
|
+
switch (pm) {
|
|
69
|
+
case "bun":
|
|
70
|
+
return { install: "bun install", addDev: ["bun", "add", "-D"], exec: "bunx" };
|
|
71
|
+
case "pnpm":
|
|
72
|
+
return { install: "pnpm install", addDev: ["pnpm", "add", "-D"], exec: "pnpm exec" };
|
|
73
|
+
case "yarn":
|
|
74
|
+
return { install: "yarn", addDev: ["yarn", "add", "-D"], exec: "yarn" };
|
|
75
|
+
default:
|
|
76
|
+
return { install: "npm install", addDev: ["npm", "install", "-D"], exec: "npx" };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// bin/utils/exec.ts
|
|
81
|
+
import { spawnSync } from "child_process";
|
|
82
|
+
function runCommand(command, args2, cwd) {
|
|
83
|
+
try {
|
|
84
|
+
const result = spawnSync(command, args2, {
|
|
85
|
+
cwd,
|
|
86
|
+
encoding: "utf-8",
|
|
87
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
88
|
+
});
|
|
89
|
+
return {
|
|
90
|
+
success: result.status === 0,
|
|
91
|
+
output: result.stdout || result.stderr || "",
|
|
92
|
+
code: result.status
|
|
93
|
+
};
|
|
94
|
+
} catch (err) {
|
|
95
|
+
return {
|
|
96
|
+
success: false,
|
|
97
|
+
output: err instanceof Error ? err.message : "Unknown error",
|
|
98
|
+
code: null
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// bin/utils/fs.ts
|
|
104
|
+
import {
|
|
105
|
+
chmodSync,
|
|
106
|
+
copyFileSync,
|
|
107
|
+
existsSync as existsSync2,
|
|
108
|
+
mkdirSync,
|
|
109
|
+
readFileSync as readFileSync2,
|
|
110
|
+
writeFileSync
|
|
111
|
+
} from "fs";
|
|
112
|
+
import { basename, dirname, extname, join as join2 } from "path";
|
|
113
|
+
function writeFile(filePath, content, executable = false) {
|
|
114
|
+
const dir = dirname(filePath);
|
|
115
|
+
if (!existsSync2(dir)) {
|
|
116
|
+
mkdirSync(dir, { recursive: true });
|
|
117
|
+
}
|
|
118
|
+
writeFileSync(filePath, content, "utf-8");
|
|
119
|
+
if (executable) {
|
|
120
|
+
chmodSync(filePath, 493);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function readJsonFile(filePath) {
|
|
124
|
+
if (!existsSync2(filePath)) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const content = readFileSync2(filePath, "utf-8");
|
|
129
|
+
return JSON.parse(content);
|
|
130
|
+
} catch {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function writeJsonFile(filePath, data) {
|
|
135
|
+
writeFile(filePath, `${JSON.stringify(data, null, " ")}
|
|
136
|
+
`);
|
|
137
|
+
}
|
|
138
|
+
function createBackup(filePath) {
|
|
139
|
+
if (!existsSync2(filePath)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
const dir = dirname(filePath);
|
|
143
|
+
const ext = extname(filePath);
|
|
144
|
+
const base = basename(filePath, ext);
|
|
145
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(0, 19).replace(/:/g, "-");
|
|
146
|
+
const backupPath = join2(dir, `${base}.backup.${timestamp}${ext}`);
|
|
147
|
+
copyFileSync(filePath, backupPath);
|
|
148
|
+
return backupPath;
|
|
149
|
+
}
|
|
150
|
+
function fileExists(filePath) {
|
|
151
|
+
return existsSync2(filePath);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// bin/utils/generators.ts
|
|
155
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
156
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
157
|
+
import { fileURLToPath } from "url";
|
|
158
|
+
var currentDir = dirname2(fileURLToPath(import.meta.url));
|
|
159
|
+
var packageRoot = join3(currentDir, "..", "..");
|
|
160
|
+
function generateBiomeConfig() {
|
|
161
|
+
return {
|
|
162
|
+
$schema: "https://biomejs.dev/schemas/2.3.13/schema.json",
|
|
163
|
+
extends: ["@neosianexus/quality"]
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function getExtendsPath(type) {
|
|
167
|
+
switch (type) {
|
|
168
|
+
case "nextjs":
|
|
169
|
+
return "@neosianexus/quality/tsconfig.nextjs";
|
|
170
|
+
case "react":
|
|
171
|
+
return "@neosianexus/quality/tsconfig.react";
|
|
172
|
+
default:
|
|
173
|
+
return "@neosianexus/quality/tsconfig.base";
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
function generateTsConfig(type) {
|
|
177
|
+
const config = {
|
|
178
|
+
extends: getExtendsPath(type),
|
|
179
|
+
compilerOptions: {
|
|
180
|
+
baseUrl: ".",
|
|
181
|
+
paths: {
|
|
182
|
+
"@/*": ["./src/*"]
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
include: ["src", "**/*.ts", "**/*.tsx"],
|
|
186
|
+
exclude: ["node_modules", "dist", "build", ".next"]
|
|
187
|
+
};
|
|
188
|
+
if (type === "nextjs") {
|
|
189
|
+
config.include = ["src", "next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"];
|
|
190
|
+
}
|
|
191
|
+
return config;
|
|
192
|
+
}
|
|
193
|
+
function generateCommitlintConfig() {
|
|
194
|
+
return `import config from "@neosianexus/quality/commitlint";
|
|
195
|
+
|
|
196
|
+
export default config;
|
|
197
|
+
`;
|
|
198
|
+
}
|
|
199
|
+
function generatePreCommitHook(execCommand) {
|
|
200
|
+
return `#!/bin/sh
|
|
201
|
+
|
|
202
|
+
# Pre-commit hook - runs lint-staged to check and fix staged files
|
|
203
|
+
# To skip: git commit --no-verify
|
|
204
|
+
|
|
205
|
+
echo "\\033[0;36m\u{1F50D} Running pre-commit checks...\\033[0m"
|
|
206
|
+
|
|
207
|
+
if ! ${execCommand} lint-staged; then
|
|
208
|
+
echo ""
|
|
209
|
+
echo "\\033[0;31m\u2717 Pre-commit checks failed\\033[0m"
|
|
210
|
+
echo ""
|
|
211
|
+
echo "\\033[0;33mHow to fix:\\033[0m"
|
|
212
|
+
echo " 1. Review the errors above"
|
|
213
|
+
echo " 2. Run 'bun run check:fix' to auto-fix issues"
|
|
214
|
+
echo " 3. Stage the fixed files with 'git add'"
|
|
215
|
+
echo " 4. Try committing again"
|
|
216
|
+
echo ""
|
|
217
|
+
echo "\\033[0;90mTo skip this check: git commit --no-verify\\033[0m"
|
|
218
|
+
exit 1
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
echo "\\033[0;32m\u2713 Pre-commit checks passed\\033[0m"
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
function generateCommitMsgHook(withCommitlint, execCommand) {
|
|
225
|
+
if (withCommitlint) {
|
|
226
|
+
return `#!/bin/sh
|
|
227
|
+
|
|
228
|
+
# Commit message validation - enforces Conventional Commits format
|
|
229
|
+
# To skip: git commit --no-verify
|
|
230
|
+
# Format: type(scope): description
|
|
231
|
+
# Types: feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert
|
|
232
|
+
|
|
233
|
+
if ! ${execCommand} --no -- commitlint --edit "$1"; then
|
|
234
|
+
echo ""
|
|
235
|
+
echo "\\033[0;31m\u2717 Commit message validation failed\\033[0m"
|
|
236
|
+
echo ""
|
|
237
|
+
echo "\\033[0;33mExpected format:\\033[0m type(scope): description"
|
|
238
|
+
echo ""
|
|
239
|
+
echo "\\033[0;33mExamples:\\033[0m"
|
|
240
|
+
echo " feat: add user authentication"
|
|
241
|
+
echo " fix(api): resolve timeout issue"
|
|
242
|
+
echo " docs: update README"
|
|
243
|
+
echo ""
|
|
244
|
+
echo "\\033[0;33mAllowed types:\\033[0m"
|
|
245
|
+
echo " feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert"
|
|
246
|
+
echo ""
|
|
247
|
+
echo "\\033[0;90mTo skip: git commit --no-verify\\033[0m"
|
|
248
|
+
exit 1
|
|
249
|
+
fi
|
|
250
|
+
`;
|
|
251
|
+
}
|
|
252
|
+
return `#!/bin/sh
|
|
253
|
+
|
|
254
|
+
# Conventional Commits validation (disabled)
|
|
255
|
+
# To enable, run: bunx quality init --commitlint
|
|
256
|
+
# Or manually install: bun add -D @commitlint/cli @commitlint/config-conventional
|
|
257
|
+
`;
|
|
258
|
+
}
|
|
259
|
+
function getVscodeSettings() {
|
|
260
|
+
const settingsPath = join3(packageRoot, "vscode", "settings.json");
|
|
261
|
+
if (existsSync3(settingsPath)) {
|
|
262
|
+
try {
|
|
263
|
+
return JSON.parse(readFileSync3(settingsPath, "utf-8"));
|
|
264
|
+
} catch {
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
"editor.defaultFormatter": "biomejs.biome",
|
|
269
|
+
"editor.formatOnSave": true,
|
|
270
|
+
"editor.formatOnPaste": true,
|
|
271
|
+
"editor.codeActionsOnSave": {
|
|
272
|
+
"quickfix.biome": "explicit",
|
|
273
|
+
"source.organizeImports.biome": "explicit"
|
|
274
|
+
},
|
|
275
|
+
"editor.rulers": [100],
|
|
276
|
+
"editor.tabSize": 2,
|
|
277
|
+
"editor.insertSpaces": false,
|
|
278
|
+
"files.eol": "\n",
|
|
279
|
+
"files.trimTrailingWhitespace": true,
|
|
280
|
+
"files.insertFinalNewline": true,
|
|
281
|
+
"[javascript]": { "editor.defaultFormatter": "biomejs.biome" },
|
|
282
|
+
"[javascriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
|
|
283
|
+
"[typescript]": { "editor.defaultFormatter": "biomejs.biome" },
|
|
284
|
+
"[typescriptreact]": { "editor.defaultFormatter": "biomejs.biome" },
|
|
285
|
+
"[json]": { "editor.defaultFormatter": "biomejs.biome" },
|
|
286
|
+
"[jsonc]": { "editor.defaultFormatter": "biomejs.biome" },
|
|
287
|
+
"[css]": { "editor.defaultFormatter": "biomejs.biome" },
|
|
288
|
+
"[markdown]": { "files.trimTrailingWhitespace": false },
|
|
289
|
+
"typescript.tsdk": "node_modules/typescript/lib",
|
|
290
|
+
"typescript.enablePromptUseWorkspaceTsdk": true,
|
|
291
|
+
"typescript.preferences.importModuleSpecifier": "non-relative",
|
|
292
|
+
"typescript.preferences.preferTypeOnlyAutoImports": true
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function getVscodeExtensions() {
|
|
296
|
+
const extensionsPath = join3(packageRoot, "vscode", "extensions.json");
|
|
297
|
+
if (existsSync3(extensionsPath)) {
|
|
298
|
+
try {
|
|
299
|
+
return JSON.parse(readFileSync3(extensionsPath, "utf-8"));
|
|
300
|
+
} catch {
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
return {
|
|
304
|
+
recommendations: [
|
|
305
|
+
"biomejs.biome",
|
|
306
|
+
"usernamehw.errorlens",
|
|
307
|
+
"editorconfig.editorconfig",
|
|
308
|
+
"streetsidesoftware.code-spell-checker",
|
|
309
|
+
"eamodio.gitlens",
|
|
310
|
+
"gruntfuggly.todo-tree"
|
|
311
|
+
],
|
|
312
|
+
unwantedRecommendations: ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint"]
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function generateKnipConfig(type) {
|
|
316
|
+
const baseConfig = {
|
|
317
|
+
$schema: "https://unpkg.com/knip@5/schema.json",
|
|
318
|
+
ignore: ["**/*.d.ts"],
|
|
319
|
+
ignoreBinaries: ["biome"]
|
|
320
|
+
};
|
|
321
|
+
if (type === "nextjs") {
|
|
322
|
+
return {
|
|
323
|
+
...baseConfig,
|
|
324
|
+
entry: ["src/app/**/page.tsx", "src/app/**/layout.tsx", "src/app/**/route.ts"],
|
|
325
|
+
project: ["src/**/*.{ts,tsx}"],
|
|
326
|
+
ignoreDependencies: ["tailwindcss", "postcss", "autoprefixer"],
|
|
327
|
+
next: {
|
|
328
|
+
entry: ["next.config.{js,ts,mjs}"]
|
|
329
|
+
},
|
|
330
|
+
postcss: {
|
|
331
|
+
config: ["postcss.config.{js,mjs,cjs}"]
|
|
332
|
+
},
|
|
333
|
+
tailwind: {
|
|
334
|
+
config: ["tailwind.config.{js,ts,mjs,cjs}"]
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
if (type === "react") {
|
|
339
|
+
return {
|
|
340
|
+
...baseConfig,
|
|
341
|
+
project: ["src/**/*.{ts,tsx,js,jsx}"],
|
|
342
|
+
entry: ["src/index.{ts,tsx}", "src/main.{ts,tsx}", "src/App.{ts,tsx}"],
|
|
343
|
+
ignoreDependencies: ["tailwindcss", "postcss", "autoprefixer"]
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
...baseConfig,
|
|
348
|
+
project: ["src/**/*.{ts,tsx,js,jsx}"],
|
|
349
|
+
entry: ["src/index.ts", "src/main.ts"]
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function getPackageScripts(options) {
|
|
353
|
+
const scripts = {
|
|
354
|
+
lint: "biome lint .",
|
|
355
|
+
"lint:fix": "biome lint --write --unsafe .",
|
|
356
|
+
format: "biome format --write .",
|
|
357
|
+
check: "biome check .",
|
|
358
|
+
"check:fix": "biome check --write --unsafe .",
|
|
359
|
+
typecheck: "tsc --noEmit"
|
|
360
|
+
};
|
|
361
|
+
if (options.husky) {
|
|
362
|
+
scripts.prepare = "husky";
|
|
363
|
+
}
|
|
364
|
+
if (options.commitlint) {
|
|
365
|
+
scripts.commitlint = "commitlint --edit";
|
|
366
|
+
}
|
|
367
|
+
if (options.knip) {
|
|
368
|
+
scripts.knip = "knip";
|
|
369
|
+
scripts["knip:fix"] = "knip --fix";
|
|
370
|
+
}
|
|
371
|
+
return scripts;
|
|
372
|
+
}
|
|
373
|
+
function getLintStagedConfig() {
|
|
374
|
+
return {
|
|
375
|
+
"*.{js,jsx,ts,tsx,json,css,md}": ["biome check --write --unsafe --no-errors-on-unmatched"]
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
function generateClaudeMd(options) {
|
|
379
|
+
const { projectType, commitlint, knip, packageManager } = options;
|
|
380
|
+
const pm = packageManager === "npm" ? "npm run" : packageManager;
|
|
381
|
+
const sections = [];
|
|
382
|
+
sections.push(`# Project Quality Configuration
|
|
383
|
+
|
|
384
|
+
This project uses \`@neosianexus/quality\` with ultra-strict TypeScript and Biome.`);
|
|
385
|
+
const commands = [
|
|
386
|
+
`${pm} check # Lint + Format (Biome)`,
|
|
387
|
+
`${pm} typecheck # TypeScript strict mode`
|
|
388
|
+
];
|
|
389
|
+
if (knip) {
|
|
390
|
+
commands.push(`${pm} knip # Dead code detection`);
|
|
391
|
+
}
|
|
392
|
+
sections.push(`## Verification Commands
|
|
393
|
+
|
|
394
|
+
IMPORTANT: Always run these before committing:
|
|
395
|
+
|
|
396
|
+
\`\`\`bash
|
|
397
|
+
${commands.join("\n")}
|
|
398
|
+
\`\`\``);
|
|
399
|
+
sections.push(`## Critical TypeScript Rules
|
|
400
|
+
|
|
401
|
+
This project uses ultra-strict TypeScript. You MUST:
|
|
402
|
+
|
|
403
|
+
- \`noUncheckedIndexedAccess\`: Always check array/object access (\`arr[0]?.value\`)
|
|
404
|
+
- \`exactOptionalPropertyTypes\`: \`undefined\` must be explicit for optional props
|
|
405
|
+
- \`noImplicitAny\`: Never use implicit \`any\`, always type explicitly
|
|
406
|
+
- \`verbatimModuleSyntax\`: Use \`import type { X }\` for type-only imports`);
|
|
407
|
+
let biomeRules = `## Biome Rules (replaces ESLint + Prettier)
|
|
408
|
+
|
|
409
|
+
- NO \`forEach\` \u2192 use \`for...of\` or \`map\`
|
|
410
|
+
- NO CommonJS \u2192 use ES modules (\`import\`/\`export\`)
|
|
411
|
+
- NO non-null assertions (\`!\`) \u2192 use proper null checks
|
|
412
|
+
- NO implicit \`any\` \u2192 always type explicitly
|
|
413
|
+
- Max 15 cognitive complexity per function
|
|
414
|
+
- Max 4 parameters per function`;
|
|
415
|
+
if (projectType === "nextjs") {
|
|
416
|
+
biomeRules += `
|
|
417
|
+
- Default exports allowed ONLY for Next.js pages/layouts/routes`;
|
|
418
|
+
} else {
|
|
419
|
+
biomeRules += `
|
|
420
|
+
- NO default exports (use named exports)`;
|
|
421
|
+
}
|
|
422
|
+
sections.push(biomeRules);
|
|
423
|
+
sections.push(`## Code Style (enforced automatically)
|
|
424
|
+
|
|
425
|
+
- Indentation: Tabs (2-space width)
|
|
426
|
+
- Quotes: Double quotes (\`"\`)
|
|
427
|
+
- Semicolons: Always
|
|
428
|
+
- Line width: 100 characters
|
|
429
|
+
- Trailing commas: Everywhere`);
|
|
430
|
+
if (commitlint) {
|
|
431
|
+
sections.push(`## Commit Format (Conventional Commits)
|
|
432
|
+
|
|
433
|
+
\`\`\`
|
|
434
|
+
<type>(<scope>): <subject>
|
|
435
|
+
\`\`\`
|
|
436
|
+
|
|
437
|
+
Types: \`feat\` | \`fix\` | \`docs\` | \`style\` | \`refactor\` | \`perf\` | \`test\` | \`build\` | \`ci\` | \`chore\``);
|
|
438
|
+
}
|
|
439
|
+
let fileNaming = `## File Naming
|
|
440
|
+
|
|
441
|
+
- Components: \`PascalCase.tsx\` (e.g., \`UserCard.tsx\`)
|
|
442
|
+
- Utilities: \`camelCase.ts\` (e.g., \`formatDate.ts\`)
|
|
443
|
+
- Configs: \`kebab-case\` (e.g., \`next.config.ts\`)`;
|
|
444
|
+
if (projectType === "nextjs") {
|
|
445
|
+
fileNaming += `
|
|
446
|
+
- Routes: \`page.tsx\`, \`layout.tsx\`, \`route.ts\``;
|
|
447
|
+
}
|
|
448
|
+
sections.push(fileNaming);
|
|
449
|
+
return `${sections.join("\n\n")}
|
|
450
|
+
`;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// bin/commands/init.ts
|
|
454
|
+
async function promptInitOptions(defaults) {
|
|
455
|
+
const options = await p.group(
|
|
456
|
+
{
|
|
457
|
+
projectType: () => p.select({
|
|
458
|
+
message: "Type de projet ?",
|
|
459
|
+
initialValue: defaults.projectType,
|
|
460
|
+
options: [
|
|
461
|
+
{
|
|
462
|
+
value: "nextjs",
|
|
463
|
+
label: "Next.js",
|
|
464
|
+
hint: defaults.projectType === "nextjs" ? "d\xE9tect\xE9" : void 0
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
value: "react",
|
|
468
|
+
label: "React",
|
|
469
|
+
hint: defaults.projectType === "react" ? "d\xE9tect\xE9" : void 0
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
value: "base",
|
|
473
|
+
label: "Node.js / TypeScript",
|
|
474
|
+
hint: defaults.projectType === "base" ? "d\xE9tect\xE9" : void 0
|
|
475
|
+
}
|
|
476
|
+
]
|
|
477
|
+
}),
|
|
478
|
+
packageManager: () => p.select({
|
|
479
|
+
message: "Package manager ?",
|
|
480
|
+
initialValue: defaults.packageManager,
|
|
481
|
+
options: [
|
|
482
|
+
{
|
|
483
|
+
value: "bun",
|
|
484
|
+
label: "Bun",
|
|
485
|
+
hint: defaults.packageManager === "bun" ? "d\xE9tect\xE9" : "recommand\xE9"
|
|
486
|
+
},
|
|
487
|
+
{
|
|
488
|
+
value: "pnpm",
|
|
489
|
+
label: "pnpm",
|
|
490
|
+
hint: defaults.packageManager === "pnpm" ? "d\xE9tect\xE9" : void 0
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
value: "yarn",
|
|
494
|
+
label: "Yarn",
|
|
495
|
+
hint: defaults.packageManager === "yarn" ? "d\xE9tect\xE9" : void 0
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
value: "npm",
|
|
499
|
+
label: "npm",
|
|
500
|
+
hint: defaults.packageManager === "npm" ? "d\xE9tect\xE9" : void 0
|
|
501
|
+
}
|
|
502
|
+
]
|
|
503
|
+
}),
|
|
504
|
+
commitlint: () => p.confirm({
|
|
505
|
+
message: "Activer Conventional Commits (commitlint) ?",
|
|
506
|
+
initialValue: true
|
|
507
|
+
}),
|
|
508
|
+
husky: () => p.confirm({
|
|
509
|
+
message: "Configurer les git hooks (Husky + lint-staged) ?",
|
|
510
|
+
initialValue: true
|
|
511
|
+
}),
|
|
512
|
+
vscode: () => p.confirm({
|
|
513
|
+
message: "Ajouter la configuration VS Code ?",
|
|
514
|
+
initialValue: true
|
|
515
|
+
}),
|
|
516
|
+
knip: () => p.confirm({
|
|
517
|
+
message: "Ajouter Knip (d\xE9tection de code mort) ?",
|
|
518
|
+
initialValue: true
|
|
519
|
+
}),
|
|
520
|
+
claudeMd: () => p.confirm({
|
|
521
|
+
message: "Cr\xE9er CLAUDE.md (instructions pour Claude Code) ?",
|
|
522
|
+
initialValue: true
|
|
523
|
+
})
|
|
524
|
+
},
|
|
525
|
+
{
|
|
526
|
+
onCancel: () => {
|
|
527
|
+
p.cancel("Annul\xE9.");
|
|
528
|
+
process.exit(0);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
);
|
|
532
|
+
return options;
|
|
533
|
+
}
|
|
534
|
+
function executeInit(options) {
|
|
535
|
+
const {
|
|
536
|
+
cwd,
|
|
537
|
+
packageManager,
|
|
538
|
+
projectType,
|
|
539
|
+
commitlint,
|
|
540
|
+
husky,
|
|
541
|
+
vscode,
|
|
542
|
+
knip,
|
|
543
|
+
claudeMd,
|
|
544
|
+
force,
|
|
545
|
+
dryRun
|
|
546
|
+
} = options;
|
|
547
|
+
const pmCommands = getPackageManagerCommands(packageManager);
|
|
548
|
+
const tasks = [];
|
|
549
|
+
const biomePath = join4(cwd, "biome.json");
|
|
550
|
+
if (!fileExists(biomePath) || force) {
|
|
551
|
+
if (!dryRun) {
|
|
552
|
+
writeJsonFile(biomePath, generateBiomeConfig());
|
|
553
|
+
}
|
|
554
|
+
tasks.push("biome.json");
|
|
555
|
+
}
|
|
556
|
+
const tsconfigPath = join4(cwd, "tsconfig.json");
|
|
557
|
+
if (!fileExists(tsconfigPath) || force) {
|
|
558
|
+
if (!dryRun) {
|
|
559
|
+
writeJsonFile(tsconfigPath, generateTsConfig(projectType));
|
|
560
|
+
}
|
|
561
|
+
tasks.push("tsconfig.json");
|
|
562
|
+
}
|
|
563
|
+
if (projectType === "nextjs" || projectType === "react") {
|
|
564
|
+
const typesDir = join4(cwd, "src", "types");
|
|
565
|
+
const cssDeclarationPath = join4(typesDir, "css.d.ts");
|
|
566
|
+
if (!fileExists(cssDeclarationPath) || force) {
|
|
567
|
+
if (!dryRun) {
|
|
568
|
+
if (!existsSync4(typesDir)) {
|
|
569
|
+
mkdirSync2(typesDir, { recursive: true });
|
|
570
|
+
}
|
|
571
|
+
writeFile(cssDeclarationPath, 'declare module "*.css";\n');
|
|
572
|
+
}
|
|
573
|
+
tasks.push("src/types/css.d.ts");
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
if (vscode) {
|
|
577
|
+
const vscodeDir = join4(cwd, ".vscode");
|
|
578
|
+
if (!(dryRun || existsSync4(vscodeDir))) {
|
|
579
|
+
mkdirSync2(vscodeDir, { recursive: true });
|
|
580
|
+
}
|
|
581
|
+
const settingsPath = join4(vscodeDir, "settings.json");
|
|
582
|
+
if (!fileExists(settingsPath) || force) {
|
|
583
|
+
if (!dryRun) {
|
|
584
|
+
writeJsonFile(settingsPath, getVscodeSettings());
|
|
585
|
+
}
|
|
586
|
+
tasks.push(".vscode/settings.json");
|
|
587
|
+
}
|
|
588
|
+
const extensionsPath = join4(vscodeDir, "extensions.json");
|
|
589
|
+
if (!fileExists(extensionsPath) || force) {
|
|
590
|
+
if (!dryRun) {
|
|
591
|
+
writeJsonFile(extensionsPath, getVscodeExtensions());
|
|
592
|
+
}
|
|
593
|
+
tasks.push(".vscode/extensions.json");
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const packageJson = readPackageJson(cwd);
|
|
597
|
+
if (packageJson) {
|
|
598
|
+
const scripts = packageJson.scripts || {};
|
|
599
|
+
const newScripts = getPackageScripts({ commitlint, knip, husky });
|
|
600
|
+
let scriptsAdded = 0;
|
|
601
|
+
for (const [name, command] of Object.entries(newScripts)) {
|
|
602
|
+
if (!scripts[name]) {
|
|
603
|
+
scripts[name] = command;
|
|
604
|
+
scriptsAdded++;
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
if (scriptsAdded > 0) {
|
|
608
|
+
packageJson.scripts = scripts;
|
|
609
|
+
}
|
|
610
|
+
if (husky && !packageJson["lint-staged"]) {
|
|
611
|
+
packageJson["lint-staged"] = getLintStagedConfig();
|
|
612
|
+
}
|
|
613
|
+
if (!dryRun) {
|
|
614
|
+
writeJsonFile(join4(cwd, "package.json"), packageJson);
|
|
615
|
+
}
|
|
616
|
+
if (scriptsAdded > 0) {
|
|
617
|
+
tasks.push(`package.json (${scriptsAdded} scripts)`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
if (husky) {
|
|
621
|
+
const deps = ["husky", "lint-staged"];
|
|
622
|
+
if (commitlint) {
|
|
623
|
+
deps.push("@commitlint/cli", "@commitlint/config-conventional");
|
|
624
|
+
}
|
|
625
|
+
if (!dryRun) {
|
|
626
|
+
const spinner3 = p.spinner();
|
|
627
|
+
spinner3.start(`Installation des d\xE9pendances (${packageManager})...`);
|
|
628
|
+
const [cmd, ...baseArgs] = pmCommands.addDev;
|
|
629
|
+
const result = runCommand(cmd, [...baseArgs, ...deps], cwd);
|
|
630
|
+
if (result.success) {
|
|
631
|
+
spinner3.stop("D\xE9pendances install\xE9es");
|
|
632
|
+
} else {
|
|
633
|
+
spinner3.stop("\xC9chec de l'installation");
|
|
634
|
+
p.log.warn(`Installez manuellement: ${pmCommands.addDev.join(" ")} ${deps.join(" ")}`);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
tasks.push(`d\xE9pendances: ${deps.join(", ")}`);
|
|
638
|
+
if (!isGitRepository(cwd)) {
|
|
639
|
+
if (!dryRun) {
|
|
640
|
+
runCommand("git", ["init"], cwd);
|
|
641
|
+
}
|
|
642
|
+
tasks.push("git init");
|
|
643
|
+
}
|
|
644
|
+
const huskyDir = join4(cwd, ".husky");
|
|
645
|
+
if (!(dryRun || existsSync4(huskyDir))) {
|
|
646
|
+
mkdirSync2(huskyDir, { recursive: true });
|
|
647
|
+
}
|
|
648
|
+
const preCommitPath = join4(huskyDir, "pre-commit");
|
|
649
|
+
if (!fileExists(preCommitPath) || force) {
|
|
650
|
+
if (!dryRun) {
|
|
651
|
+
writeFile(preCommitPath, generatePreCommitHook(pmCommands.exec), true);
|
|
652
|
+
}
|
|
653
|
+
tasks.push(".husky/pre-commit");
|
|
654
|
+
}
|
|
655
|
+
const commitMsgPath = join4(huskyDir, "commit-msg");
|
|
656
|
+
if (!fileExists(commitMsgPath) || force) {
|
|
657
|
+
if (!dryRun) {
|
|
658
|
+
writeFile(commitMsgPath, generateCommitMsgHook(commitlint, pmCommands.exec), true);
|
|
659
|
+
}
|
|
660
|
+
tasks.push(".husky/commit-msg");
|
|
661
|
+
}
|
|
662
|
+
if (!dryRun) {
|
|
663
|
+
runCommand("npx", ["husky"], cwd);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (commitlint) {
|
|
667
|
+
const commitlintPath = join4(cwd, "commitlint.config.js");
|
|
668
|
+
if (!fileExists(commitlintPath) || force) {
|
|
669
|
+
if (!dryRun) {
|
|
670
|
+
writeFile(commitlintPath, generateCommitlintConfig());
|
|
671
|
+
}
|
|
672
|
+
tasks.push("commitlint.config.js");
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
if (knip) {
|
|
676
|
+
const knipPath = join4(cwd, "knip.json");
|
|
677
|
+
if (!fileExists(knipPath) || force) {
|
|
678
|
+
if (!dryRun) {
|
|
679
|
+
writeJsonFile(knipPath, generateKnipConfig(projectType));
|
|
680
|
+
}
|
|
681
|
+
tasks.push("knip.json");
|
|
682
|
+
}
|
|
683
|
+
if (!dryRun) {
|
|
684
|
+
const [cmd, ...baseArgs] = pmCommands.addDev;
|
|
685
|
+
runCommand(cmd, [...baseArgs, "knip"], cwd);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (claudeMd) {
|
|
689
|
+
const claudeMdPath = join4(cwd, "CLAUDE.md");
|
|
690
|
+
if (!fileExists(claudeMdPath) || force) {
|
|
691
|
+
if (!dryRun) {
|
|
692
|
+
writeFile(
|
|
693
|
+
claudeMdPath,
|
|
694
|
+
generateClaudeMd({
|
|
695
|
+
projectType,
|
|
696
|
+
commitlint,
|
|
697
|
+
knip,
|
|
698
|
+
packageManager
|
|
699
|
+
})
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
tasks.push("CLAUDE.md");
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
var initCommand = defineCommand({
|
|
708
|
+
meta: {
|
|
709
|
+
name: "init",
|
|
710
|
+
description: "Initialize quality configuration in your project"
|
|
711
|
+
},
|
|
712
|
+
args: {
|
|
713
|
+
yes: {
|
|
714
|
+
type: "boolean",
|
|
715
|
+
alias: "y",
|
|
716
|
+
description: "Skip prompts and use defaults",
|
|
717
|
+
default: false
|
|
718
|
+
},
|
|
719
|
+
force: {
|
|
720
|
+
type: "boolean",
|
|
721
|
+
alias: "f",
|
|
722
|
+
description: "Overwrite existing files",
|
|
723
|
+
default: false
|
|
724
|
+
},
|
|
725
|
+
commitlint: {
|
|
726
|
+
type: "boolean",
|
|
727
|
+
alias: "c",
|
|
728
|
+
description: "Enable Conventional Commits validation",
|
|
729
|
+
default: void 0
|
|
730
|
+
},
|
|
731
|
+
"skip-husky": {
|
|
732
|
+
type: "boolean",
|
|
733
|
+
description: "Skip Husky and lint-staged setup",
|
|
734
|
+
default: false
|
|
735
|
+
},
|
|
736
|
+
"skip-vscode": {
|
|
737
|
+
type: "boolean",
|
|
738
|
+
description: "Skip VS Code configuration",
|
|
739
|
+
default: false
|
|
740
|
+
},
|
|
741
|
+
knip: {
|
|
742
|
+
type: "boolean",
|
|
743
|
+
alias: "k",
|
|
744
|
+
description: "Add Knip for dead code detection",
|
|
745
|
+
default: void 0
|
|
746
|
+
},
|
|
747
|
+
"claude-md": {
|
|
748
|
+
type: "boolean",
|
|
749
|
+
description: "Create CLAUDE.md for Claude Code instructions",
|
|
750
|
+
default: void 0
|
|
751
|
+
},
|
|
752
|
+
"skip-claude-md": {
|
|
753
|
+
type: "boolean",
|
|
754
|
+
description: "Skip CLAUDE.md creation",
|
|
755
|
+
default: false
|
|
756
|
+
},
|
|
757
|
+
"dry-run": {
|
|
758
|
+
type: "boolean",
|
|
759
|
+
alias: "d",
|
|
760
|
+
description: "Preview changes without writing files",
|
|
761
|
+
default: false
|
|
762
|
+
}
|
|
763
|
+
},
|
|
764
|
+
async run({ args: args2 }) {
|
|
765
|
+
const cwd = process.cwd();
|
|
766
|
+
const detectedPM = detectPackageManager(cwd);
|
|
767
|
+
const detectedType = detectProjectType(cwd);
|
|
768
|
+
p.intro(`${pc.cyan(pc.bold(PACKAGE_NAME))} ${pc.dim(`v${VERSION}`)}`);
|
|
769
|
+
if (args2["dry-run"]) {
|
|
770
|
+
p.log.warn(pc.yellow("Mode dry-run: aucun fichier ne sera modifi\xE9"));
|
|
771
|
+
}
|
|
772
|
+
let options;
|
|
773
|
+
if (args2.yes) {
|
|
774
|
+
options = {
|
|
775
|
+
cwd,
|
|
776
|
+
packageManager: detectedPM,
|
|
777
|
+
projectType: detectedType,
|
|
778
|
+
commitlint: args2.commitlint ?? true,
|
|
779
|
+
husky: !args2["skip-husky"],
|
|
780
|
+
vscode: !args2["skip-vscode"],
|
|
781
|
+
knip: args2.knip ?? true,
|
|
782
|
+
claudeMd: args2["claude-md"] ?? !args2["skip-claude-md"],
|
|
783
|
+
force: args2.force,
|
|
784
|
+
dryRun: args2["dry-run"]
|
|
785
|
+
};
|
|
786
|
+
p.log.info(`Projet: ${pc.cyan(options.projectType)}`);
|
|
787
|
+
p.log.info(`Package manager: ${pc.cyan(options.packageManager)}`);
|
|
788
|
+
} else {
|
|
789
|
+
const prompted = await promptInitOptions({
|
|
790
|
+
packageManager: detectedPM,
|
|
791
|
+
projectType: detectedType
|
|
792
|
+
});
|
|
793
|
+
if (!prompted) {
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
796
|
+
options = {
|
|
797
|
+
cwd,
|
|
798
|
+
...prompted,
|
|
799
|
+
force: args2.force,
|
|
800
|
+
dryRun: args2["dry-run"]
|
|
801
|
+
};
|
|
802
|
+
}
|
|
803
|
+
const spinner3 = p.spinner();
|
|
804
|
+
spinner3.start("Configuration en cours...");
|
|
805
|
+
executeInit(options);
|
|
806
|
+
spinner3.stop("Configuration termin\xE9e");
|
|
807
|
+
p.log.success(pc.green("Setup termin\xE9 !"));
|
|
808
|
+
p.note(
|
|
809
|
+
[
|
|
810
|
+
`${pc.cyan("Scripts disponibles:")}`,
|
|
811
|
+
` ${pc.dim("bun run")} check ${pc.dim("# Lint + Format")}`,
|
|
812
|
+
` ${pc.dim("bun run")} check:fix ${pc.dim("# Auto-fix")}`,
|
|
813
|
+
` ${pc.dim("bun run")} typecheck ${pc.dim("# TypeScript")}`,
|
|
814
|
+
options.knip ? ` ${pc.dim("bun run")} knip ${pc.dim("# Code mort")}` : "",
|
|
815
|
+
"",
|
|
816
|
+
options.commitlint ? [
|
|
817
|
+
`${pc.cyan("Format des commits:")}`,
|
|
818
|
+
` ${pc.green("feat")}: nouvelle fonctionnalit\xE9`,
|
|
819
|
+
` ${pc.green("fix")}: correction de bug`,
|
|
820
|
+
` ${pc.green("docs")}: documentation`
|
|
821
|
+
].join("\n") : `${pc.dim("Tip: Ajoutez commitlint avec")} quality init --commitlint`,
|
|
822
|
+
"",
|
|
823
|
+
options.claudeMd ? `${pc.cyan("CLAUDE.md cr\xE9\xE9")} ${pc.dim("- Instructions pour Claude Code")}` : `${pc.dim("Tip: Ajoutez CLAUDE.md avec")} quality init --claude-md`
|
|
824
|
+
].filter(Boolean).join("\n"),
|
|
825
|
+
"Prochaines \xE9tapes"
|
|
826
|
+
);
|
|
827
|
+
p.outro(`${pc.dim("Documentation:")} ${pc.cyan("https://github.com/neosianexus/quality")}`);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
|
|
831
|
+
// bin/commands/upgrade.ts
|
|
832
|
+
import { join as join5 } from "path";
|
|
833
|
+
import * as p2 from "@clack/prompts";
|
|
834
|
+
import { defineCommand as defineCommand2 } from "citty";
|
|
835
|
+
import merge from "deepmerge";
|
|
836
|
+
import pc2 from "picocolors";
|
|
837
|
+
function smartMerge(defaults, userConfig) {
|
|
838
|
+
return merge(defaults, userConfig, {
|
|
839
|
+
// User arrays completely override defaults (don't merge arrays)
|
|
840
|
+
arrayMerge: (_target, source) => source,
|
|
841
|
+
// Clone objects to avoid mutation
|
|
842
|
+
clone: true
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
function analyzeChanges(current, updated) {
|
|
846
|
+
const added = [];
|
|
847
|
+
const modified = [];
|
|
848
|
+
const removed = [];
|
|
849
|
+
const currentKeys = new Set(Object.keys(current));
|
|
850
|
+
const updatedKeys = new Set(Object.keys(updated));
|
|
851
|
+
for (const key of updatedKeys) {
|
|
852
|
+
if (!currentKeys.has(key)) {
|
|
853
|
+
added.push(key);
|
|
854
|
+
} else if (JSON.stringify(current[key]) !== JSON.stringify(updated[key])) {
|
|
855
|
+
modified.push(key);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
for (const key of currentKeys) {
|
|
859
|
+
if (!updatedKeys.has(key)) {
|
|
860
|
+
removed.push(key);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
return { added, modified, removed };
|
|
864
|
+
}
|
|
865
|
+
function upgradeConfigFile(config, options) {
|
|
866
|
+
if (!config.currentContent) {
|
|
867
|
+
if (!options.dryRun) {
|
|
868
|
+
writeJsonFile(config.path, config.newDefaults);
|
|
869
|
+
}
|
|
870
|
+
return { upgraded: true, backupPath: null };
|
|
871
|
+
}
|
|
872
|
+
let finalContent;
|
|
873
|
+
let backupPath = null;
|
|
874
|
+
if (config.requiresMerge) {
|
|
875
|
+
finalContent = smartMerge(config.newDefaults, config.currentContent);
|
|
876
|
+
} else {
|
|
877
|
+
finalContent = config.newDefaults;
|
|
878
|
+
}
|
|
879
|
+
if (JSON.stringify(config.currentContent) === JSON.stringify(finalContent)) {
|
|
880
|
+
return { upgraded: false, backupPath: null };
|
|
881
|
+
}
|
|
882
|
+
if (options.backup && !options.dryRun) {
|
|
883
|
+
backupPath = createBackup(config.path);
|
|
884
|
+
}
|
|
885
|
+
if (!options.dryRun) {
|
|
886
|
+
writeJsonFile(config.path, finalContent);
|
|
887
|
+
}
|
|
888
|
+
return { upgraded: true, backupPath };
|
|
889
|
+
}
|
|
890
|
+
var upgradeCommand = defineCommand2({
|
|
891
|
+
meta: {
|
|
892
|
+
name: "upgrade",
|
|
893
|
+
description: "Upgrade existing quality configuration to the latest version"
|
|
894
|
+
},
|
|
895
|
+
args: {
|
|
896
|
+
yes: {
|
|
897
|
+
type: "boolean",
|
|
898
|
+
alias: "y",
|
|
899
|
+
description: "Skip confirmation prompts",
|
|
900
|
+
default: false
|
|
901
|
+
},
|
|
902
|
+
force: {
|
|
903
|
+
type: "boolean",
|
|
904
|
+
alias: "f",
|
|
905
|
+
description: "Replace configs instead of merging",
|
|
906
|
+
default: false
|
|
907
|
+
},
|
|
908
|
+
"no-backup": {
|
|
909
|
+
type: "boolean",
|
|
910
|
+
description: "Don't create backups before modifying files",
|
|
911
|
+
default: false
|
|
912
|
+
},
|
|
913
|
+
"dry-run": {
|
|
914
|
+
type: "boolean",
|
|
915
|
+
alias: "d",
|
|
916
|
+
description: "Preview changes without writing files",
|
|
917
|
+
default: false
|
|
918
|
+
}
|
|
919
|
+
},
|
|
920
|
+
async run({ args: args2 }) {
|
|
921
|
+
const cwd = process.cwd();
|
|
922
|
+
const projectType = detectProjectType(cwd);
|
|
923
|
+
p2.intro(`${pc2.cyan(pc2.bold(PACKAGE_NAME))} ${pc2.magenta("upgrade")} ${pc2.dim(`v${VERSION}`)}`);
|
|
924
|
+
if (args2["dry-run"]) {
|
|
925
|
+
p2.log.warn(pc2.yellow("Mode dry-run: aucun fichier ne sera modifi\xE9"));
|
|
926
|
+
}
|
|
927
|
+
const configs = [
|
|
928
|
+
{
|
|
929
|
+
name: "biome.json",
|
|
930
|
+
path: join5(cwd, "biome.json"),
|
|
931
|
+
currentContent: readJsonFile(join5(cwd, "biome.json")),
|
|
932
|
+
newDefaults: generateBiomeConfig(),
|
|
933
|
+
requiresMerge: true
|
|
934
|
+
// biome.json can be safely merged
|
|
935
|
+
},
|
|
936
|
+
{
|
|
937
|
+
name: "tsconfig.json",
|
|
938
|
+
path: join5(cwd, "tsconfig.json"),
|
|
939
|
+
currentContent: readJsonFile(join5(cwd, "tsconfig.json")),
|
|
940
|
+
newDefaults: generateTsConfig(projectType),
|
|
941
|
+
requiresMerge: true
|
|
942
|
+
// tsconfig.json should preserve user paths/includes
|
|
943
|
+
},
|
|
944
|
+
{
|
|
945
|
+
name: "knip.json",
|
|
946
|
+
path: join5(cwd, "knip.json"),
|
|
947
|
+
currentContent: readJsonFile(join5(cwd, "knip.json")),
|
|
948
|
+
newDefaults: generateKnipConfig(projectType),
|
|
949
|
+
requiresMerge: true
|
|
950
|
+
// knip.json should preserve user ignores
|
|
951
|
+
},
|
|
952
|
+
{
|
|
953
|
+
name: ".vscode/settings.json",
|
|
954
|
+
path: join5(cwd, ".vscode", "settings.json"),
|
|
955
|
+
currentContent: readJsonFile(join5(cwd, ".vscode", "settings.json")),
|
|
956
|
+
newDefaults: getVscodeSettings(),
|
|
957
|
+
requiresMerge: true
|
|
958
|
+
// VSCode settings should be merged
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
name: ".vscode/extensions.json",
|
|
962
|
+
path: join5(cwd, ".vscode", "extensions.json"),
|
|
963
|
+
currentContent: readJsonFile(join5(cwd, ".vscode", "extensions.json")),
|
|
964
|
+
newDefaults: getVscodeExtensions(),
|
|
965
|
+
requiresMerge: true
|
|
966
|
+
// Extensions can be merged
|
|
967
|
+
}
|
|
968
|
+
];
|
|
969
|
+
const toUpgrade = [];
|
|
970
|
+
for (const config of configs) {
|
|
971
|
+
if (!config.currentContent) {
|
|
972
|
+
toUpgrade.push({
|
|
973
|
+
config,
|
|
974
|
+
changes: { added: Object.keys(config.newDefaults), modified: [], removed: [] },
|
|
975
|
+
isNew: true
|
|
976
|
+
});
|
|
977
|
+
} else {
|
|
978
|
+
const merged = args2.force ? config.newDefaults : smartMerge(config.newDefaults, config.currentContent);
|
|
979
|
+
const changes = analyzeChanges(config.currentContent, merged);
|
|
980
|
+
if (changes.added.length > 0 || changes.modified.length > 0) {
|
|
981
|
+
toUpgrade.push({ config, changes, isNew: false });
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
const packageJson = readPackageJson(cwd);
|
|
986
|
+
const newScripts = getPackageScripts({ commitlint: true, knip: true, husky: true });
|
|
987
|
+
const currentScripts = packageJson?.scripts || {};
|
|
988
|
+
const missingScripts = [];
|
|
989
|
+
for (const [name] of Object.entries(newScripts)) {
|
|
990
|
+
if (!currentScripts[name]) {
|
|
991
|
+
missingScripts.push(name);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (toUpgrade.length === 0 && missingScripts.length === 0) {
|
|
995
|
+
p2.log.success("Toutes les configurations sont \xE0 jour !");
|
|
996
|
+
p2.outro(pc2.dim("Rien \xE0 faire."));
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
p2.log.info(pc2.cyan("Fichiers \xE0 mettre \xE0 jour:"));
|
|
1000
|
+
for (const { config, changes, isNew } of toUpgrade) {
|
|
1001
|
+
if (isNew) {
|
|
1002
|
+
p2.log.step(` ${pc2.green("+")} ${config.name} ${pc2.dim("(nouveau)")}`);
|
|
1003
|
+
} else {
|
|
1004
|
+
const parts = [];
|
|
1005
|
+
if (changes.added.length > 0) {
|
|
1006
|
+
parts.push(pc2.green(`+${changes.added.length}`));
|
|
1007
|
+
}
|
|
1008
|
+
if (changes.modified.length > 0) {
|
|
1009
|
+
parts.push(pc2.yellow(`~${changes.modified.length}`));
|
|
1010
|
+
}
|
|
1011
|
+
p2.log.step(` ${pc2.yellow("~")} ${config.name} ${pc2.dim(`(${parts.join(", ")})`)}`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (missingScripts.length > 0) {
|
|
1015
|
+
p2.log.step(` ${pc2.green("+")} package.json ${pc2.dim(`(${missingScripts.length} scripts)`)}`);
|
|
1016
|
+
}
|
|
1017
|
+
if (!(args2.yes || args2["dry-run"])) {
|
|
1018
|
+
const shouldContinue = await p2.confirm({
|
|
1019
|
+
message: args2["no-backup"] ? "Continuer sans backup ?" : "Continuer ? (les fichiers seront sauvegard\xE9s)",
|
|
1020
|
+
initialValue: true
|
|
1021
|
+
});
|
|
1022
|
+
if (!shouldContinue || p2.isCancel(shouldContinue)) {
|
|
1023
|
+
p2.cancel("Annul\xE9.");
|
|
1024
|
+
process.exit(0);
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
const spinner3 = p2.spinner();
|
|
1028
|
+
spinner3.start("Mise \xE0 jour des configurations...");
|
|
1029
|
+
const results = [];
|
|
1030
|
+
for (const { config } of toUpgrade) {
|
|
1031
|
+
const result = upgradeConfigFile(config, {
|
|
1032
|
+
force: args2.force,
|
|
1033
|
+
dryRun: args2["dry-run"],
|
|
1034
|
+
backup: !args2["no-backup"]
|
|
1035
|
+
});
|
|
1036
|
+
if (result.upgraded) {
|
|
1037
|
+
results.push({ name: config.name, backupPath: result.backupPath });
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
if (packageJson && missingScripts.length > 0) {
|
|
1041
|
+
if (!(args2["no-backup"] || args2["dry-run"])) {
|
|
1042
|
+
createBackup(join5(cwd, "package.json"));
|
|
1043
|
+
}
|
|
1044
|
+
const scripts = packageJson.scripts || {};
|
|
1045
|
+
for (const name of missingScripts) {
|
|
1046
|
+
const script = newScripts[name];
|
|
1047
|
+
if (script) {
|
|
1048
|
+
scripts[name] = script;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
packageJson.scripts = scripts;
|
|
1052
|
+
if (!packageJson["lint-staged"]) {
|
|
1053
|
+
packageJson["lint-staged"] = getLintStagedConfig();
|
|
1054
|
+
}
|
|
1055
|
+
if (!args2["dry-run"]) {
|
|
1056
|
+
writeJsonFile(join5(cwd, "package.json"), packageJson);
|
|
1057
|
+
}
|
|
1058
|
+
results.push({ name: "package.json", backupPath: null });
|
|
1059
|
+
}
|
|
1060
|
+
spinner3.stop("Mise \xE0 jour termin\xE9e");
|
|
1061
|
+
p2.log.success(pc2.green(`${results.length} fichier(s) mis \xE0 jour`));
|
|
1062
|
+
const backups = results.filter((r) => r.backupPath);
|
|
1063
|
+
if (backups.length > 0) {
|
|
1064
|
+
p2.note(backups.map((b) => `${pc2.dim(b.backupPath)}`).join("\n"), "Backups cr\xE9\xE9s");
|
|
1065
|
+
}
|
|
1066
|
+
if (args2["dry-run"]) {
|
|
1067
|
+
p2.note("Ex\xE9cutez sans --dry-run pour appliquer les changements", "Mode dry-run");
|
|
1068
|
+
}
|
|
1069
|
+
p2.outro(pc2.green("Configuration mise \xE0 jour !"));
|
|
1070
|
+
}
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
// bin/cli.ts
|
|
1074
|
+
var main = defineCommand3({
|
|
1075
|
+
meta: {
|
|
1076
|
+
name: "quality",
|
|
1077
|
+
version: VERSION,
|
|
1078
|
+
description: `${PACKAGE_NAME} - Ultra-strict Biome + TypeScript + Husky configuration`
|
|
1079
|
+
},
|
|
1080
|
+
subCommands: {
|
|
1081
|
+
init: initCommand,
|
|
1082
|
+
upgrade: upgradeCommand
|
|
1083
|
+
}
|
|
1084
|
+
});
|
|
1085
|
+
var args = process.argv.slice(2);
|
|
1086
|
+
var subcommands = ["init", "upgrade"];
|
|
1087
|
+
var hasSubcommand = args.some(
|
|
1088
|
+
(arg) => subcommands.includes(arg) || arg === "--help" || arg === "-h" || arg === "--version"
|
|
1089
|
+
);
|
|
1090
|
+
if (hasSubcommand) {
|
|
1091
|
+
runMain(main);
|
|
1092
|
+
} else {
|
|
1093
|
+
runCommand2(initCommand, { rawArgs: args });
|
|
1094
|
+
}
|
|
1095
|
+
/**
|
|
1096
|
+
* @fileoverview Quality CLI - Ultra-strict Biome + TypeScript + Husky configuration
|
|
1097
|
+
* @author neosianexus
|
|
1098
|
+
* @license MIT
|
|
1099
|
+
*/
|