@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/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
+ */