@soulbatical/tetra-dev-toolkit 1.3.3 → 1.5.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.
@@ -9,10 +9,18 @@
9
9
  * - Configuration file
10
10
  *
11
11
  * Usage:
12
- * tetra-setup # Interactive setup
12
+ * tetra-setup # Interactive setup (hooks + ci + config)
13
13
  * tetra-setup hooks # Setup Husky hooks only
14
14
  * tetra-setup ci # Setup GitHub Actions only
15
15
  * tetra-setup config # Create .tetra-quality.json
16
+ * tetra-setup prettier # Setup Prettier + lint-staged
17
+ * tetra-setup eslint-security # Setup ESLint security plugins
18
+ * tetra-setup coverage # Setup test coverage thresholds
19
+ * tetra-setup lighthouse # Setup Lighthouse CI budgets
20
+ * tetra-setup commitlint # Setup commitlint + commit-msg hook
21
+ * tetra-setup depcruiser # Setup dependency-cruiser
22
+ * tetra-setup knip # Setup Knip (dead code detection)
23
+ * tetra-setup license-audit # Setup license compliance checking
16
24
  */
17
25
 
18
26
  import { program } from 'commander'
@@ -26,7 +34,7 @@ program
26
34
  .name('tetra-setup')
27
35
  .description('Setup Tetra Dev Toolkit in your project')
28
36
  .version('1.2.0')
29
- .argument('[component]', 'Component to setup: hooks, ci, config, or all (default)')
37
+ .argument('[component]', 'Component to setup: hooks, ci, config, prettier, eslint-security, coverage, lighthouse, commitlint, depcruiser, knip, license-audit, or all')
30
38
  .option('-f, --force', 'Overwrite existing files')
31
39
  .action(async (component, options) => {
32
40
  console.log('')
@@ -49,8 +57,33 @@ program
49
57
  case 'config':
50
58
  await setupConfig(options)
51
59
  break
60
+ case 'prettier':
61
+ await setupPrettier(options)
62
+ break
63
+ case 'eslint-security':
64
+ await setupEslintSecurity(options)
65
+ break
66
+ case 'coverage':
67
+ await setupCoverage(options)
68
+ break
69
+ case 'lighthouse':
70
+ await setupLighthouse(options)
71
+ break
72
+ case 'commitlint':
73
+ await setupCommitlint(options)
74
+ break
75
+ case 'depcruiser':
76
+ await setupDepCruiser(options)
77
+ break
78
+ case 'knip':
79
+ await setupKnip(options)
80
+ break
81
+ case 'license-audit':
82
+ await setupLicenseAudit(options)
83
+ break
52
84
  default:
53
85
  console.log(`Unknown component: ${comp}`)
86
+ console.log('Available: hooks, ci, config, prettier, eslint-security, coverage, lighthouse, commitlint, depcruiser, knip, license-audit')
54
87
  }
55
88
  }
56
89
 
@@ -258,4 +291,524 @@ async function setupConfig(options) {
258
291
  }
259
292
  }
260
293
 
294
+ // ─── Prettier ───────────────────────────────────────────────────
295
+
296
+ async function setupPrettier(options) {
297
+ console.log('🎨 Setting up Prettier + lint-staged...')
298
+
299
+ const prettierPath = join(projectRoot, '.prettierrc.json')
300
+ if (!existsSync(prettierPath) || options.force) {
301
+ const prettierConfig = {
302
+ semi: true,
303
+ trailingComma: 'es5',
304
+ singleQuote: true,
305
+ printWidth: 100,
306
+ tabWidth: 2,
307
+ useTabs: false,
308
+ arrowParens: 'always',
309
+ endOfLine: 'lf'
310
+ }
311
+ writeFileSync(prettierPath, JSON.stringify(prettierConfig, null, 2) + '\n')
312
+ console.log(' ✅ Created .prettierrc.json')
313
+ } else {
314
+ console.log(' ⏭️ .prettierrc.json already exists')
315
+ }
316
+
317
+ const ignorePath = join(projectRoot, '.prettierignore')
318
+ if (!existsSync(ignorePath) || options.force) {
319
+ const ignoreContent = `node_modules
320
+ dist
321
+ build
322
+ .next
323
+ coverage
324
+ *.min.js
325
+ *.min.css
326
+ package-lock.json
327
+ `
328
+ writeFileSync(ignorePath, ignoreContent)
329
+ console.log(' ✅ Created .prettierignore')
330
+ }
331
+
332
+ // Add lint-staged config
333
+ const lintStagedPath = join(projectRoot, '.lintstagedrc.json')
334
+ if (!existsSync(lintStagedPath) || options.force) {
335
+ const lintStagedConfig = {
336
+ '*.{js,jsx,ts,tsx}': ['eslint --fix', 'prettier --write'],
337
+ '*.{json,css,scss,md,yml,yaml}': ['prettier --write']
338
+ }
339
+ writeFileSync(lintStagedPath, JSON.stringify(lintStagedConfig, null, 2) + '\n')
340
+ console.log(' ✅ Created .lintstagedrc.json')
341
+ }
342
+
343
+ // Add scripts to package.json
344
+ const packagePath = join(projectRoot, 'package.json')
345
+ if (existsSync(packagePath)) {
346
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
347
+ pkg.scripts = pkg.scripts || {}
348
+ let changed = false
349
+
350
+ if (!pkg.scripts.format) {
351
+ pkg.scripts.format = 'prettier --write .'
352
+ changed = true
353
+ }
354
+ if (!pkg.scripts['format:check']) {
355
+ pkg.scripts['format:check'] = 'prettier --check .'
356
+ changed = true
357
+ }
358
+
359
+ if (changed) {
360
+ writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
361
+ console.log(' ✅ Added format scripts to package.json')
362
+ }
363
+ }
364
+
365
+ console.log(' 📦 Run: npm install --save-dev prettier lint-staged')
366
+ }
367
+
368
+ // ─── ESLint Security ────────────────────────────────────────────
369
+
370
+ async function setupEslintSecurity(options) {
371
+ console.log('🔒 Setting up ESLint security plugins...')
372
+
373
+ // Check for existing ESLint config
374
+ const eslintConfigs = [
375
+ '.eslintrc.js', '.eslintrc.cjs', '.eslintrc.json',
376
+ 'eslint.config.js', 'eslint.config.mjs',
377
+ 'backend/.eslintrc.js', 'backend/eslint.config.js'
378
+ ]
379
+
380
+ let hasEslint = false
381
+ for (const config of eslintConfigs) {
382
+ if (existsSync(join(projectRoot, config))) {
383
+ hasEslint = true
384
+ console.log(` Found existing config: ${config}`)
385
+ break
386
+ }
387
+ }
388
+
389
+ if (!hasEslint) {
390
+ // Create a new ESLint flat config with security plugins
391
+ const eslintPath = join(projectRoot, 'eslint.config.js')
392
+ if (!existsSync(eslintPath) || options.force) {
393
+ const eslintContent = `import js from '@eslint/js'
394
+ import security from 'eslint-plugin-security'
395
+ import sonarjs from 'eslint-plugin-sonarjs'
396
+
397
+ export default [
398
+ js.configs.recommended,
399
+ {
400
+ plugins: {
401
+ security,
402
+ sonarjs
403
+ },
404
+ rules: {
405
+ // Security rules
406
+ 'security/detect-unsafe-regex': 'error',
407
+ 'security/detect-eval-with-expression': 'error',
408
+ 'security/detect-no-csrf-before-method-override': 'error',
409
+ 'security/detect-non-literal-regexp': 'warn',
410
+ 'security/detect-object-injection': 'off', // too many false positives
411
+ 'security/detect-possible-timing-attacks': 'warn',
412
+
413
+ // SonarJS rules
414
+ 'sonarjs/cognitive-complexity': ['warn', 25],
415
+ 'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }],
416
+ 'sonarjs/slow-regex': 'warn',
417
+
418
+ // General safety
419
+ 'no-eval': 'error',
420
+ 'no-implied-eval': 'error',
421
+ 'no-new-func': 'error'
422
+ }
423
+ }
424
+ ]
425
+ `
426
+ writeFileSync(eslintPath, eslintContent)
427
+ console.log(' ✅ Created eslint.config.js with security plugins')
428
+ }
429
+ } else {
430
+ console.log(' ⚠️ ESLint config exists — manually add these plugins:')
431
+ console.log(' - eslint-plugin-security')
432
+ console.log(' - eslint-plugin-sonarjs')
433
+ }
434
+
435
+ console.log(' 📦 Run: npm install --save-dev eslint eslint-plugin-security eslint-plugin-sonarjs')
436
+ }
437
+
438
+ // ─── Coverage Thresholds ────────────────────────────────────────
439
+
440
+ async function setupCoverage(options) {
441
+ console.log('📊 Setting up test coverage thresholds...')
442
+
443
+ // Detect test framework
444
+ const packagePath = join(projectRoot, 'package.json')
445
+ let framework = null
446
+
447
+ const packageLocations = ['package.json', 'backend/package.json']
448
+ for (const loc of packageLocations) {
449
+ const pkgPath = join(projectRoot, loc)
450
+ if (!existsSync(pkgPath)) continue
451
+
452
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'))
453
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
454
+ if (allDeps.vitest) { framework = 'vitest'; break }
455
+ if (allDeps.jest) { framework = 'jest'; break }
456
+ }
457
+
458
+ if (!framework) {
459
+ console.log(' ⚠️ No test framework detected. Install vitest or jest first.')
460
+ console.log(' 📦 Recommended: npm install --save-dev vitest @vitest/coverage-v8')
461
+ return
462
+ }
463
+
464
+ if (framework === 'vitest') {
465
+ const vitestPath = join(projectRoot, 'vitest.config.ts')
466
+ if (!existsSync(vitestPath) || options.force) {
467
+ const vitestContent = `import { defineConfig } from 'vitest/config'
468
+
469
+ export default defineConfig({
470
+ test: {
471
+ coverage: {
472
+ provider: 'v8',
473
+ reporter: ['text', 'lcov', 'html'],
474
+ reportsDirectory: './coverage',
475
+ thresholds: {
476
+ statements: 70,
477
+ branches: 60,
478
+ functions: 70,
479
+ lines: 70
480
+ }
481
+ }
482
+ }
483
+ })
484
+ `
485
+ writeFileSync(vitestPath, vitestContent)
486
+ console.log(' ✅ Created vitest.config.ts with coverage thresholds (70/60/70/70)')
487
+ }
488
+ console.log(' 📦 Run: npm install --save-dev @vitest/coverage-v8')
489
+ }
490
+
491
+ if (framework === 'jest') {
492
+ console.log(' ℹ️ Add to your jest.config.js:')
493
+ console.log(`
494
+ coverageThreshold: {
495
+ global: {
496
+ statements: 70,
497
+ branches: 60,
498
+ functions: 70,
499
+ lines: 70
500
+ }
501
+ }
502
+ `)
503
+ }
504
+
505
+ // Add coverage script
506
+ if (existsSync(packagePath)) {
507
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
508
+ pkg.scripts = pkg.scripts || {}
509
+ if (!pkg.scripts['test:coverage']) {
510
+ pkg.scripts['test:coverage'] = framework === 'vitest'
511
+ ? 'vitest run --coverage'
512
+ : 'jest --coverage'
513
+ writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
514
+ console.log(' ✅ Added test:coverage script to package.json')
515
+ }
516
+ }
517
+ }
518
+
519
+ // ─── Lighthouse ─────────────────────────────────────────────────
520
+
521
+ async function setupLighthouse(options) {
522
+ console.log('🏎️ Setting up Lighthouse CI...')
523
+
524
+ const lighthousercPath = join(projectRoot, 'lighthouserc.json')
525
+ if (!existsSync(lighthousercPath) || options.force) {
526
+ const config = {
527
+ ci: {
528
+ collect: {
529
+ numberOfRuns: 3,
530
+ settings: {
531
+ preset: 'desktop'
532
+ }
533
+ },
534
+ assert: {
535
+ assertions: {
536
+ 'categories:performance': ['warn', { minScore: 0.7 }],
537
+ 'categories:seo': ['error', { minScore: 0.9 }],
538
+ 'categories:accessibility': ['warn', { minScore: 0.85 }],
539
+ 'categories:best-practices': ['warn', { minScore: 0.8 }],
540
+ 'first-contentful-paint': ['warn', { maxNumericValue: 2500 }],
541
+ 'largest-contentful-paint': ['warn', { maxNumericValue: 3500 }],
542
+ 'cumulative-layout-shift': ['warn', { maxNumericValue: 0.1 }],
543
+ 'total-blocking-time': ['warn', { maxNumericValue: 500 }]
544
+ }
545
+ }
546
+ }
547
+ }
548
+ writeFileSync(lighthousercPath, JSON.stringify(config, null, 2) + '\n')
549
+ console.log(' ✅ Created lighthouserc.json')
550
+ }
551
+
552
+ const budgetPath = join(projectRoot, 'lighthouse-budget.json')
553
+ if (!existsSync(budgetPath) || options.force) {
554
+ const budget = [{
555
+ path: '/*',
556
+ resourceSizes: [
557
+ { resourceType: 'script', budget: 500 },
558
+ { resourceType: 'image', budget: 300 },
559
+ { resourceType: 'stylesheet', budget: 100 },
560
+ { resourceType: 'font', budget: 100 },
561
+ { resourceType: 'total', budget: 1500 }
562
+ ],
563
+ resourceCounts: [
564
+ { resourceType: 'third-party', budget: 15 }
565
+ ]
566
+ }]
567
+ writeFileSync(budgetPath, JSON.stringify(budget, null, 2) + '\n')
568
+ console.log(' ✅ Created lighthouse-budget.json')
569
+ }
570
+
571
+ // Create GitHub Actions workflow
572
+ const workflowDir = join(projectRoot, '.github/workflows')
573
+ if (!existsSync(workflowDir)) mkdirSync(workflowDir, { recursive: true })
574
+
575
+ const workflowPath = join(workflowDir, 'lighthouse.yml')
576
+ if (!existsSync(workflowPath) || options.force) {
577
+ const workflow = `name: Lighthouse CI
578
+
579
+ on:
580
+ push:
581
+ branches: [main, master]
582
+ pull_request:
583
+ branches: [main, master]
584
+
585
+ jobs:
586
+ lighthouse:
587
+ name: Lighthouse Audit
588
+ runs-on: ubuntu-latest
589
+ steps:
590
+ - uses: actions/checkout@v4
591
+ - name: Audit URLs
592
+ uses: treosh/lighthouse-ci-action@v12
593
+ with:
594
+ configPath: ./lighthouserc.json
595
+ budgetPath: ./lighthouse-budget.json
596
+ uploadArtifacts: true
597
+ temporaryPublicStorage: true
598
+ `
599
+ writeFileSync(workflowPath, workflow)
600
+ console.log(' ✅ Created .github/workflows/lighthouse.yml')
601
+ }
602
+
603
+ console.log(' ℹ️ Update lighthouserc.json with your production URLs')
604
+ }
605
+
606
+ // ─── Commitlint ─────────────────────────────────────────────────
607
+
608
+ async function setupCommitlint(options) {
609
+ console.log('📝 Setting up commitlint...')
610
+
611
+ const commitlintPath = join(projectRoot, 'commitlint.config.js')
612
+ if (!existsSync(commitlintPath) || options.force) {
613
+ const content = `export default {
614
+ extends: ['@commitlint/config-conventional'],
615
+ rules: {
616
+ 'type-enum': [2, 'always', [
617
+ 'feat', 'fix', 'docs', 'style', 'refactor',
618
+ 'perf', 'test', 'build', 'ci', 'chore', 'security'
619
+ ]],
620
+ 'subject-case': [2, 'never', ['start-case', 'pascal-case', 'upper-case']],
621
+ 'header-max-length': [2, 'always', 100]
622
+ }
623
+ }
624
+ `
625
+ writeFileSync(commitlintPath, content)
626
+ console.log(' ✅ Created commitlint.config.js')
627
+ }
628
+
629
+ // Add commit-msg hook
630
+ const huskyDir = join(projectRoot, '.husky')
631
+ if (existsSync(huskyDir)) {
632
+ const commitMsgPath = join(huskyDir, 'commit-msg')
633
+ if (!existsSync(commitMsgPath) || options.force) {
634
+ const hookContent = `#!/bin/sh
635
+ npx --no -- commitlint --edit \${1}
636
+ `
637
+ writeFileSync(commitMsgPath, hookContent)
638
+ execSync(`chmod +x ${commitMsgPath}`)
639
+ console.log(' ✅ Created .husky/commit-msg hook')
640
+ }
641
+ } else {
642
+ console.log(' ⚠️ No .husky directory — run "tetra-setup hooks" first')
643
+ }
644
+
645
+ console.log(' 📦 Run: npm install --save-dev @commitlint/cli @commitlint/config-conventional')
646
+ }
647
+
648
+ // ─── Dependency Cruiser ─────────────────────────────────────────
649
+
650
+ async function setupDepCruiser(options) {
651
+ console.log('🔍 Setting up dependency-cruiser...')
652
+
653
+ const configPath = join(projectRoot, '.dependency-cruiser.cjs')
654
+ if (!existsSync(configPath) || options.force) {
655
+ const content = `/** @type {import('dependency-cruiser').IConfiguration} */
656
+ module.exports = {
657
+ forbidden: [
658
+ {
659
+ name: 'no-circular',
660
+ severity: 'error',
661
+ comment: 'Circular dependencies cause maintenance problems and unexpected behavior',
662
+ from: {},
663
+ to: { circular: true }
664
+ },
665
+ {
666
+ name: 'no-orphans',
667
+ severity: 'info',
668
+ comment: 'Modules without incoming or outgoing connections',
669
+ from: { orphan: true, pathNot: ['\\\\.(test|spec)\\\\.(ts|js)$', 'types\\\\.ts$'] },
670
+ to: {}
671
+ },
672
+ {
673
+ name: 'no-dev-deps-in-src',
674
+ severity: 'error',
675
+ comment: 'devDependencies should not be imported in production code',
676
+ from: { path: '^src', pathNot: '\\\\.(test|spec)\\\\.' },
677
+ to: { dependencyTypes: ['npm-dev'] }
678
+ },
679
+ {
680
+ name: 'no-deprecated-core',
681
+ severity: 'warn',
682
+ comment: 'Deprecated Node.js core modules',
683
+ from: {},
684
+ to: { dependencyTypes: ['core'], path: '^(punycode|domain|constants|sys|_linklist|_stream_wrap)$' }
685
+ }
686
+ ],
687
+ options: {
688
+ doNotFollow: { path: 'node_modules' },
689
+ tsPreCompilationDeps: true,
690
+ tsConfig: { fileName: 'tsconfig.json' },
691
+ enhancedResolveOptions: {
692
+ exportsFields: ['exports'],
693
+ conditionNames: ['import', 'require', 'node', 'default']
694
+ },
695
+ reporterOptions: {
696
+ dot: { collapsePattern: 'node_modules/[^/]+' }
697
+ }
698
+ }
699
+ }
700
+ `
701
+ writeFileSync(configPath, content)
702
+ console.log(' ✅ Created .dependency-cruiser.cjs')
703
+ }
704
+
705
+ // Add scripts
706
+ const packagePath = join(projectRoot, 'package.json')
707
+ if (existsSync(packagePath)) {
708
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
709
+ pkg.scripts = pkg.scripts || {}
710
+ let changed = false
711
+
712
+ if (!pkg.scripts['check:deps']) {
713
+ pkg.scripts['check:deps'] = 'depcruise src --config .dependency-cruiser.cjs'
714
+ changed = true
715
+ }
716
+ if (!pkg.scripts['check:circular']) {
717
+ pkg.scripts['check:circular'] = 'depcruise src --config .dependency-cruiser.cjs --output-type err-only'
718
+ changed = true
719
+ }
720
+
721
+ if (changed) {
722
+ writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
723
+ console.log(' ✅ Added check:deps and check:circular scripts')
724
+ }
725
+ }
726
+
727
+ console.log(' 📦 Run: npm install --save-dev dependency-cruiser')
728
+ }
729
+
730
+ // ─── Knip (Dead Code Detection) ─────────────────────────────
731
+
732
+ async function setupKnip(options) {
733
+ console.log('🔍 Setting up Knip (dead code detection)...')
734
+
735
+ const knipPath = join(projectRoot, 'knip.config.ts')
736
+ if (!existsSync(knipPath) || options.force) {
737
+ const content = `import type { KnipConfig } from 'knip'
738
+
739
+ const config: KnipConfig = {
740
+ entry: ['src/index.{ts,js}', 'src/main.{ts,tsx}'],
741
+ project: ['src/**/*.{ts,tsx,js,jsx}'],
742
+ ignore: [
743
+ '**/*.test.{ts,tsx}',
744
+ '**/*.spec.{ts,tsx}',
745
+ '**/test/**',
746
+ '**/tests/**'
747
+ ],
748
+ ignoreDependencies: [
749
+ // Add dependencies that are used but not detected by Knip
750
+ ]
751
+ }
752
+
753
+ export default config
754
+ `
755
+ writeFileSync(knipPath, content)
756
+ console.log(' ✅ Created knip.config.ts')
757
+ } else {
758
+ console.log(' ⏭️ knip.config.ts already exists')
759
+ }
760
+
761
+ // Add scripts
762
+ const packagePath = join(projectRoot, 'package.json')
763
+ if (existsSync(packagePath)) {
764
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
765
+ pkg.scripts = pkg.scripts || {}
766
+ let changed = false
767
+
768
+ if (!pkg.scripts.knip && !pkg.scripts['check:unused']) {
769
+ pkg.scripts.knip = 'knip'
770
+ pkg.scripts['knip:fix'] = 'knip --fix'
771
+ changed = true
772
+ }
773
+
774
+ if (changed) {
775
+ writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
776
+ console.log(' ✅ Added knip scripts to package.json')
777
+ }
778
+ }
779
+
780
+ console.log(' 📦 Run: npm install --save-dev knip')
781
+ }
782
+
783
+ // ─── License Audit ──────────────────────────────────────────
784
+
785
+ async function setupLicenseAudit(options) {
786
+ console.log('📜 Setting up license compliance checking...')
787
+
788
+ // Add license check script
789
+ const packagePath = join(projectRoot, 'package.json')
790
+ if (existsSync(packagePath)) {
791
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'))
792
+ pkg.scripts = pkg.scripts || {}
793
+ let changed = false
794
+
795
+ if (!pkg.scripts['check:licenses']) {
796
+ pkg.scripts['check:licenses'] = 'license-checker --production --failOn "GPL-2.0;GPL-3.0;AGPL-1.0;AGPL-3.0" --excludePrivatePackages'
797
+ changed = true
798
+ }
799
+ if (!pkg.scripts['licenses:summary']) {
800
+ pkg.scripts['licenses:summary'] = 'license-checker --production --summary'
801
+ changed = true
802
+ }
803
+
804
+ if (changed) {
805
+ writeFileSync(packagePath, JSON.stringify(pkg, null, 2) + '\n')
806
+ console.log(' ✅ Added license check scripts to package.json')
807
+ console.log(' ℹ️ Blocked licenses: GPL-2.0, GPL-3.0, AGPL-1.0, AGPL-3.0')
808
+ }
809
+ }
810
+
811
+ console.log(' 📦 Run: npm install --save-dev license-checker')
812
+ }
813
+
261
814
  program.parse()