@raftlabs/raftstack 1.7.2 → 1.8.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.
Files changed (3) hide show
  1. package/dist/cli.js +1066 -203
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -496,8 +496,26 @@ function getCommitMsgHook() {
496
496
  return `commitlint --edit "$1"
497
497
  `;
498
498
  }
499
- function getPrePushHook() {
500
- return `validate-branch-name
499
+ function getBuildCommand(projectType) {
500
+ switch (projectType) {
501
+ case "turbo":
502
+ return "pnpm turbo build";
503
+ case "nx":
504
+ return "pnpm nx affected --target=build --parallel=3";
505
+ case "pnpm-workspace":
506
+ return "pnpm -r build";
507
+ default:
508
+ return "pnpm build";
509
+ }
510
+ }
511
+ function getPrePushHook(projectType) {
512
+ const buildCommand = getBuildCommand(projectType);
513
+ return `# Validate branch naming convention
514
+ validate-branch-name
515
+
516
+ # Build all packages - push only succeeds if builds pass
517
+ echo "\u{1F528} Building..."
518
+ ${buildCommand}
501
519
  `;
502
520
  }
503
521
  async function generateHuskyHooks(targetDir, projectType, _pm) {
@@ -534,7 +552,7 @@ async function generateHuskyHooks(targetDir, projectType, _pm) {
534
552
  }
535
553
  }
536
554
  const prePushPath = join4(huskyDir, "pre-push");
537
- const prePushResult = await writeFileSafe(prePushPath, getPrePushHook(), {
555
+ const prePushResult = await writeFileSafe(prePushPath, getPrePushHook(projectType), {
538
556
  executable: true,
539
557
  backup: true
540
558
  });
@@ -550,41 +568,15 @@ async function generateHuskyHooks(targetDir, projectType, _pm) {
550
568
  // src/generators/commitlint.ts
551
569
  import { join as join5 } from "path";
552
570
  function getCommitlintConfig(asanaBaseUrl) {
553
- const baseConfig = `/** @type {import('@commitlint/types').UserConfig} */
554
- export default {
555
- extends: ['@commitlint/config-conventional'],
556
- rules: {
557
- // Type must be one of the conventional types
558
- 'type-enum': [
559
- 2,
560
- 'always',
561
- [
562
- 'feat', // New feature
563
- 'fix', // Bug fix
564
- 'docs', // Documentation changes
565
- 'style', // Code style changes (formatting, etc.)
566
- 'refactor', // Code refactoring
567
- 'perf', // Performance improvements
568
- 'test', // Adding or updating tests
569
- 'build', // Build system changes
570
- 'ci', // CI configuration changes
571
- 'chore', // Other changes (maintenance, etc.)
572
- 'revert', // Reverting changes
573
- ],
574
- ],
575
- // Subject should not be empty
576
- 'subject-empty': [2, 'never'],
577
- // Type should not be empty
578
- 'type-empty': [2, 'never'],
579
- // Subject should be lowercase
580
- 'subject-case': [2, 'always', 'lower-case'],
581
- // Header max length
582
- 'header-max-length': [2, 'always', 100],`;
583
- if (asanaBaseUrl) {
584
- return `${baseConfig}
585
- // Asana task link (warning only - won't block commits)
586
- 'asana-task-link': [1, 'always'],
587
- },
571
+ const asanaIssueSection = asanaBaseUrl ? `
572
+ allowCustomIssuePrefix: true,
573
+ issuePrefixes: [
574
+ { value: 'asana', name: 'asana: Link to Asana task' },
575
+ { value: 'closes', name: 'closes: Close an issue' },
576
+ { value: 'fixes', name: 'fixes: Fix an issue' },
577
+ ],` : `
578
+ allowCustomIssuePrefix: false,`;
579
+ const asanaPluginSection = asanaBaseUrl ? `
588
580
  plugins: [
589
581
  {
590
582
  rules: {
@@ -601,57 +593,22 @@ export default {
601
593
  },
602
594
  },
603
595
  },
604
- ],
605
- };
606
- `;
607
- }
608
- return `${baseConfig}
609
- },
610
- };
611
- `;
612
- }
613
- async function generateCommitlint(targetDir, asanaBaseUrl) {
614
- const result = {
615
- created: [],
616
- modified: [],
617
- skipped: [],
618
- backedUp: []
619
- };
620
- const configPath = join5(targetDir, "commitlint.config.js");
621
- const writeResult = await writeFileSafe(
622
- configPath,
623
- getCommitlintConfig(asanaBaseUrl),
624
- { backup: true }
625
- );
626
- if (writeResult.created) {
627
- result.created.push("commitlint.config.js");
628
- if (writeResult.backedUp) {
629
- result.backedUp.push(writeResult.backedUp);
630
- }
631
- }
632
- return result;
633
- }
634
-
635
- // src/generators/cz-git.ts
636
- import { join as join6 } from "path";
637
- function getCzGitConfig(asanaBaseUrl) {
638
- const asanaSection = asanaBaseUrl ? `
639
- // Asana task reference settings
640
- allowCustomIssuePrefix: true,
641
- allowEmptyIssuePrefix: true,
642
- issuePrefixes: [
643
- { value: 'asana', name: 'asana: Link to Asana task' },
644
- { value: 'closes', name: 'closes: Close an issue' },
645
- { value: 'fixes', name: 'fixes: Fix an issue' },
646
- ],
647
- customIssuePrefixAlign: 'top',` : `
648
- allowCustomIssuePrefix: false,
649
- allowEmptyIssuePrefix: true,`;
650
- return `// @ts-check
651
-
652
- /** @type {import('cz-git').UserConfig} */
653
- module.exports = {
596
+ ],` : "";
597
+ const asanaRule = asanaBaseUrl ? `
598
+ // Asana task link (warning only - won't block commits)
599
+ 'asana-task-link': [1, 'always'],` : "";
600
+ return `/** @type {import('cz-git').UserConfig} */
601
+ export default {
654
602
  extends: ['@commitlint/config-conventional'],
603
+ // Parser preset to support emoji prefixes in commit messages
604
+ parserPreset: {
605
+ parserOpts: {
606
+ headerPattern:
607
+ /^(?:(?:\\p{Emoji_Presentation}|\\p{Emoji}\\uFE0F?)|:[a-z_]+:)?\\s*(\\w+)(?:\\(([^)]*)\\))?:\\s*(.+)$/u,
608
+ headerCorrespondence: ['type', 'scope', 'subject'],
609
+ },
610
+ },
611
+ // cz-git prompt configuration (read directly from commitlint config)
655
612
  prompt: {
656
613
  alias: {
657
614
  fd: 'docs: fix typos',
@@ -683,61 +640,87 @@ module.exports = {
683
640
  { value: 'revert', name: 'revert: \u23EA Reverting changes', emoji: ':rewind:' },
684
641
  ],
685
642
  useEmoji: true,
686
- emojiAlign: 'center',
643
+ emojiAlign: 'left',
687
644
  useAI: false,
688
- aiNumber: 1,
689
- themeColorCode: '',
690
645
  scopes: [],
691
646
  allowCustomScopes: true,
692
647
  allowEmptyScopes: true,
693
- customScopesAlign: 'bottom',
694
- customScopesAlias: 'custom',
695
- emptyScopesAlias: 'empty',
696
- upperCaseSubject: false,
697
- markBreakingChangeMode: false,
698
648
  allowBreakingChanges: ['feat', 'fix'],
699
- breaklineNumber: 100,
700
- breaklineChar: '|',
701
- skipQuestions: [],${asanaSection}
702
- confirmColorize: true,
703
- minSubjectLength: 0,
704
- defaultBody: '',
705
- defaultIssues: '',
706
- defaultScope: '',
707
- defaultSubject: '',
649
+ breaklineNumber: 100,${asanaIssueSection}
650
+ allowEmptyIssuePrefix: true,
708
651
  },
652
+ rules: {
653
+ // Type must be one of the conventional types
654
+ 'type-enum': [
655
+ 2,
656
+ 'always',
657
+ [
658
+ 'feat',
659
+ 'fix',
660
+ 'docs',
661
+ 'style',
662
+ 'refactor',
663
+ 'perf',
664
+ 'test',
665
+ 'build',
666
+ 'ci',
667
+ 'chore',
668
+ 'revert',
669
+ ],
670
+ ],
671
+ // Subject should not be empty
672
+ 'subject-empty': [2, 'never'],
673
+ // Type should not be empty
674
+ 'type-empty': [2, 'never'],
675
+ // Subject should be lowercase
676
+ 'subject-case': [2, 'always', 'lower-case'],
677
+ // Header max length
678
+ 'header-max-length': [2, 'always', 100],${asanaRule}
679
+ },${asanaPluginSection}
709
680
  };
710
681
  `;
711
682
  }
712
- async function generateCzGit(targetDir, asanaBaseUrl) {
683
+ async function generateCommitlint(targetDir, asanaBaseUrl) {
713
684
  const result = {
714
685
  created: [],
715
686
  modified: [],
716
687
  skipped: [],
717
688
  backedUp: []
718
689
  };
719
- const configPath = join6(targetDir, ".czrc");
690
+ const configPath = join5(targetDir, "commitlint.config.js");
720
691
  const writeResult = await writeFileSafe(
721
692
  configPath,
722
- JSON.stringify({ path: "node_modules/cz-git" }, null, 2) + "\n",
693
+ getCommitlintConfig(asanaBaseUrl),
723
694
  { backup: true }
724
695
  );
725
696
  if (writeResult.created) {
726
- result.created.push(".czrc");
697
+ result.created.push("commitlint.config.js");
727
698
  if (writeResult.backedUp) {
728
699
  result.backedUp.push(writeResult.backedUp);
729
700
  }
730
701
  }
731
- const czConfigPath = join6(targetDir, "cz.config.js");
732
- const czConfigResult = await writeFileSafe(
733
- czConfigPath,
734
- getCzGitConfig(asanaBaseUrl),
702
+ return result;
703
+ }
704
+
705
+ // src/generators/cz-git.ts
706
+ import { join as join6 } from "path";
707
+ async function generateCzGit(targetDir, _asanaBaseUrl) {
708
+ const result = {
709
+ created: [],
710
+ modified: [],
711
+ skipped: [],
712
+ backedUp: []
713
+ };
714
+ const configPath = join6(targetDir, ".czrc");
715
+ const writeResult = await writeFileSafe(
716
+ configPath,
717
+ JSON.stringify({ path: "node_modules/cz-git" }, null, 2) + "\n",
735
718
  { backup: true }
736
719
  );
737
- if (czConfigResult.created) {
738
- result.created.push("cz.config.js");
739
- if (czConfigResult.backedUp) {
740
- result.backedUp.push(czConfigResult.backedUp);
720
+ if (writeResult.created) {
721
+ result.created.push(".czrc");
722
+ if (writeResult.backedUp) {
723
+ result.backedUp.push(writeResult.backedUp);
741
724
  }
742
725
  }
743
726
  return result;
@@ -811,23 +794,42 @@ function addPackageJsonConfig(pkg, key, config, overwrite = false) {
811
794
  var RAFTSTACK_PACKAGES = [
812
795
  // Commit tooling
813
796
  "@commitlint/cli",
797
+ // ^20.3.0
814
798
  "@commitlint/config-conventional",
799
+ // ^20.3.0
815
800
  "cz-git",
801
+ // ^1.12.0
816
802
  "czg",
803
+ // ^1.12.0
817
804
  "husky",
805
+ // ^9.1.7
818
806
  "lint-staged",
807
+ // ^16.2.0
819
808
  "validate-branch-name",
809
+ // ^1.3.2
820
810
  // Linting & formatting
821
811
  "eslint",
812
+ // ^9.39.0
822
813
  "@eslint/js",
814
+ // ^9.39.0
823
815
  "typescript-eslint",
816
+ // ^8.39.0
824
817
  "eslint-config-prettier",
818
+ // ^10.1.0
825
819
  "prettier",
820
+ // ^3.8.0
826
821
  "globals"
822
+ // ^17.0.0
827
823
  ];
828
824
  var REACT_ESLINT_PACKAGES = [
829
825
  "eslint-plugin-react",
826
+ // ^7.37.0
830
827
  "eslint-plugin-react-hooks"
828
+ // ^5.2.0
829
+ ];
830
+ var NEXTJS_ESLINT_PACKAGES = [
831
+ "eslint-config-next"
832
+ // ^16.1.0
831
833
  ];
832
834
  function isPnpmWorkspace(targetDir) {
833
835
  return existsSync4(join7(targetDir, "pnpm-workspace.yaml"));
@@ -1533,6 +1535,8 @@ build
1533
1535
  *.lock
1534
1536
  pnpm-lock.yaml
1535
1537
  coverage
1538
+ # Generated files
1539
+ **/routeTree.gen.ts
1536
1540
  `;
1537
1541
  }
1538
1542
  function hasPrettierConfig(targetDir) {
@@ -1663,9 +1667,37 @@ async function hasReact(targetDir) {
1663
1667
  }
1664
1668
  return false;
1665
1669
  }
1666
- function generateTsConfig(hasReactDep) {
1667
- if (hasReactDep) {
1668
- return `import eslint from "@eslint/js";
1670
+ async function hasNextJs(targetDir) {
1671
+ try {
1672
+ const pkgPath = join16(targetDir, "package.json");
1673
+ if (existsSync6(pkgPath)) {
1674
+ const content = await readFile4(pkgPath, "utf-8");
1675
+ const pkg = JSON.parse(content);
1676
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1677
+ return "next" in deps;
1678
+ }
1679
+ } catch {
1680
+ }
1681
+ return false;
1682
+ }
1683
+ function generateNextJsConfig() {
1684
+ return `import { defineConfig, globalIgnores } from "eslint/config";
1685
+ import nextVitals from "eslint-config-next/core-web-vitals";
1686
+ import nextTs from "eslint-config-next/typescript";
1687
+ import prettier from "eslint-config-prettier";
1688
+
1689
+ const eslintConfig = defineConfig([
1690
+ ...nextVitals,
1691
+ ...nextTs,
1692
+ prettier,
1693
+ globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
1694
+ ]);
1695
+
1696
+ export default eslintConfig;
1697
+ `;
1698
+ }
1699
+ function generateReactTsConfig() {
1700
+ return `import eslint from "@eslint/js";
1669
1701
  import tseslint from "typescript-eslint";
1670
1702
  import eslintConfigPrettier from "eslint-config-prettier";
1671
1703
  import reactPlugin from "eslint-plugin-react";
@@ -1721,7 +1753,7 @@ export default tseslint.config(
1721
1753
  },
1722
1754
  },
1723
1755
  {
1724
- // CommonJS config files (cz.config.js, commitlint.config.js, etc.)
1756
+ // CommonJS config files (commitlint.config.js, etc.)
1725
1757
  files: ["*.config.js", "*.config.cjs"],
1726
1758
  languageOptions: {
1727
1759
  globals: {
@@ -1735,7 +1767,8 @@ export default tseslint.config(
1735
1767
  }
1736
1768
  );
1737
1769
  `;
1738
- }
1770
+ }
1771
+ function generateTsConfig() {
1739
1772
  return `import eslint from "@eslint/js";
1740
1773
  import tseslint from "typescript-eslint";
1741
1774
  import eslintConfigPrettier from "eslint-config-prettier";
@@ -1769,7 +1802,7 @@ export default tseslint.config(
1769
1802
  },
1770
1803
  },
1771
1804
  {
1772
- // CommonJS config files (cz.config.js, commitlint.config.js, etc.)
1805
+ // CommonJS config files (commitlint.config.js, etc.)
1773
1806
  files: ["*.config.js", "*.config.cjs"],
1774
1807
  languageOptions: {
1775
1808
  globals: {
@@ -1784,9 +1817,8 @@ export default tseslint.config(
1784
1817
  );
1785
1818
  `;
1786
1819
  }
1787
- function generateJsConfig(hasReactDep) {
1788
- if (hasReactDep) {
1789
- return `import eslint from "@eslint/js";
1820
+ function generateReactJsConfig() {
1821
+ return `import eslint from "@eslint/js";
1790
1822
  import eslintConfigPrettier from "eslint-config-prettier";
1791
1823
  import reactPlugin from "eslint-plugin-react";
1792
1824
  import reactHooksPlugin from "eslint-plugin-react-hooks";
@@ -1832,7 +1864,7 @@ export default [
1832
1864
  },
1833
1865
  },
1834
1866
  {
1835
- // CommonJS config files (cz.config.js, commitlint.config.js, etc.)
1867
+ // CommonJS config files (commitlint.config.js, etc.)
1836
1868
  files: ["*.config.js", "*.config.cjs"],
1837
1869
  languageOptions: {
1838
1870
  globals: {
@@ -1846,7 +1878,8 @@ export default [
1846
1878
  },
1847
1879
  ];
1848
1880
  `;
1849
- }
1881
+ }
1882
+ function generateJsConfig() {
1850
1883
  return `import eslint from "@eslint/js";
1851
1884
  import eslintConfigPrettier from "eslint-config-prettier";
1852
1885
  import globals from "globals";
@@ -1870,7 +1903,7 @@ export default [
1870
1903
  },
1871
1904
  },
1872
1905
  {
1873
- // CommonJS config files (cz.config.js, commitlint.config.js, etc.)
1906
+ // CommonJS config files (commitlint.config.js, etc.)
1874
1907
  files: ["*.config.js", "*.config.cjs"],
1875
1908
  languageOptions: {
1876
1909
  globals: {
@@ -1888,6 +1921,9 @@ export default [
1888
1921
  async function detectReact(targetDir) {
1889
1922
  return hasReact(targetDir);
1890
1923
  }
1924
+ async function detectNextJs(targetDir) {
1925
+ return hasNextJs(targetDir);
1926
+ }
1891
1927
  async function generateEslint(targetDir, usesTypeScript, force = false) {
1892
1928
  const result = {
1893
1929
  created: [],
@@ -1896,17 +1932,29 @@ async function generateEslint(targetDir, usesTypeScript, force = false) {
1896
1932
  backedUp: []
1897
1933
  };
1898
1934
  if (!force && await hasEslint(targetDir)) {
1899
- result.skipped.push("eslint.config.js (ESLint already configured)");
1935
+ result.skipped.push("eslint.config.mjs (ESLint already configured)");
1900
1936
  return result;
1901
1937
  }
1938
+ const usesNextJs = await hasNextJs(targetDir);
1902
1939
  const usesReact = await hasReact(targetDir);
1903
- const config = usesTypeScript ? generateTsConfig(usesReact) : generateJsConfig(usesReact);
1904
- const configPath = join16(targetDir, "eslint.config.js");
1940
+ let config;
1941
+ if (usesNextJs && usesTypeScript) {
1942
+ config = generateNextJsConfig();
1943
+ } else if (usesReact && usesTypeScript) {
1944
+ config = generateReactTsConfig();
1945
+ } else if (usesTypeScript) {
1946
+ config = generateTsConfig();
1947
+ } else if (usesReact) {
1948
+ config = generateReactJsConfig();
1949
+ } else {
1950
+ config = generateJsConfig();
1951
+ }
1952
+ const configPath = join16(targetDir, "eslint.config.mjs");
1905
1953
  const writeResult = await writeFileSafe(configPath, config);
1906
1954
  if (writeResult.backedUp) {
1907
- result.backedUp.push("eslint.config.js");
1955
+ result.backedUp.push("eslint.config.mjs");
1908
1956
  }
1909
- result.created.push("eslint.config.js");
1957
+ result.created.push("eslint.config.mjs");
1910
1958
  return result;
1911
1959
  }
1912
1960
 
@@ -2052,12 +2100,319 @@ ${pm.run} test
2052
2100
  return result;
2053
2101
  }
2054
2102
 
2103
+ // src/generators/shared-configs.ts
2104
+ import { join as join18 } from "path";
2105
+ function getEslintConfigPackageJson() {
2106
+ return JSON.stringify(
2107
+ {
2108
+ name: "@workspace/eslint-config",
2109
+ version: "0.0.0",
2110
+ type: "module",
2111
+ private: true,
2112
+ exports: {
2113
+ "./base": "./base.js",
2114
+ "./next-js": "./next.js",
2115
+ "./react-internal": "./react-internal.js",
2116
+ "./vite": "./vite.js"
2117
+ },
2118
+ devDependencies: {
2119
+ "@eslint/js": "^9.39.0",
2120
+ "@next/eslint-plugin-next": "^16.1.0",
2121
+ eslint: "^9.39.0",
2122
+ "eslint-config-prettier": "^10.1.0",
2123
+ "eslint-plugin-only-warn": "^1.1.0",
2124
+ "eslint-plugin-react": "^7.37.0",
2125
+ "eslint-plugin-react-hooks": "^5.2.0",
2126
+ "eslint-plugin-turbo": "^2.6.0",
2127
+ globals: "^17.0.0",
2128
+ "typescript-eslint": "^8.39.0"
2129
+ }
2130
+ },
2131
+ null,
2132
+ 2
2133
+ ) + "\n";
2134
+ }
2135
+ function getBaseEslintConfig() {
2136
+ return `import js from "@eslint/js";
2137
+ import eslintConfigPrettier from "eslint-config-prettier";
2138
+ import onlyWarn from "eslint-plugin-only-warn";
2139
+ import turboPlugin from "eslint-plugin-turbo";
2140
+ import tseslint from "typescript-eslint";
2141
+
2142
+ /**
2143
+ * Base ESLint configuration for all packages
2144
+ * Includes TypeScript, Prettier, and Turborepo rules
2145
+ */
2146
+ export const config = [
2147
+ js.configs.recommended,
2148
+ eslintConfigPrettier,
2149
+ ...tseslint.configs.recommended,
2150
+ {
2151
+ plugins: { turbo: turboPlugin },
2152
+ rules: { "turbo/no-undeclared-env-vars": "warn" },
2153
+ },
2154
+ { plugins: { onlyWarn } },
2155
+ { ignores: ["dist/**"] },
2156
+ ];
2157
+ `;
2158
+ }
2159
+ function getNextJsEslintConfig() {
2160
+ return `import { defineConfig, globalIgnores } from "eslint/config";
2161
+ import nextVitals from "eslint-config-next/core-web-vitals";
2162
+ import nextTs from "eslint-config-next/typescript";
2163
+ import prettier from "eslint-config-prettier";
2164
+ import { config as baseConfig } from "./base.js";
2165
+
2166
+ /**
2167
+ * ESLint configuration for Next.js applications
2168
+ * Extends base config with Next.js specific rules
2169
+ */
2170
+ export const nextJsConfig = defineConfig([
2171
+ ...baseConfig,
2172
+ ...nextVitals,
2173
+ ...nextTs,
2174
+ prettier,
2175
+ globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
2176
+ ]);
2177
+
2178
+ export default nextJsConfig;
2179
+ `;
2180
+ }
2181
+ function getReactInternalEslintConfig() {
2182
+ return `import eslintConfigPrettier from "eslint-config-prettier";
2183
+ import reactPlugin from "eslint-plugin-react";
2184
+ import reactHooksPlugin from "eslint-plugin-react-hooks";
2185
+ import globals from "globals";
2186
+ import { config as baseConfig } from "./base.js";
2187
+
2188
+ /**
2189
+ * ESLint configuration for internal React libraries/packages
2190
+ * Extends base config with React-specific rules
2191
+ */
2192
+ export const reactInternalConfig = [
2193
+ ...baseConfig,
2194
+ eslintConfigPrettier,
2195
+ {
2196
+ languageOptions: {
2197
+ parserOptions: {
2198
+ ecmaFeatures: { jsx: true },
2199
+ },
2200
+ globals: {
2201
+ ...globals.browser,
2202
+ },
2203
+ },
2204
+ plugins: {
2205
+ react: reactPlugin,
2206
+ "react-hooks": reactHooksPlugin,
2207
+ },
2208
+ rules: {
2209
+ "react/react-in-jsx-scope": "off",
2210
+ "react/prop-types": "off",
2211
+ "react-hooks/rules-of-hooks": "error",
2212
+ "react-hooks/exhaustive-deps": "warn",
2213
+ },
2214
+ settings: {
2215
+ react: { version: "detect" },
2216
+ },
2217
+ },
2218
+ { ignores: ["dist/**", "node_modules/**"] },
2219
+ ];
2220
+
2221
+ export default reactInternalConfig;
2222
+ `;
2223
+ }
2224
+ function getViteEslintConfig() {
2225
+ return `import eslintConfigPrettier from "eslint-config-prettier";
2226
+ import reactPlugin from "eslint-plugin-react";
2227
+ import reactHooksPlugin from "eslint-plugin-react-hooks";
2228
+ import globals from "globals";
2229
+ import { config as baseConfig } from "./base.js";
2230
+
2231
+ /**
2232
+ * ESLint configuration for Vite-based applications
2233
+ * Extends base config with React and browser globals
2234
+ */
2235
+ export const viteConfig = [
2236
+ ...baseConfig,
2237
+ eslintConfigPrettier,
2238
+ {
2239
+ languageOptions: {
2240
+ parserOptions: {
2241
+ ecmaFeatures: { jsx: true },
2242
+ },
2243
+ globals: {
2244
+ ...globals.browser,
2245
+ ...globals.node,
2246
+ },
2247
+ },
2248
+ plugins: {
2249
+ react: reactPlugin,
2250
+ "react-hooks": reactHooksPlugin,
2251
+ },
2252
+ rules: {
2253
+ "react/react-in-jsx-scope": "off",
2254
+ "react/prop-types": "off",
2255
+ "react-hooks/rules-of-hooks": "error",
2256
+ "react-hooks/exhaustive-deps": "warn",
2257
+ },
2258
+ settings: {
2259
+ react: { version: "detect" },
2260
+ },
2261
+ },
2262
+ { ignores: ["dist/**", "node_modules/**"] },
2263
+ ];
2264
+
2265
+ export default viteConfig;
2266
+ `;
2267
+ }
2268
+ function getTsConfigPackageJson() {
2269
+ return JSON.stringify(
2270
+ {
2271
+ name: "@workspace/typescript-config",
2272
+ version: "0.0.0",
2273
+ private: true
2274
+ },
2275
+ null,
2276
+ 2
2277
+ ) + "\n";
2278
+ }
2279
+ function getBaseTsConfig() {
2280
+ return JSON.stringify(
2281
+ {
2282
+ $schema: "https://json.schemastore.org/tsconfig",
2283
+ display: "Default",
2284
+ compilerOptions: {
2285
+ declaration: true,
2286
+ declarationMap: true,
2287
+ esModuleInterop: true,
2288
+ incremental: false,
2289
+ isolatedModules: true,
2290
+ lib: ["es2022", "DOM", "DOM.Iterable"],
2291
+ module: "NodeNext",
2292
+ moduleDetection: "force",
2293
+ moduleResolution: "NodeNext",
2294
+ noUncheckedIndexedAccess: true,
2295
+ resolveJsonModule: true,
2296
+ skipLibCheck: true,
2297
+ strict: true,
2298
+ target: "ES2022"
2299
+ }
2300
+ },
2301
+ null,
2302
+ 2
2303
+ ) + "\n";
2304
+ }
2305
+ function getNextJsTsConfig() {
2306
+ return JSON.stringify(
2307
+ {
2308
+ $schema: "https://json.schemastore.org/tsconfig",
2309
+ display: "Next.js",
2310
+ extends: "./base.json",
2311
+ compilerOptions: {
2312
+ plugins: [{ name: "next" }],
2313
+ module: "ESNext",
2314
+ moduleResolution: "Bundler",
2315
+ allowJs: true,
2316
+ jsx: "preserve",
2317
+ noEmit: true
2318
+ }
2319
+ },
2320
+ null,
2321
+ 2
2322
+ ) + "\n";
2323
+ }
2324
+ function getReactLibraryTsConfig() {
2325
+ return JSON.stringify(
2326
+ {
2327
+ $schema: "https://json.schemastore.org/tsconfig",
2328
+ display: "React Library",
2329
+ extends: "./base.json",
2330
+ compilerOptions: {
2331
+ jsx: "react-jsx",
2332
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
2333
+ module: "ESNext",
2334
+ moduleResolution: "Bundler"
2335
+ }
2336
+ },
2337
+ null,
2338
+ 2
2339
+ ) + "\n";
2340
+ }
2341
+ function getNodeLibraryTsConfig() {
2342
+ return JSON.stringify(
2343
+ {
2344
+ $schema: "https://json.schemastore.org/tsconfig",
2345
+ display: "Node Library",
2346
+ extends: "./base.json",
2347
+ compilerOptions: {
2348
+ lib: ["ES2022"],
2349
+ module: "NodeNext",
2350
+ moduleResolution: "NodeNext"
2351
+ }
2352
+ },
2353
+ null,
2354
+ 2
2355
+ ) + "\n";
2356
+ }
2357
+ function isMonorepo(projectType) {
2358
+ return projectType === "turbo" || projectType === "nx" || projectType === "pnpm-workspace";
2359
+ }
2360
+ async function generateSharedConfigs(targetDir, projectType) {
2361
+ const result = {
2362
+ created: [],
2363
+ modified: [],
2364
+ skipped: [],
2365
+ backedUp: []
2366
+ };
2367
+ if (!isMonorepo(projectType)) {
2368
+ return result;
2369
+ }
2370
+ const packagesDir = join18(targetDir, "packages");
2371
+ const eslintConfigDir = join18(packagesDir, "eslint-config");
2372
+ await ensureDir(eslintConfigDir);
2373
+ const eslintFiles = [
2374
+ { path: join18(eslintConfigDir, "package.json"), content: getEslintConfigPackageJson(), name: "packages/eslint-config/package.json" },
2375
+ { path: join18(eslintConfigDir, "base.js"), content: getBaseEslintConfig(), name: "packages/eslint-config/base.js" },
2376
+ { path: join18(eslintConfigDir, "next.js"), content: getNextJsEslintConfig(), name: "packages/eslint-config/next.js" },
2377
+ { path: join18(eslintConfigDir, "react-internal.js"), content: getReactInternalEslintConfig(), name: "packages/eslint-config/react-internal.js" },
2378
+ { path: join18(eslintConfigDir, "vite.js"), content: getViteEslintConfig(), name: "packages/eslint-config/vite.js" }
2379
+ ];
2380
+ for (const file of eslintFiles) {
2381
+ const writeResult = await writeFileSafe(file.path, file.content, { backup: true });
2382
+ if (writeResult.created) {
2383
+ result.created.push(file.name);
2384
+ if (writeResult.backedUp) {
2385
+ result.backedUp.push(file.name);
2386
+ }
2387
+ }
2388
+ }
2389
+ const tsConfigDir = join18(packagesDir, "typescript-config");
2390
+ await ensureDir(tsConfigDir);
2391
+ const tsFiles = [
2392
+ { path: join18(tsConfigDir, "package.json"), content: getTsConfigPackageJson(), name: "packages/typescript-config/package.json" },
2393
+ { path: join18(tsConfigDir, "base.json"), content: getBaseTsConfig(), name: "packages/typescript-config/base.json" },
2394
+ { path: join18(tsConfigDir, "nextjs.json"), content: getNextJsTsConfig(), name: "packages/typescript-config/nextjs.json" },
2395
+ { path: join18(tsConfigDir, "react-library.json"), content: getReactLibraryTsConfig(), name: "packages/typescript-config/react-library.json" },
2396
+ { path: join18(tsConfigDir, "node-library.json"), content: getNodeLibraryTsConfig(), name: "packages/typescript-config/node-library.json" }
2397
+ ];
2398
+ for (const file of tsFiles) {
2399
+ const writeResult = await writeFileSafe(file.path, file.content, { backup: true });
2400
+ if (writeResult.created) {
2401
+ result.created.push(file.name);
2402
+ if (writeResult.backedUp) {
2403
+ result.backedUp.push(file.name);
2404
+ }
2405
+ }
2406
+ }
2407
+ return result;
2408
+ }
2409
+
2055
2410
  // src/utils/git.ts
2056
2411
  import { execa } from "execa";
2057
2412
  import { existsSync as existsSync7 } from "fs";
2058
- import { join as join18 } from "path";
2413
+ import { join as join19 } from "path";
2059
2414
  async function isGitRepo(targetDir = process.cwd()) {
2060
- if (existsSync7(join18(targetDir, ".git"))) {
2415
+ if (existsSync7(join19(targetDir, ".git"))) {
2061
2416
  return true;
2062
2417
  }
2063
2418
  try {
@@ -2162,8 +2517,14 @@ async function runInit(targetDir = process.cwd()) {
2162
2517
  return;
2163
2518
  }
2164
2519
  const usesReact = await detectReact(targetDir);
2520
+ const usesNextJs = await detectNextJs(targetDir);
2165
2521
  const installSpinner = p2.spinner();
2166
- const packagesToInstall = usesReact ? [...RAFTSTACK_PACKAGES, ...REACT_ESLINT_PACKAGES] : RAFTSTACK_PACKAGES;
2522
+ let packagesToInstall = [...RAFTSTACK_PACKAGES];
2523
+ if (usesNextJs) {
2524
+ packagesToInstall = [...packagesToInstall, ...NEXTJS_ESLINT_PACKAGES];
2525
+ } else if (usesReact) {
2526
+ packagesToInstall = [...packagesToInstall, ...REACT_ESLINT_PACKAGES];
2527
+ }
2167
2528
  installSpinner.start("Installing dependencies...");
2168
2529
  const installResult = await installPackages(
2169
2530
  config.packageManager,
@@ -2199,6 +2560,9 @@ async function runInit(targetDir = process.cwd()) {
2199
2560
  results.push(await generateBranchValidation(targetDir));
2200
2561
  results.push(await generateEslint(targetDir, config.usesTypeScript, false));
2201
2562
  results.push(await generatePrettier(targetDir));
2563
+ if (isMonorepo(config.projectType)) {
2564
+ results.push(await generateSharedConfigs(targetDir, config.projectType));
2565
+ }
2202
2566
  results.push(await generatePRTemplate(targetDir, !!config.asanaBaseUrl));
2203
2567
  results.push(
2204
2568
  await generateGitHubWorkflows(
@@ -2543,41 +2907,380 @@ async function runSetupProtection(targetDir = process.cwd()) {
2543
2907
  }
2544
2908
 
2545
2909
  // src/commands/metrics.ts
2546
- import { execa as execa3 } from "execa";
2910
+ import { execa as execa4 } from "execa";
2547
2911
  import * as p4 from "@clack/prompts";
2548
2912
  import pc4 from "picocolors";
2549
- async function getRecentCommits(targetDir, days) {
2913
+
2914
+ // src/utils/code-analyzer.ts
2915
+ import { execa as execa3 } from "execa";
2916
+ import { readFileSync as readFileSync2 } from "fs";
2917
+ var THRESHOLDS = {
2918
+ "file-length": 300,
2919
+ "function-length": 30,
2920
+ "max-params": 3,
2921
+ "cyclomatic-complexity": 10
2922
+ };
2923
+ async function findSourceFiles(targetDir) {
2550
2924
  try {
2551
2925
  const { stdout } = await execa3(
2552
2926
  "git",
2553
- ["log", `--since=${days} days ago`, "--oneline", "--no-merges"],
2927
+ ["ls-files", "*.ts", "*.tsx", "*.js", "*.jsx"],
2554
2928
  { cwd: targetDir }
2555
2929
  );
2556
- return stdout.trim().split("\n").filter(Boolean);
2930
+ return stdout.trim().split("\n").filter(Boolean).filter(
2931
+ (f) => !f.includes("node_modules") && !f.includes("dist/") && !f.includes("build/") && !f.includes(".min.") && !f.endsWith(".d.ts")
2932
+ );
2557
2933
  } catch {
2558
2934
  return [];
2559
2935
  }
2560
2936
  }
2561
- async function getCommitMessages(targetDir, days) {
2937
+ function extractFunctions(source, _filePath) {
2938
+ const functions = [];
2939
+ const lines = source.split("\n");
2940
+ const functionPatterns = [
2941
+ // function name(params) or async function name(params)
2942
+ /^(\s*)(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
2943
+ // const name = (params) => or const name = async (params) =>
2944
+ /^(\s*)(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
2945
+ // const name = function(params)
2946
+ /^(\s*)(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(([^)]*)\)/,
2947
+ // class method: name(params) { or async name(params) {
2948
+ /^(\s*)(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/
2949
+ ];
2950
+ for (let i = 0; i < lines.length; i++) {
2951
+ const line = lines[i];
2952
+ for (const pattern of functionPatterns) {
2953
+ const match = line.match(pattern);
2954
+ if (match) {
2955
+ const [, indent, name, params] = match;
2956
+ if (name === "constructor" || name === "if" || name === "for" || name === "while" || name === "switch" || name === "catch") {
2957
+ continue;
2958
+ }
2959
+ const paramCount = countParameters(params);
2960
+ const endLine = findFunctionEnd(lines, i, indent.length);
2961
+ if (endLine > i) {
2962
+ const body = lines.slice(i, endLine + 1).join("\n");
2963
+ functions.push({
2964
+ name,
2965
+ startLine: i + 1,
2966
+ // 1-indexed
2967
+ endLine: endLine + 1,
2968
+ paramCount,
2969
+ body
2970
+ });
2971
+ }
2972
+ break;
2973
+ }
2974
+ }
2975
+ }
2976
+ return functions;
2977
+ }
2978
+ function countParameters(params) {
2979
+ const trimmed = params.trim();
2980
+ if (!trimmed) return 0;
2981
+ let depth = 0;
2982
+ let count = 1;
2983
+ for (const char of trimmed) {
2984
+ if (char === "(" || char === "{" || char === "[" || char === "<") {
2985
+ depth++;
2986
+ } else if (char === ")" || char === "}" || char === "]" || char === ">") {
2987
+ depth--;
2988
+ } else if (char === "," && depth === 0) {
2989
+ count++;
2990
+ }
2991
+ }
2992
+ return count;
2993
+ }
2994
+ function findFunctionEnd(lines, startLine, _baseIndent) {
2995
+ let braceDepth = 0;
2996
+ let foundOpenBrace = false;
2997
+ for (let i = startLine; i < lines.length; i++) {
2998
+ const line = lines[i];
2999
+ for (const char of line) {
3000
+ if (char === "{") {
3001
+ braceDepth++;
3002
+ foundOpenBrace = true;
3003
+ } else if (char === "}") {
3004
+ braceDepth--;
3005
+ if (foundOpenBrace && braceDepth === 0) {
3006
+ return i;
3007
+ }
3008
+ }
3009
+ }
3010
+ }
3011
+ return startLine;
3012
+ }
3013
+ function countComplexity(code) {
3014
+ let complexity = 1;
3015
+ const patterns = [
3016
+ /\bif\s*\(/g,
3017
+ /\belse\s+if\s*\(/g,
3018
+ /\bcase\s+/g,
3019
+ /\bfor\s*\(/g,
3020
+ /\bwhile\s*\(/g,
3021
+ /\bdo\s*\{/g,
3022
+ /\bcatch\s*\(/g,
3023
+ /&&/g,
3024
+ /\|\|/g,
3025
+ /\?\?/g
3026
+ // nullish coalescing
3027
+ ];
3028
+ const ternaryPattern = /\?[^:?]+:/g;
3029
+ for (const pattern of patterns) {
3030
+ const matches = code.match(pattern);
3031
+ if (matches) {
3032
+ complexity += matches.length;
3033
+ }
3034
+ }
3035
+ const ternaryMatches = code.match(ternaryPattern);
3036
+ if (ternaryMatches) {
3037
+ complexity += ternaryMatches.length;
3038
+ }
3039
+ return complexity;
3040
+ }
3041
+ function findMagicNumbers(source, filePath) {
3042
+ const violations = [];
3043
+ const lines = source.split("\n");
3044
+ const allowedNumbers = /* @__PURE__ */ new Set([
3045
+ "0",
3046
+ "1",
3047
+ "-1",
3048
+ "2",
3049
+ "100",
3050
+ "1000",
3051
+ "0.5",
3052
+ "0.1"
3053
+ ]);
3054
+ const numberPattern = /-?\d+\.?\d*/g;
3055
+ for (let i = 0; i < lines.length; i++) {
3056
+ const line = lines[i];
3057
+ const trimmedLine = line.trim();
3058
+ if (/^\s*(?:export\s+)?const\s+\w+\s*[:=]/.test(line)) {
3059
+ continue;
3060
+ }
3061
+ if (trimmedLine.startsWith("import ")) {
3062
+ continue;
3063
+ }
3064
+ if (trimmedLine.startsWith("//") || trimmedLine.startsWith("*")) {
3065
+ continue;
3066
+ }
3067
+ const cleanedLine = line.replace(/\[\d+\]/g, "").replace(/\.length\s*[<>=]+\s*\d+/g, "").replace(/:\s*number/g, "").replace(/[<>=]+\s*0\b/g, "").replace(/\+\+|--/g, "");
3068
+ const matches = cleanedLine.match(numberPattern);
3069
+ if (matches) {
3070
+ for (const match of matches) {
3071
+ if (allowedNumbers.has(match)) continue;
3072
+ if (/0[xXbBoO]/.test(match)) continue;
3073
+ if (line.includes(`"${match}`) || line.includes(`'${match}`)) continue;
3074
+ violations.push({
3075
+ filePath,
3076
+ rule: "magic-number",
3077
+ line: i + 1,
3078
+ message: `Magic number ${match} should be a named constant`
3079
+ });
3080
+ }
3081
+ }
3082
+ }
3083
+ return violations;
3084
+ }
3085
+ function analyzeFile(filePath, source) {
3086
+ const violations = [];
3087
+ const lines = source.split("\n");
3088
+ const lineCount = lines.length;
3089
+ if (lineCount > THRESHOLDS["file-length"]) {
3090
+ violations.push({
3091
+ filePath,
3092
+ rule: "file-length",
3093
+ line: 1,
3094
+ message: `File has ${lineCount} lines (max: ${THRESHOLDS["file-length"]})`
3095
+ });
3096
+ }
3097
+ const functions = extractFunctions(source, filePath);
3098
+ for (const fn of functions) {
3099
+ const fnLineCount = fn.endLine - fn.startLine + 1;
3100
+ if (fnLineCount > THRESHOLDS["function-length"]) {
3101
+ violations.push({
3102
+ filePath,
3103
+ rule: "function-length",
3104
+ line: fn.startLine,
3105
+ message: `Function '${fn.name}' has ${fnLineCount} lines (max: ${THRESHOLDS["function-length"]})`
3106
+ });
3107
+ }
3108
+ if (fn.paramCount > THRESHOLDS["max-params"]) {
3109
+ violations.push({
3110
+ filePath,
3111
+ rule: "max-params",
3112
+ line: fn.startLine,
3113
+ message: `Function '${fn.name}' has ${fn.paramCount} parameters (max: ${THRESHOLDS["max-params"]})`
3114
+ });
3115
+ }
3116
+ const complexity = countComplexity(fn.body);
3117
+ if (complexity > THRESHOLDS["cyclomatic-complexity"]) {
3118
+ violations.push({
3119
+ filePath,
3120
+ rule: "cyclomatic-complexity",
3121
+ line: fn.startLine,
3122
+ message: `Function '${fn.name}' has complexity ${complexity} (max: ${THRESHOLDS["cyclomatic-complexity"]})`
3123
+ });
3124
+ }
3125
+ }
3126
+ const magicViolations = findMagicNumbers(source, filePath);
3127
+ violations.push(...magicViolations);
3128
+ return violations;
3129
+ }
3130
+ async function analyzeCodebase(targetDir) {
3131
+ const files = await findSourceFiles(targetDir);
3132
+ const allViolations = [];
3133
+ let totalLines = 0;
3134
+ const violationsByRule = {
3135
+ "file-length": 0,
3136
+ "function-length": 0,
3137
+ "max-params": 0,
3138
+ "cyclomatic-complexity": 0,
3139
+ "magic-number": 0
3140
+ };
3141
+ const violationsByFile = /* @__PURE__ */ new Map();
3142
+ for (const file of files) {
3143
+ try {
3144
+ const fullPath = `${targetDir}/${file}`;
3145
+ const source = readFileSync2(fullPath, "utf-8");
3146
+ totalLines += source.split("\n").length;
3147
+ const violations = analyzeFile(file, source);
3148
+ for (const v of violations) {
3149
+ violationsByRule[v.rule]++;
3150
+ violationsByFile.set(file, (violationsByFile.get(file) || 0) + 1);
3151
+ }
3152
+ allViolations.push(...violations);
3153
+ } catch {
3154
+ }
3155
+ }
3156
+ const filesWithoutFileLengthViolation = files.length - violationsByRule["file-length"];
3157
+ const fileLengthCompliance = files.length > 0 ? Math.round(filesWithoutFileLengthViolation / files.length * 100) : 100;
3158
+ const calculateRuleCompliance = (violations, fileCount) => {
3159
+ if (fileCount === 0) return 100;
3160
+ const expectedMax = Math.max(1, Math.floor(fileCount / 5));
3161
+ const ratio = Math.min(1, violations / expectedMax);
3162
+ return Math.round((1 - ratio * 0.5) * 100);
3163
+ };
3164
+ const complianceByRule = {
3165
+ "file-length": fileLengthCompliance,
3166
+ "function-length": calculateRuleCompliance(
3167
+ violationsByRule["function-length"],
3168
+ files.length
3169
+ ),
3170
+ "max-params": calculateRuleCompliance(
3171
+ violationsByRule["max-params"],
3172
+ files.length
3173
+ ),
3174
+ "cyclomatic-complexity": calculateRuleCompliance(
3175
+ violationsByRule["cyclomatic-complexity"],
3176
+ files.length
3177
+ ),
3178
+ "magic-number": calculateRuleCompliance(
3179
+ violationsByRule["magic-number"],
3180
+ files.length
3181
+ )
3182
+ };
3183
+ const overallCompliance = Math.round(
3184
+ Object.values(complianceByRule).reduce((a, b) => a + b, 0) / 5
3185
+ );
3186
+ const worstFiles = Array.from(violationsByFile.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([path, count]) => ({ path, count }));
3187
+ return {
3188
+ filesAnalyzed: files.length,
3189
+ totalLines,
3190
+ violations: allViolations,
3191
+ complianceByRule,
3192
+ overallCompliance,
3193
+ worstFiles
3194
+ };
3195
+ }
3196
+
3197
+ // src/commands/metrics.ts
3198
+ var CONVENTIONAL_COMMIT_PATTERN = /^(✨|🐛|📝|💄|♻️|⚡|✅|📦|👷|🔧|⏪)\s+(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9-]+\))?:\s.+/;
3199
+ async function getCommitsWithAuthors(targetDir, days) {
2562
3200
  try {
2563
- const { stdout } = await execa3(
3201
+ const { stdout } = await execa4(
2564
3202
  "git",
2565
3203
  [
2566
3204
  "log",
2567
3205
  `--since=${days} days ago`,
2568
- "--format=%B---COMMIT_SEPARATOR---",
3206
+ "--format=%H|%an|%ae|%s---BODY---%B---END---",
2569
3207
  "--no-merges"
2570
3208
  ],
2571
3209
  { cwd: targetDir }
2572
3210
  );
2573
- return stdout.split("---COMMIT_SEPARATOR---").filter((m) => m.trim());
3211
+ const commits = [];
3212
+ const entries = stdout.split("---END---").filter((e) => e.trim());
3213
+ for (const entry of entries) {
3214
+ const bodyMarker = entry.indexOf("---BODY---");
3215
+ if (bodyMarker === -1) continue;
3216
+ const headerPart = entry.substring(0, bodyMarker).trim();
3217
+ const bodyPart = entry.substring(bodyMarker + 10).trim();
3218
+ const parts = headerPart.split("|");
3219
+ if (parts.length >= 4) {
3220
+ commits.push({
3221
+ hash: parts[0],
3222
+ authorName: parts[1],
3223
+ authorEmail: parts[2],
3224
+ subject: parts.slice(3).join("|"),
3225
+ // Subject might contain |
3226
+ body: bodyPart
3227
+ });
3228
+ }
3229
+ }
3230
+ return commits;
2574
3231
  } catch {
2575
3232
  return [];
2576
3233
  }
2577
3234
  }
3235
+ function isConventionalCommit(subject) {
3236
+ return CONVENTIONAL_COMMIT_PATTERN.test(subject);
3237
+ }
3238
+ function hasTaskLink(commit) {
3239
+ const fullMessage = `${commit.subject}
3240
+ ${commit.body}`;
3241
+ return fullMessage.includes("app.asana.com") || fullMessage.includes("Task:") || fullMessage.includes("task:") || fullMessage.includes("Closes #") || fullMessage.includes("Fixes #") || fullMessage.includes("Resolves #") || /https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/\d+/.test(fullMessage);
3242
+ }
3243
+ function calculateAuthorMetrics(commits) {
3244
+ const authorMap = /* @__PURE__ */ new Map();
3245
+ for (const commit of commits) {
3246
+ const existing = authorMap.get(commit.authorEmail);
3247
+ if (existing) {
3248
+ existing.commits.push(commit);
3249
+ existing.name = commit.authorName;
3250
+ } else {
3251
+ authorMap.set(commit.authorEmail, {
3252
+ name: commit.authorName,
3253
+ email: commit.authorEmail,
3254
+ commits: [commit]
3255
+ });
3256
+ }
3257
+ }
3258
+ const authorMetrics = [];
3259
+ for (const [, author] of authorMap) {
3260
+ const totalCommits = author.commits.length;
3261
+ const withTaskLinks = author.commits.filter(hasTaskLink).length;
3262
+ const conventional = author.commits.filter(
3263
+ (c) => isConventionalCommit(c.subject)
3264
+ ).length;
3265
+ const taskLinkCompliance = totalCommits > 0 ? Math.round(withTaskLinks / totalCommits * 100) : 100;
3266
+ const conventionalCompliance = totalCommits > 0 ? Math.round(conventional / totalCommits * 100) : 100;
3267
+ const overallScore = Math.round(
3268
+ taskLinkCompliance * 0.4 + conventionalCompliance * 0.6
3269
+ );
3270
+ authorMetrics.push({
3271
+ name: author.name,
3272
+ email: author.email,
3273
+ totalCommits,
3274
+ taskLinkCompliance,
3275
+ conventionalCompliance,
3276
+ overallScore
3277
+ });
3278
+ }
3279
+ return authorMetrics.sort((a, b) => b.overallScore - a.overallScore);
3280
+ }
2578
3281
  async function getBranchNames(targetDir) {
2579
3282
  try {
2580
- const { stdout } = await execa3(
3283
+ const { stdout } = await execa4(
2581
3284
  "git",
2582
3285
  ["branch", "-a", "--format=%(refname:short)"],
2583
3286
  { cwd: targetDir }
@@ -2591,22 +3294,11 @@ function isValidBranchName(name) {
2591
3294
  const pattern = /^(main|staging|development|master)$|^(feature|bugfix|hotfix|chore|refactor)\/[a-z0-9-]+$/;
2592
3295
  return pattern.test(name);
2593
3296
  }
2594
- function hasTaskLink(message) {
2595
- return message.includes("app.asana.com") || message.includes("Task:") || message.includes("task:") || message.includes("Closes #") || message.includes("Fixes #");
2596
- }
2597
- async function calculateMetrics(targetDir, days) {
2598
- const [commits, commitMessages, branches] = await Promise.all([
2599
- getRecentCommits(targetDir, days),
2600
- getCommitMessages(targetDir, days),
2601
- getBranchNames(targetDir)
2602
- ]);
2603
- const commitsWithTaskLinks = commitMessages.filter(hasTaskLink).length;
3297
+ async function calculateBranchMetrics(targetDir) {
3298
+ const branches = await getBranchNames(targetDir);
2604
3299
  const validBranches = branches.filter(isValidBranchName);
2605
3300
  const invalidBranches = branches.filter((b) => !isValidBranchName(b));
2606
3301
  return {
2607
- totalCommits: commits.length,
2608
- commitsWithTaskLinks,
2609
- taskLinkCompliance: commits.length > 0 ? Math.round(commitsWithTaskLinks / commits.length * 100) : 100,
2610
3302
  branchNames: branches,
2611
3303
  validBranches: validBranches.length,
2612
3304
  invalidBranches: invalidBranches.length,
@@ -2618,56 +3310,215 @@ function formatCompliance(percentage) {
2618
3310
  if (percentage >= 70) return pc4.yellow(`${percentage}%`);
2619
3311
  return pc4.red(`${percentage}%`);
2620
3312
  }
2621
- async function runMetrics(targetDir) {
2622
- p4.intro(pc4.bgCyan(pc4.black(" RaftStack Metrics ")));
3313
+ function formatLeaderboard(authors, title, limit) {
3314
+ if (authors.length === 0) return "";
3315
+ const lines = [pc4.bold(title)];
3316
+ const displayed = authors.slice(0, limit);
3317
+ displayed.forEach((author, index) => {
3318
+ const score = formatCompliance(author.overallScore);
3319
+ const truncatedEmail = author.email.length > 25 ? author.email.substring(0, 22) + "..." : author.email;
3320
+ lines.push(
3321
+ ` ${index + 1}. ${author.name} (${truncatedEmail}) - ${score} - ${author.totalCommits} commits`
3322
+ );
3323
+ });
3324
+ return lines.join("\n");
3325
+ }
3326
+ function formatCodebaseMetrics(metrics) {
3327
+ const ruleNames = {
3328
+ "file-length": `File length (\u2264300)`,
3329
+ "function-length": `Function length (\u226430)`,
3330
+ "max-params": `Max parameters (\u22643)`,
3331
+ "cyclomatic-complexity": `Cyclomatic complexity`,
3332
+ "magic-number": `Magic numbers`
3333
+ };
3334
+ const lines = [
3335
+ pc4.bold("CODEBASE COMPLIANCE"),
3336
+ ` Files analyzed: ${metrics.filesAnalyzed}`,
3337
+ ` Total lines: ${metrics.totalLines.toLocaleString()}`,
3338
+ "",
3339
+ pc4.bold(" RULE COMPLIANCE")
3340
+ ];
3341
+ for (const rule of Object.keys(ruleNames)) {
3342
+ const compliance = metrics.complianceByRule[rule];
3343
+ const label = ruleNames[rule].padEnd(28);
3344
+ lines.push(` ${label}${formatCompliance(compliance)}`);
3345
+ }
3346
+ lines.push("");
3347
+ lines.push(` ${pc4.bold("OVERALL:")} ${formatCompliance(metrics.overallCompliance)}`);
3348
+ if (metrics.worstFiles.length > 0) {
3349
+ lines.push("");
3350
+ lines.push(pc4.bold(" TOP VIOLATIONS"));
3351
+ for (const file of metrics.worstFiles.slice(0, 5)) {
3352
+ lines.push(` ${file.path} (${file.count} violations)`);
3353
+ const fileViolations = metrics.violations.filter(
3354
+ (v) => v.filePath === file.path
3355
+ );
3356
+ const byRule = /* @__PURE__ */ new Map();
3357
+ for (const v of fileViolations) {
3358
+ byRule.set(v.rule, (byRule.get(v.rule) || 0) + 1);
3359
+ }
3360
+ for (const [rule, count] of byRule) {
3361
+ lines.push(` - ${count}x ${rule}`);
3362
+ }
3363
+ }
3364
+ }
3365
+ return lines.join("\n");
3366
+ }
3367
+ async function runMetrics(targetDir, options = {}) {
3368
+ const { git: gitOnly, code: codeOnly, ci: ciMode, threshold = 70 } = options;
3369
+ const showGit = !codeOnly;
3370
+ const showCode = !gitOnly;
3371
+ const days = options.days || (ciMode ? 30 : null);
3372
+ if (!ciMode) {
3373
+ p4.intro(pc4.bgCyan(pc4.black(" RaftStack Metrics ")));
3374
+ }
2623
3375
  if (!await isGitRepo(targetDir)) {
3376
+ if (ciMode) {
3377
+ console.error("Error: Not a git repository");
3378
+ process.exit(1);
3379
+ }
2624
3380
  p4.cancel("Not a git repository");
2625
3381
  process.exit(1);
2626
3382
  }
2627
- const daysOption = await p4.select({
2628
- message: "Time period to analyze:",
2629
- options: [
2630
- { value: 7, label: "Last 7 days" },
2631
- { value: 14, label: "Last 14 days" },
2632
- { value: 30, label: "Last 30 days" },
2633
- { value: 90, label: "Last 90 days" }
2634
- ]
2635
- });
2636
- if (p4.isCancel(daysOption)) {
2637
- p4.cancel("Operation cancelled");
2638
- process.exit(0);
2639
- }
2640
- const days = daysOption;
2641
- const spinner4 = p4.spinner();
2642
- spinner4.start("Analyzing repository...");
2643
- const metrics = await calculateMetrics(targetDir, days);
2644
- spinner4.stop("Analysis complete");
2645
- p4.note(
2646
- `${pc4.bold("Commits")} (last ${days} days)
2647
- Total: ${metrics.totalCommits}
2648
- With task links: ${metrics.commitsWithTaskLinks}
2649
- Compliance: ${formatCompliance(metrics.taskLinkCompliance)}
3383
+ let selectedDays = days;
3384
+ if (!selectedDays && !ciMode) {
3385
+ const daysOption = await p4.select({
3386
+ message: "Time period to analyze:",
3387
+ options: [
3388
+ { value: 7, label: "Last 7 days" },
3389
+ { value: 14, label: "Last 14 days" },
3390
+ { value: 30, label: "Last 30 days" },
3391
+ { value: 90, label: "Last 90 days" }
3392
+ ]
3393
+ });
3394
+ if (p4.isCancel(daysOption)) {
3395
+ p4.cancel("Operation cancelled");
3396
+ process.exit(0);
3397
+ }
3398
+ selectedDays = daysOption;
3399
+ }
3400
+ const analyzeDays = selectedDays || 30;
3401
+ const spinner4 = ciMode ? null : p4.spinner();
3402
+ spinner4?.start("Analyzing repository...");
3403
+ let overallCompliance = 100;
3404
+ const complianceScores = [];
3405
+ if (showGit) {
3406
+ const [commits, branchMetrics] = await Promise.all([
3407
+ getCommitsWithAuthors(targetDir, analyzeDays),
3408
+ calculateBranchMetrics(targetDir)
3409
+ ]);
3410
+ const authorMetrics = calculateAuthorMetrics(commits);
3411
+ const totalCommits = commits.length;
3412
+ const withTaskLinks = commits.filter(hasTaskLink).length;
3413
+ const conventional = commits.filter(
3414
+ (c) => isConventionalCommit(c.subject)
3415
+ ).length;
3416
+ const taskLinkCompliance = totalCommits > 0 ? Math.round(withTaskLinks / totalCommits * 100) : 100;
3417
+ const conventionalCompliance = totalCommits > 0 ? Math.round(conventional / totalCommits * 100) : 100;
3418
+ complianceScores.push(
3419
+ taskLinkCompliance,
3420
+ conventionalCompliance,
3421
+ branchMetrics.branchCompliance
3422
+ );
3423
+ spinner4?.stop("Git analysis complete");
3424
+ if (ciMode) {
3425
+ console.log("\n=== GIT METRICS ===");
3426
+ console.log(`Commits (last ${analyzeDays} days): ${totalCommits}`);
3427
+ console.log(`Task link compliance: ${taskLinkCompliance}%`);
3428
+ console.log(`Conventional commit compliance: ${conventionalCompliance}%`);
3429
+ console.log(`Branch compliance: ${branchMetrics.branchCompliance}%`);
3430
+ } else {
3431
+ p4.note(
3432
+ `${pc4.bold("Commits")} (last ${analyzeDays} days)
3433
+ Total: ${totalCommits}
3434
+ With task links: ${withTaskLinks} (${formatCompliance(taskLinkCompliance)})
3435
+ Conventional format: ${conventional} (${formatCompliance(conventionalCompliance)})
2650
3436
 
2651
3437
  ${pc4.bold("Branches")}
2652
- Total: ${metrics.branchNames.length}
2653
- Valid naming: ${metrics.validBranches}
2654
- Invalid naming: ${metrics.invalidBranches}
2655
- Compliance: ${formatCompliance(metrics.branchCompliance)}`,
2656
- "Repository Metrics"
2657
- );
2658
- if (metrics.invalidBranches > 0) {
2659
- const invalidBranches = metrics.branchNames.filter(
2660
- (b) => !isValidBranchName(b)
2661
- );
2662
- p4.log.warn(
2663
- `Invalid branch names:
3438
+ Total: ${branchMetrics.branchNames.length}
3439
+ Valid naming: ${branchMetrics.validBranches}
3440
+ Invalid naming: ${branchMetrics.invalidBranches}
3441
+ Compliance: ${formatCompliance(branchMetrics.branchCompliance)}`,
3442
+ "Git Metrics"
3443
+ );
3444
+ if (authorMetrics.length > 0) {
3445
+ const topPerformers = authorMetrics.filter((a) => a.overallScore >= 70);
3446
+ const needsImprovement = authorMetrics.filter((a) => a.overallScore < 70).reverse();
3447
+ let leaderboardText = "";
3448
+ if (topPerformers.length > 0) {
3449
+ leaderboardText += formatLeaderboard(
3450
+ topPerformers,
3451
+ "TOP PERFORMERS",
3452
+ 5
3453
+ );
3454
+ }
3455
+ if (needsImprovement.length > 0) {
3456
+ if (leaderboardText) leaderboardText += "\n\n";
3457
+ leaderboardText += formatLeaderboard(
3458
+ needsImprovement,
3459
+ "NEEDS IMPROVEMENT",
3460
+ 5
3461
+ );
3462
+ }
3463
+ if (leaderboardText) {
3464
+ p4.note(leaderboardText, "Author Leaderboard");
3465
+ }
3466
+ }
3467
+ if (branchMetrics.invalidBranches > 0) {
3468
+ const invalidBranches = branchMetrics.branchNames.filter(
3469
+ (b) => !isValidBranchName(b)
3470
+ );
3471
+ p4.log.warn(
3472
+ `Invalid branch names:
2664
3473
  ${invalidBranches.slice(0, 10).join("\n ")}${invalidBranches.length > 10 ? `
2665
3474
  ... and ${invalidBranches.length - 10} more` : ""}`
3475
+ );
3476
+ }
3477
+ }
3478
+ }
3479
+ if (showCode) {
3480
+ if (!ciMode && showGit) {
3481
+ spinner4?.start("Analyzing codebase...");
3482
+ } else if (!ciMode) {
3483
+ spinner4?.start("Analyzing codebase...");
3484
+ }
3485
+ const codebaseMetrics = await analyzeCodebase(targetDir);
3486
+ complianceScores.push(codebaseMetrics.overallCompliance);
3487
+ spinner4?.stop("Codebase analysis complete");
3488
+ if (ciMode) {
3489
+ console.log("\n=== CODEBASE METRICS ===");
3490
+ console.log(`Files analyzed: ${codebaseMetrics.filesAnalyzed}`);
3491
+ console.log(`Overall compliance: ${codebaseMetrics.overallCompliance}%`);
3492
+ for (const [rule, compliance] of Object.entries(
3493
+ codebaseMetrics.complianceByRule
3494
+ )) {
3495
+ console.log(` ${rule}: ${compliance}%`);
3496
+ }
3497
+ } else {
3498
+ p4.note(formatCodebaseMetrics(codebaseMetrics), "Codebase Analysis");
3499
+ }
3500
+ }
3501
+ if (complianceScores.length > 0) {
3502
+ overallCompliance = Math.round(
3503
+ complianceScores.reduce((a, b) => a + b, 0) / complianceScores.length
2666
3504
  );
2667
3505
  }
2668
- const overallCompliance = Math.round(
2669
- (metrics.taskLinkCompliance + metrics.branchCompliance) / 2
2670
- );
3506
+ if (ciMode) {
3507
+ console.log(`
3508
+ OVERALL COMPLIANCE: ${overallCompliance}%`);
3509
+ console.log(`THRESHOLD: ${threshold}%`);
3510
+ if (overallCompliance < threshold) {
3511
+ console.log(
3512
+ `
3513
+ FAILED: Compliance ${overallCompliance}% is below threshold ${threshold}%`
3514
+ );
3515
+ process.exit(1);
3516
+ } else {
3517
+ console.log(`
3518
+ PASSED: Compliance meets threshold`);
3519
+ process.exit(0);
3520
+ }
3521
+ }
2671
3522
  if (overallCompliance >= 90) {
2672
3523
  p4.outro(pc4.green("\u2713 Excellent compliance! Keep up the good work."));
2673
3524
  } else if (overallCompliance >= 70) {
@@ -2680,7 +3531,7 @@ ${pc4.bold("Branches")}
2680
3531
  // package.json
2681
3532
  var package_default = {
2682
3533
  name: "@raftlabs/raftstack",
2683
- version: "1.7.2",
3534
+ version: "1.8.0",
2684
3535
  description: "CLI tool for setting up Git hooks, commit conventions, and GitHub integration",
2685
3536
  type: "module",
2686
3537
  main: "./dist/index.js",
@@ -2770,8 +3621,20 @@ program.command("init").description("Initialize RaftStack configuration in your
2770
3621
  program.command("setup-protection").description("Configure GitHub branch protection rules via API").action(async () => {
2771
3622
  await runSetupProtection(process.cwd());
2772
3623
  });
2773
- program.command("metrics").description("Analyze repository compliance with RaftStack conventions").action(async () => {
2774
- await runMetrics(process.cwd());
2775
- });
3624
+ program.command("metrics").description("Analyze repository compliance with RaftStack conventions").option("--git", "Only show Git metrics (commits, branches, authors)").option("--code", "Only show codebase compliance metrics").option("--ci", "CI mode: exit 1 if below threshold").option(
3625
+ "--threshold <n>",
3626
+ "Minimum compliance percentage (default: 70)",
3627
+ "70"
3628
+ ).option("--days <n>", "Time period in days (default: 30 in CI mode)").action(
3629
+ async (options) => {
3630
+ await runMetrics(process.cwd(), {
3631
+ git: options.git,
3632
+ code: options.code,
3633
+ ci: options.ci,
3634
+ threshold: options.threshold ? parseInt(options.threshold, 10) : 70,
3635
+ days: options.days ? parseInt(options.days, 10) : void 0
3636
+ });
3637
+ }
3638
+ );
2776
3639
  program.parse();
2777
3640
  //# sourceMappingURL=cli.js.map