@raftlabs/raftstack 1.7.2 → 1.9.1

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/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) {
@@ -1646,14 +1650,65 @@ async function generateClaudeSkills(targetDir, options) {
1646
1650
  return result;
1647
1651
  }
1648
1652
 
1649
- // src/generators/eslint.ts
1653
+ // src/generators/claude-commands.ts
1650
1654
  import { existsSync as existsSync6 } from "fs";
1655
+ import { readdir as readdir2, copyFile as copyFile3 } from "fs/promises";
1656
+ import { join as join16, dirname as dirname3 } from "path";
1657
+ import { fileURLToPath as fileURLToPath2 } from "url";
1658
+ function getPackageCommandsDir() {
1659
+ const currentFilePath = fileURLToPath2(import.meta.url);
1660
+ const packageRoot = join16(dirname3(currentFilePath), "..");
1661
+ return join16(packageRoot, ".claude", "commands");
1662
+ }
1663
+ async function copyDirectory2(srcDir, destDir, result, baseDir) {
1664
+ await ensureDir(destDir);
1665
+ const entries = await readdir2(srcDir, { withFileTypes: true });
1666
+ for (const entry of entries) {
1667
+ const srcPath = join16(srcDir, entry.name);
1668
+ const destPath = join16(destDir, entry.name);
1669
+ const relativePath = destPath.replace(baseDir + "/", "");
1670
+ if (entry.isDirectory()) {
1671
+ await copyDirectory2(srcPath, destPath, result, baseDir);
1672
+ } else {
1673
+ if (existsSync6(destPath)) {
1674
+ const backupPath = await backupFile(destPath);
1675
+ if (backupPath) {
1676
+ result.backedUp.push(relativePath);
1677
+ }
1678
+ }
1679
+ await copyFile3(srcPath, destPath);
1680
+ result.created.push(relativePath);
1681
+ }
1682
+ }
1683
+ }
1684
+ async function generateClaudeCommands(targetDir) {
1685
+ const result = {
1686
+ created: [],
1687
+ modified: [],
1688
+ skipped: [],
1689
+ backedUp: []
1690
+ };
1691
+ const packageCommandsDir = getPackageCommandsDir();
1692
+ const targetCommandsDir = join16(targetDir, ".claude", "commands");
1693
+ if (!existsSync6(packageCommandsDir)) {
1694
+ console.warn(
1695
+ "Warning: Commands directory not found in package. Skipping commands generation."
1696
+ );
1697
+ return result;
1698
+ }
1699
+ await ensureDir(join16(targetDir, ".claude"));
1700
+ await copyDirectory2(packageCommandsDir, targetCommandsDir, result, targetDir);
1701
+ return result;
1702
+ }
1703
+
1704
+ // src/generators/eslint.ts
1705
+ import { existsSync as existsSync7 } from "fs";
1651
1706
  import { readFile as readFile4 } from "fs/promises";
1652
- import { join as join16 } from "path";
1707
+ import { join as join17 } from "path";
1653
1708
  async function hasReact(targetDir) {
1654
1709
  try {
1655
- const pkgPath = join16(targetDir, "package.json");
1656
- if (existsSync6(pkgPath)) {
1710
+ const pkgPath = join17(targetDir, "package.json");
1711
+ if (existsSync7(pkgPath)) {
1657
1712
  const content = await readFile4(pkgPath, "utf-8");
1658
1713
  const pkg = JSON.parse(content);
1659
1714
  const deps = { ...pkg.dependencies, ...pkg.devDependencies };
@@ -1663,9 +1718,37 @@ async function hasReact(targetDir) {
1663
1718
  }
1664
1719
  return false;
1665
1720
  }
1666
- function generateTsConfig(hasReactDep) {
1667
- if (hasReactDep) {
1668
- return `import eslint from "@eslint/js";
1721
+ async function hasNextJs(targetDir) {
1722
+ try {
1723
+ const pkgPath = join17(targetDir, "package.json");
1724
+ if (existsSync7(pkgPath)) {
1725
+ const content = await readFile4(pkgPath, "utf-8");
1726
+ const pkg = JSON.parse(content);
1727
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
1728
+ return "next" in deps;
1729
+ }
1730
+ } catch {
1731
+ }
1732
+ return false;
1733
+ }
1734
+ function generateNextJsConfig() {
1735
+ return `import { defineConfig, globalIgnores } from "eslint/config";
1736
+ import nextVitals from "eslint-config-next/core-web-vitals";
1737
+ import nextTs from "eslint-config-next/typescript";
1738
+ import prettier from "eslint-config-prettier";
1739
+
1740
+ const eslintConfig = defineConfig([
1741
+ ...nextVitals,
1742
+ ...nextTs,
1743
+ prettier,
1744
+ globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
1745
+ ]);
1746
+
1747
+ export default eslintConfig;
1748
+ `;
1749
+ }
1750
+ function generateReactTsConfig() {
1751
+ return `import eslint from "@eslint/js";
1669
1752
  import tseslint from "typescript-eslint";
1670
1753
  import eslintConfigPrettier from "eslint-config-prettier";
1671
1754
  import reactPlugin from "eslint-plugin-react";
@@ -1721,7 +1804,7 @@ export default tseslint.config(
1721
1804
  },
1722
1805
  },
1723
1806
  {
1724
- // CommonJS config files (cz.config.js, commitlint.config.js, etc.)
1807
+ // CommonJS config files (commitlint.config.js, etc.)
1725
1808
  files: ["*.config.js", "*.config.cjs"],
1726
1809
  languageOptions: {
1727
1810
  globals: {
@@ -1735,7 +1818,8 @@ export default tseslint.config(
1735
1818
  }
1736
1819
  );
1737
1820
  `;
1738
- }
1821
+ }
1822
+ function generateTsConfig() {
1739
1823
  return `import eslint from "@eslint/js";
1740
1824
  import tseslint from "typescript-eslint";
1741
1825
  import eslintConfigPrettier from "eslint-config-prettier";
@@ -1769,7 +1853,7 @@ export default tseslint.config(
1769
1853
  },
1770
1854
  },
1771
1855
  {
1772
- // CommonJS config files (cz.config.js, commitlint.config.js, etc.)
1856
+ // CommonJS config files (commitlint.config.js, etc.)
1773
1857
  files: ["*.config.js", "*.config.cjs"],
1774
1858
  languageOptions: {
1775
1859
  globals: {
@@ -1784,9 +1868,8 @@ export default tseslint.config(
1784
1868
  );
1785
1869
  `;
1786
1870
  }
1787
- function generateJsConfig(hasReactDep) {
1788
- if (hasReactDep) {
1789
- return `import eslint from "@eslint/js";
1871
+ function generateReactJsConfig() {
1872
+ return `import eslint from "@eslint/js";
1790
1873
  import eslintConfigPrettier from "eslint-config-prettier";
1791
1874
  import reactPlugin from "eslint-plugin-react";
1792
1875
  import reactHooksPlugin from "eslint-plugin-react-hooks";
@@ -1832,7 +1915,7 @@ export default [
1832
1915
  },
1833
1916
  },
1834
1917
  {
1835
- // CommonJS config files (cz.config.js, commitlint.config.js, etc.)
1918
+ // CommonJS config files (commitlint.config.js, etc.)
1836
1919
  files: ["*.config.js", "*.config.cjs"],
1837
1920
  languageOptions: {
1838
1921
  globals: {
@@ -1846,7 +1929,8 @@ export default [
1846
1929
  },
1847
1930
  ];
1848
1931
  `;
1849
- }
1932
+ }
1933
+ function generateJsConfig() {
1850
1934
  return `import eslint from "@eslint/js";
1851
1935
  import eslintConfigPrettier from "eslint-config-prettier";
1852
1936
  import globals from "globals";
@@ -1870,7 +1954,7 @@ export default [
1870
1954
  },
1871
1955
  },
1872
1956
  {
1873
- // CommonJS config files (cz.config.js, commitlint.config.js, etc.)
1957
+ // CommonJS config files (commitlint.config.js, etc.)
1874
1958
  files: ["*.config.js", "*.config.cjs"],
1875
1959
  languageOptions: {
1876
1960
  globals: {
@@ -1888,6 +1972,9 @@ export default [
1888
1972
  async function detectReact(targetDir) {
1889
1973
  return hasReact(targetDir);
1890
1974
  }
1975
+ async function detectNextJs(targetDir) {
1976
+ return hasNextJs(targetDir);
1977
+ }
1891
1978
  async function generateEslint(targetDir, usesTypeScript, force = false) {
1892
1979
  const result = {
1893
1980
  created: [],
@@ -1896,22 +1983,34 @@ async function generateEslint(targetDir, usesTypeScript, force = false) {
1896
1983
  backedUp: []
1897
1984
  };
1898
1985
  if (!force && await hasEslint(targetDir)) {
1899
- result.skipped.push("eslint.config.js (ESLint already configured)");
1986
+ result.skipped.push("eslint.config.mjs (ESLint already configured)");
1900
1987
  return result;
1901
1988
  }
1989
+ const usesNextJs = await hasNextJs(targetDir);
1902
1990
  const usesReact = await hasReact(targetDir);
1903
- const config = usesTypeScript ? generateTsConfig(usesReact) : generateJsConfig(usesReact);
1904
- const configPath = join16(targetDir, "eslint.config.js");
1991
+ let config;
1992
+ if (usesNextJs && usesTypeScript) {
1993
+ config = generateNextJsConfig();
1994
+ } else if (usesReact && usesTypeScript) {
1995
+ config = generateReactTsConfig();
1996
+ } else if (usesTypeScript) {
1997
+ config = generateTsConfig();
1998
+ } else if (usesReact) {
1999
+ config = generateReactJsConfig();
2000
+ } else {
2001
+ config = generateJsConfig();
2002
+ }
2003
+ const configPath = join17(targetDir, "eslint.config.mjs");
1905
2004
  const writeResult = await writeFileSafe(configPath, config);
1906
2005
  if (writeResult.backedUp) {
1907
- result.backedUp.push("eslint.config.js");
2006
+ result.backedUp.push("eslint.config.mjs");
1908
2007
  }
1909
- result.created.push("eslint.config.js");
2008
+ result.created.push("eslint.config.mjs");
1910
2009
  return result;
1911
2010
  }
1912
2011
 
1913
2012
  // src/generators/quick-reference.ts
1914
- import { join as join17 } from "path";
2013
+ import { join as join18 } from "path";
1915
2014
  async function generateQuickReference(targetDir, pm) {
1916
2015
  const result = {
1917
2016
  created: [],
@@ -1919,7 +2018,7 @@ async function generateQuickReference(targetDir, pm) {
1919
2018
  skipped: [],
1920
2019
  backedUp: []
1921
2020
  };
1922
- const quickRefPath = join17(targetDir, ".github", "QUICK_REFERENCE.md");
2021
+ const quickRefPath = join18(targetDir, ".github", "QUICK_REFERENCE.md");
1923
2022
  const content = `# RaftStack Quick Reference
1924
2023
 
1925
2024
  > One-page guide for the RaftStack Git workflow
@@ -2052,12 +2151,319 @@ ${pm.run} test
2052
2151
  return result;
2053
2152
  }
2054
2153
 
2154
+ // src/generators/shared-configs.ts
2155
+ import { join as join19 } from "path";
2156
+ function getEslintConfigPackageJson() {
2157
+ return JSON.stringify(
2158
+ {
2159
+ name: "@workspace/eslint-config",
2160
+ version: "0.0.0",
2161
+ type: "module",
2162
+ private: true,
2163
+ exports: {
2164
+ "./base": "./base.js",
2165
+ "./next-js": "./next.js",
2166
+ "./react-internal": "./react-internal.js",
2167
+ "./vite": "./vite.js"
2168
+ },
2169
+ devDependencies: {
2170
+ "@eslint/js": "^9.39.0",
2171
+ "@next/eslint-plugin-next": "^16.1.0",
2172
+ eslint: "^9.39.0",
2173
+ "eslint-config-prettier": "^10.1.0",
2174
+ "eslint-plugin-only-warn": "^1.1.0",
2175
+ "eslint-plugin-react": "^7.37.0",
2176
+ "eslint-plugin-react-hooks": "^5.2.0",
2177
+ "eslint-plugin-turbo": "^2.6.0",
2178
+ globals: "^17.0.0",
2179
+ "typescript-eslint": "^8.39.0"
2180
+ }
2181
+ },
2182
+ null,
2183
+ 2
2184
+ ) + "\n";
2185
+ }
2186
+ function getBaseEslintConfig() {
2187
+ return `import js from "@eslint/js";
2188
+ import eslintConfigPrettier from "eslint-config-prettier";
2189
+ import onlyWarn from "eslint-plugin-only-warn";
2190
+ import turboPlugin from "eslint-plugin-turbo";
2191
+ import tseslint from "typescript-eslint";
2192
+
2193
+ /**
2194
+ * Base ESLint configuration for all packages
2195
+ * Includes TypeScript, Prettier, and Turborepo rules
2196
+ */
2197
+ export const config = [
2198
+ js.configs.recommended,
2199
+ eslintConfigPrettier,
2200
+ ...tseslint.configs.recommended,
2201
+ {
2202
+ plugins: { turbo: turboPlugin },
2203
+ rules: { "turbo/no-undeclared-env-vars": "warn" },
2204
+ },
2205
+ { plugins: { onlyWarn } },
2206
+ { ignores: ["dist/**"] },
2207
+ ];
2208
+ `;
2209
+ }
2210
+ function getNextJsEslintConfig() {
2211
+ return `import { defineConfig, globalIgnores } from "eslint/config";
2212
+ import nextVitals from "eslint-config-next/core-web-vitals";
2213
+ import nextTs from "eslint-config-next/typescript";
2214
+ import prettier from "eslint-config-prettier";
2215
+ import { config as baseConfig } from "./base.js";
2216
+
2217
+ /**
2218
+ * ESLint configuration for Next.js applications
2219
+ * Extends base config with Next.js specific rules
2220
+ */
2221
+ export const nextJsConfig = defineConfig([
2222
+ ...baseConfig,
2223
+ ...nextVitals,
2224
+ ...nextTs,
2225
+ prettier,
2226
+ globalIgnores([".next/**", "out/**", "build/**", "next-env.d.ts"]),
2227
+ ]);
2228
+
2229
+ export default nextJsConfig;
2230
+ `;
2231
+ }
2232
+ function getReactInternalEslintConfig() {
2233
+ return `import eslintConfigPrettier from "eslint-config-prettier";
2234
+ import reactPlugin from "eslint-plugin-react";
2235
+ import reactHooksPlugin from "eslint-plugin-react-hooks";
2236
+ import globals from "globals";
2237
+ import { config as baseConfig } from "./base.js";
2238
+
2239
+ /**
2240
+ * ESLint configuration for internal React libraries/packages
2241
+ * Extends base config with React-specific rules
2242
+ */
2243
+ export const reactInternalConfig = [
2244
+ ...baseConfig,
2245
+ eslintConfigPrettier,
2246
+ {
2247
+ languageOptions: {
2248
+ parserOptions: {
2249
+ ecmaFeatures: { jsx: true },
2250
+ },
2251
+ globals: {
2252
+ ...globals.browser,
2253
+ },
2254
+ },
2255
+ plugins: {
2256
+ react: reactPlugin,
2257
+ "react-hooks": reactHooksPlugin,
2258
+ },
2259
+ rules: {
2260
+ "react/react-in-jsx-scope": "off",
2261
+ "react/prop-types": "off",
2262
+ "react-hooks/rules-of-hooks": "error",
2263
+ "react-hooks/exhaustive-deps": "warn",
2264
+ },
2265
+ settings: {
2266
+ react: { version: "detect" },
2267
+ },
2268
+ },
2269
+ { ignores: ["dist/**", "node_modules/**"] },
2270
+ ];
2271
+
2272
+ export default reactInternalConfig;
2273
+ `;
2274
+ }
2275
+ function getViteEslintConfig() {
2276
+ return `import eslintConfigPrettier from "eslint-config-prettier";
2277
+ import reactPlugin from "eslint-plugin-react";
2278
+ import reactHooksPlugin from "eslint-plugin-react-hooks";
2279
+ import globals from "globals";
2280
+ import { config as baseConfig } from "./base.js";
2281
+
2282
+ /**
2283
+ * ESLint configuration for Vite-based applications
2284
+ * Extends base config with React and browser globals
2285
+ */
2286
+ export const viteConfig = [
2287
+ ...baseConfig,
2288
+ eslintConfigPrettier,
2289
+ {
2290
+ languageOptions: {
2291
+ parserOptions: {
2292
+ ecmaFeatures: { jsx: true },
2293
+ },
2294
+ globals: {
2295
+ ...globals.browser,
2296
+ ...globals.node,
2297
+ },
2298
+ },
2299
+ plugins: {
2300
+ react: reactPlugin,
2301
+ "react-hooks": reactHooksPlugin,
2302
+ },
2303
+ rules: {
2304
+ "react/react-in-jsx-scope": "off",
2305
+ "react/prop-types": "off",
2306
+ "react-hooks/rules-of-hooks": "error",
2307
+ "react-hooks/exhaustive-deps": "warn",
2308
+ },
2309
+ settings: {
2310
+ react: { version: "detect" },
2311
+ },
2312
+ },
2313
+ { ignores: ["dist/**", "node_modules/**"] },
2314
+ ];
2315
+
2316
+ export default viteConfig;
2317
+ `;
2318
+ }
2319
+ function getTsConfigPackageJson() {
2320
+ return JSON.stringify(
2321
+ {
2322
+ name: "@workspace/typescript-config",
2323
+ version: "0.0.0",
2324
+ private: true
2325
+ },
2326
+ null,
2327
+ 2
2328
+ ) + "\n";
2329
+ }
2330
+ function getBaseTsConfig() {
2331
+ return JSON.stringify(
2332
+ {
2333
+ $schema: "https://json.schemastore.org/tsconfig",
2334
+ display: "Default",
2335
+ compilerOptions: {
2336
+ declaration: true,
2337
+ declarationMap: true,
2338
+ esModuleInterop: true,
2339
+ incremental: false,
2340
+ isolatedModules: true,
2341
+ lib: ["es2022", "DOM", "DOM.Iterable"],
2342
+ module: "NodeNext",
2343
+ moduleDetection: "force",
2344
+ moduleResolution: "NodeNext",
2345
+ noUncheckedIndexedAccess: true,
2346
+ resolveJsonModule: true,
2347
+ skipLibCheck: true,
2348
+ strict: true,
2349
+ target: "ES2022"
2350
+ }
2351
+ },
2352
+ null,
2353
+ 2
2354
+ ) + "\n";
2355
+ }
2356
+ function getNextJsTsConfig() {
2357
+ return JSON.stringify(
2358
+ {
2359
+ $schema: "https://json.schemastore.org/tsconfig",
2360
+ display: "Next.js",
2361
+ extends: "./base.json",
2362
+ compilerOptions: {
2363
+ plugins: [{ name: "next" }],
2364
+ module: "ESNext",
2365
+ moduleResolution: "Bundler",
2366
+ allowJs: true,
2367
+ jsx: "preserve",
2368
+ noEmit: true
2369
+ }
2370
+ },
2371
+ null,
2372
+ 2
2373
+ ) + "\n";
2374
+ }
2375
+ function getReactLibraryTsConfig() {
2376
+ return JSON.stringify(
2377
+ {
2378
+ $schema: "https://json.schemastore.org/tsconfig",
2379
+ display: "React Library",
2380
+ extends: "./base.json",
2381
+ compilerOptions: {
2382
+ jsx: "react-jsx",
2383
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
2384
+ module: "ESNext",
2385
+ moduleResolution: "Bundler"
2386
+ }
2387
+ },
2388
+ null,
2389
+ 2
2390
+ ) + "\n";
2391
+ }
2392
+ function getNodeLibraryTsConfig() {
2393
+ return JSON.stringify(
2394
+ {
2395
+ $schema: "https://json.schemastore.org/tsconfig",
2396
+ display: "Node Library",
2397
+ extends: "./base.json",
2398
+ compilerOptions: {
2399
+ lib: ["ES2022"],
2400
+ module: "NodeNext",
2401
+ moduleResolution: "NodeNext"
2402
+ }
2403
+ },
2404
+ null,
2405
+ 2
2406
+ ) + "\n";
2407
+ }
2408
+ function isMonorepo(projectType) {
2409
+ return projectType === "turbo" || projectType === "nx" || projectType === "pnpm-workspace";
2410
+ }
2411
+ async function generateSharedConfigs(targetDir, projectType) {
2412
+ const result = {
2413
+ created: [],
2414
+ modified: [],
2415
+ skipped: [],
2416
+ backedUp: []
2417
+ };
2418
+ if (!isMonorepo(projectType)) {
2419
+ return result;
2420
+ }
2421
+ const packagesDir = join19(targetDir, "packages");
2422
+ const eslintConfigDir = join19(packagesDir, "eslint-config");
2423
+ await ensureDir(eslintConfigDir);
2424
+ const eslintFiles = [
2425
+ { path: join19(eslintConfigDir, "package.json"), content: getEslintConfigPackageJson(), name: "packages/eslint-config/package.json" },
2426
+ { path: join19(eslintConfigDir, "base.js"), content: getBaseEslintConfig(), name: "packages/eslint-config/base.js" },
2427
+ { path: join19(eslintConfigDir, "next.js"), content: getNextJsEslintConfig(), name: "packages/eslint-config/next.js" },
2428
+ { path: join19(eslintConfigDir, "react-internal.js"), content: getReactInternalEslintConfig(), name: "packages/eslint-config/react-internal.js" },
2429
+ { path: join19(eslintConfigDir, "vite.js"), content: getViteEslintConfig(), name: "packages/eslint-config/vite.js" }
2430
+ ];
2431
+ for (const file of eslintFiles) {
2432
+ const writeResult = await writeFileSafe(file.path, file.content, { backup: true });
2433
+ if (writeResult.created) {
2434
+ result.created.push(file.name);
2435
+ if (writeResult.backedUp) {
2436
+ result.backedUp.push(file.name);
2437
+ }
2438
+ }
2439
+ }
2440
+ const tsConfigDir = join19(packagesDir, "typescript-config");
2441
+ await ensureDir(tsConfigDir);
2442
+ const tsFiles = [
2443
+ { path: join19(tsConfigDir, "package.json"), content: getTsConfigPackageJson(), name: "packages/typescript-config/package.json" },
2444
+ { path: join19(tsConfigDir, "base.json"), content: getBaseTsConfig(), name: "packages/typescript-config/base.json" },
2445
+ { path: join19(tsConfigDir, "nextjs.json"), content: getNextJsTsConfig(), name: "packages/typescript-config/nextjs.json" },
2446
+ { path: join19(tsConfigDir, "react-library.json"), content: getReactLibraryTsConfig(), name: "packages/typescript-config/react-library.json" },
2447
+ { path: join19(tsConfigDir, "node-library.json"), content: getNodeLibraryTsConfig(), name: "packages/typescript-config/node-library.json" }
2448
+ ];
2449
+ for (const file of tsFiles) {
2450
+ const writeResult = await writeFileSafe(file.path, file.content, { backup: true });
2451
+ if (writeResult.created) {
2452
+ result.created.push(file.name);
2453
+ if (writeResult.backedUp) {
2454
+ result.backedUp.push(file.name);
2455
+ }
2456
+ }
2457
+ }
2458
+ return result;
2459
+ }
2460
+
2055
2461
  // src/utils/git.ts
2056
2462
  import { execa } from "execa";
2057
- import { existsSync as existsSync7 } from "fs";
2058
- import { join as join18 } from "path";
2463
+ import { existsSync as existsSync8 } from "fs";
2464
+ import { join as join20 } from "path";
2059
2465
  async function isGitRepo(targetDir = process.cwd()) {
2060
- if (existsSync7(join18(targetDir, ".git"))) {
2466
+ if (existsSync8(join20(targetDir, ".git"))) {
2061
2467
  return true;
2062
2468
  }
2063
2469
  try {
@@ -2162,8 +2568,14 @@ async function runInit(targetDir = process.cwd()) {
2162
2568
  return;
2163
2569
  }
2164
2570
  const usesReact = await detectReact(targetDir);
2571
+ const usesNextJs = await detectNextJs(targetDir);
2165
2572
  const installSpinner = p2.spinner();
2166
- const packagesToInstall = usesReact ? [...RAFTSTACK_PACKAGES, ...REACT_ESLINT_PACKAGES] : RAFTSTACK_PACKAGES;
2573
+ let packagesToInstall = [...RAFTSTACK_PACKAGES];
2574
+ if (usesNextJs) {
2575
+ packagesToInstall = [...packagesToInstall, ...NEXTJS_ESLINT_PACKAGES];
2576
+ } else if (usesReact) {
2577
+ packagesToInstall = [...packagesToInstall, ...REACT_ESLINT_PACKAGES];
2578
+ }
2167
2579
  installSpinner.start("Installing dependencies...");
2168
2580
  const installResult = await installPackages(
2169
2581
  config.packageManager,
@@ -2187,8 +2599,8 @@ async function runInit(targetDir = process.cwd()) {
2187
2599
  );
2188
2600
  installFailed = true;
2189
2601
  }
2190
- const spinner4 = p2.spinner();
2191
- spinner4.start("Generating configuration files...");
2602
+ const spinner5 = p2.spinner();
2603
+ spinner5.start("Generating configuration files...");
2192
2604
  const results = [];
2193
2605
  try {
2194
2606
  results.push(
@@ -2199,6 +2611,9 @@ async function runInit(targetDir = process.cwd()) {
2199
2611
  results.push(await generateBranchValidation(targetDir));
2200
2612
  results.push(await generateEslint(targetDir, config.usesTypeScript, false));
2201
2613
  results.push(await generatePrettier(targetDir));
2614
+ if (isMonorepo(config.projectType)) {
2615
+ results.push(await generateSharedConfigs(targetDir, config.projectType));
2616
+ }
2202
2617
  results.push(await generatePRTemplate(targetDir, !!config.asanaBaseUrl));
2203
2618
  results.push(
2204
2619
  await generateGitHubWorkflows(
@@ -2220,10 +2635,11 @@ async function runInit(targetDir = process.cwd()) {
2220
2635
  results.push(await generateClaudeSkills(targetDir, {
2221
2636
  includeAsana: !!config.asanaBaseUrl
2222
2637
  }));
2638
+ results.push(await generateClaudeCommands(targetDir));
2223
2639
  results.push(await updateProjectPackageJson(targetDir, config));
2224
- spinner4.stop("Configuration files generated!");
2640
+ spinner5.stop("Configuration files generated!");
2225
2641
  } catch (error) {
2226
- spinner4.stop("Error generating files");
2642
+ spinner5.stop("Error generating files");
2227
2643
  p2.log.error(
2228
2644
  pc2.red(
2229
2645
  `Error: ${error instanceof Error ? error.message : "Unknown error"}`
@@ -2371,11 +2787,11 @@ async function applyMergeStrategy(owner, repo, settings) {
2371
2787
  async function runSetupProtection(targetDir = process.cwd()) {
2372
2788
  console.log();
2373
2789
  p3.intro(pc3.bgCyan(pc3.black(" Branch Protection Setup ")));
2374
- const spinner4 = p3.spinner();
2375
- spinner4.start("Checking GitHub CLI...");
2790
+ const spinner5 = p3.spinner();
2791
+ spinner5.start("Checking GitHub CLI...");
2376
2792
  const ghAvailable = await isGhCliAvailable();
2377
2793
  if (!ghAvailable) {
2378
- spinner4.stop("GitHub CLI not found or not authenticated");
2794
+ spinner5.stop("GitHub CLI not found or not authenticated");
2379
2795
  console.log();
2380
2796
  p3.log.error(pc3.red("The GitHub CLI (gh) is required for this command."));
2381
2797
  p3.log.info("Install it from: https://cli.github.com/");
@@ -2388,18 +2804,18 @@ async function runSetupProtection(targetDir = process.cwd()) {
2388
2804
  );
2389
2805
  process.exit(1);
2390
2806
  }
2391
- spinner4.stop("GitHub CLI ready");
2392
- spinner4.start("Getting repository info...");
2807
+ spinner5.stop("GitHub CLI ready");
2808
+ spinner5.start("Getting repository info...");
2393
2809
  const repoInfo = await getGitHubRepoInfo(targetDir);
2394
2810
  if (!repoInfo) {
2395
- spinner4.stop("Could not determine repository");
2811
+ spinner5.stop("Could not determine repository");
2396
2812
  p3.log.error(
2397
2813
  pc3.red("Could not determine the GitHub repository for this directory.")
2398
2814
  );
2399
2815
  p3.log.info("Make sure you're in a git repository with a GitHub remote.");
2400
2816
  process.exit(1);
2401
2817
  }
2402
- spinner4.stop(`Repository: ${pc3.cyan(`${repoInfo.owner}/${repoInfo.repo}`)}`);
2818
+ spinner5.stop(`Repository: ${pc3.cyan(`${repoInfo.owner}/${repoInfo.repo}`)}`);
2403
2819
  const branches = await p3.multiselect({
2404
2820
  message: "Which branches need protection?",
2405
2821
  options: [
@@ -2488,13 +2904,13 @@ async function runSetupProtection(targetDir = process.cwd()) {
2488
2904
  p3.cancel("Setup cancelled.");
2489
2905
  process.exit(0);
2490
2906
  }
2491
- spinner4.start("Configuring merge strategy...");
2907
+ spinner5.start("Configuring merge strategy...");
2492
2908
  try {
2493
2909
  const repoSettings = getMergeStrategySettings(mergeStrategy);
2494
2910
  await applyMergeStrategy(repoInfo.owner, repoInfo.repo, repoSettings);
2495
- spinner4.stop("Merge strategy configured!");
2911
+ spinner5.stop("Merge strategy configured!");
2496
2912
  } catch (error) {
2497
- spinner4.stop("Failed to configure merge strategy");
2913
+ spinner5.stop("Failed to configure merge strategy");
2498
2914
  const errorMsg = error instanceof Error ? error.message : "Unknown error";
2499
2915
  p3.log.warn(pc3.yellow(`Warning: Could not set merge strategy: ${errorMsg}`));
2500
2916
  p3.log.info(pc3.dim("Continuing with branch protection..."));
@@ -2502,16 +2918,16 @@ async function runSetupProtection(targetDir = process.cwd()) {
2502
2918
  const protectedBranches = [];
2503
2919
  const failedBranches = [];
2504
2920
  for (const branch of branches) {
2505
- spinner4.start(`Protecting branch: ${branch}...`);
2921
+ spinner5.start(`Protecting branch: ${branch}...`);
2506
2922
  try {
2507
2923
  const settings = getDefaultSettings(branch);
2508
2924
  settings.requiredReviews = requiredReviews;
2509
2925
  await applyBranchProtection(repoInfo.owner, repoInfo.repo, settings);
2510
2926
  protectedBranches.push(branch);
2511
- spinner4.stop(`Protected: ${pc3.green(branch)}`);
2927
+ spinner5.stop(`Protected: ${pc3.green(branch)}`);
2512
2928
  } catch (error) {
2513
2929
  failedBranches.push(branch);
2514
- spinner4.stop(`Failed: ${pc3.red(branch)}`);
2930
+ spinner5.stop(`Failed: ${pc3.red(branch)}`);
2515
2931
  const errorMsg = error instanceof Error ? error.message : "Unknown error";
2516
2932
  p3.log.warn(
2517
2933
  pc3.yellow(
@@ -2543,41 +2959,380 @@ async function runSetupProtection(targetDir = process.cwd()) {
2543
2959
  }
2544
2960
 
2545
2961
  // src/commands/metrics.ts
2546
- import { execa as execa3 } from "execa";
2962
+ import { execa as execa4 } from "execa";
2547
2963
  import * as p4 from "@clack/prompts";
2548
2964
  import pc4 from "picocolors";
2549
- async function getRecentCommits(targetDir, days) {
2965
+
2966
+ // src/utils/code-analyzer.ts
2967
+ import { execa as execa3 } from "execa";
2968
+ import { readFileSync as readFileSync2 } from "fs";
2969
+ var THRESHOLDS = {
2970
+ "file-length": 300,
2971
+ "function-length": 30,
2972
+ "max-params": 3,
2973
+ "cyclomatic-complexity": 10
2974
+ };
2975
+ async function findSourceFiles(targetDir) {
2550
2976
  try {
2551
2977
  const { stdout } = await execa3(
2552
2978
  "git",
2553
- ["log", `--since=${days} days ago`, "--oneline", "--no-merges"],
2979
+ ["ls-files", "*.ts", "*.tsx", "*.js", "*.jsx"],
2554
2980
  { cwd: targetDir }
2555
2981
  );
2556
- return stdout.trim().split("\n").filter(Boolean);
2982
+ return stdout.trim().split("\n").filter(Boolean).filter(
2983
+ (f) => !f.includes("node_modules") && !f.includes("dist/") && !f.includes("build/") && !f.includes(".min.") && !f.endsWith(".d.ts")
2984
+ );
2557
2985
  } catch {
2558
2986
  return [];
2559
2987
  }
2560
2988
  }
2561
- async function getCommitMessages(targetDir, days) {
2989
+ function extractFunctions(source, _filePath) {
2990
+ const functions = [];
2991
+ const lines = source.split("\n");
2992
+ const functionPatterns = [
2993
+ // function name(params) or async function name(params)
2994
+ /^(\s*)(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
2995
+ // const name = (params) => or const name = async (params) =>
2996
+ /^(\s*)(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
2997
+ // const name = function(params)
2998
+ /^(\s*)(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?function\s*\(([^)]*)\)/,
2999
+ // class method: name(params) { or async name(params) {
3000
+ /^(\s*)(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/
3001
+ ];
3002
+ for (let i = 0; i < lines.length; i++) {
3003
+ const line = lines[i];
3004
+ for (const pattern of functionPatterns) {
3005
+ const match = line.match(pattern);
3006
+ if (match) {
3007
+ const [, indent, name, params] = match;
3008
+ if (name === "constructor" || name === "if" || name === "for" || name === "while" || name === "switch" || name === "catch") {
3009
+ continue;
3010
+ }
3011
+ const paramCount = countParameters(params);
3012
+ const endLine = findFunctionEnd(lines, i, indent.length);
3013
+ if (endLine > i) {
3014
+ const body = lines.slice(i, endLine + 1).join("\n");
3015
+ functions.push({
3016
+ name,
3017
+ startLine: i + 1,
3018
+ // 1-indexed
3019
+ endLine: endLine + 1,
3020
+ paramCount,
3021
+ body
3022
+ });
3023
+ }
3024
+ break;
3025
+ }
3026
+ }
3027
+ }
3028
+ return functions;
3029
+ }
3030
+ function countParameters(params) {
3031
+ const trimmed = params.trim();
3032
+ if (!trimmed) return 0;
3033
+ let depth = 0;
3034
+ let count = 1;
3035
+ for (const char of trimmed) {
3036
+ if (char === "(" || char === "{" || char === "[" || char === "<") {
3037
+ depth++;
3038
+ } else if (char === ")" || char === "}" || char === "]" || char === ">") {
3039
+ depth--;
3040
+ } else if (char === "," && depth === 0) {
3041
+ count++;
3042
+ }
3043
+ }
3044
+ return count;
3045
+ }
3046
+ function findFunctionEnd(lines, startLine, _baseIndent) {
3047
+ let braceDepth = 0;
3048
+ let foundOpenBrace = false;
3049
+ for (let i = startLine; i < lines.length; i++) {
3050
+ const line = lines[i];
3051
+ for (const char of line) {
3052
+ if (char === "{") {
3053
+ braceDepth++;
3054
+ foundOpenBrace = true;
3055
+ } else if (char === "}") {
3056
+ braceDepth--;
3057
+ if (foundOpenBrace && braceDepth === 0) {
3058
+ return i;
3059
+ }
3060
+ }
3061
+ }
3062
+ }
3063
+ return startLine;
3064
+ }
3065
+ function countComplexity(code) {
3066
+ let complexity = 1;
3067
+ const patterns = [
3068
+ /\bif\s*\(/g,
3069
+ /\belse\s+if\s*\(/g,
3070
+ /\bcase\s+/g,
3071
+ /\bfor\s*\(/g,
3072
+ /\bwhile\s*\(/g,
3073
+ /\bdo\s*\{/g,
3074
+ /\bcatch\s*\(/g,
3075
+ /&&/g,
3076
+ /\|\|/g,
3077
+ /\?\?/g
3078
+ // nullish coalescing
3079
+ ];
3080
+ const ternaryPattern = /\?[^:?]+:/g;
3081
+ for (const pattern of patterns) {
3082
+ const matches = code.match(pattern);
3083
+ if (matches) {
3084
+ complexity += matches.length;
3085
+ }
3086
+ }
3087
+ const ternaryMatches = code.match(ternaryPattern);
3088
+ if (ternaryMatches) {
3089
+ complexity += ternaryMatches.length;
3090
+ }
3091
+ return complexity;
3092
+ }
3093
+ function findMagicNumbers(source, filePath) {
3094
+ const violations = [];
3095
+ const lines = source.split("\n");
3096
+ const allowedNumbers = /* @__PURE__ */ new Set([
3097
+ "0",
3098
+ "1",
3099
+ "-1",
3100
+ "2",
3101
+ "100",
3102
+ "1000",
3103
+ "0.5",
3104
+ "0.1"
3105
+ ]);
3106
+ const numberPattern = /-?\d+\.?\d*/g;
3107
+ for (let i = 0; i < lines.length; i++) {
3108
+ const line = lines[i];
3109
+ const trimmedLine = line.trim();
3110
+ if (/^\s*(?:export\s+)?const\s+\w+\s*[:=]/.test(line)) {
3111
+ continue;
3112
+ }
3113
+ if (trimmedLine.startsWith("import ")) {
3114
+ continue;
3115
+ }
3116
+ if (trimmedLine.startsWith("//") || trimmedLine.startsWith("*")) {
3117
+ continue;
3118
+ }
3119
+ const cleanedLine = line.replace(/\[\d+\]/g, "").replace(/\.length\s*[<>=]+\s*\d+/g, "").replace(/:\s*number/g, "").replace(/[<>=]+\s*0\b/g, "").replace(/\+\+|--/g, "");
3120
+ const matches = cleanedLine.match(numberPattern);
3121
+ if (matches) {
3122
+ for (const match of matches) {
3123
+ if (allowedNumbers.has(match)) continue;
3124
+ if (/0[xXbBoO]/.test(match)) continue;
3125
+ if (line.includes(`"${match}`) || line.includes(`'${match}`)) continue;
3126
+ violations.push({
3127
+ filePath,
3128
+ rule: "magic-number",
3129
+ line: i + 1,
3130
+ message: `Magic number ${match} should be a named constant`
3131
+ });
3132
+ }
3133
+ }
3134
+ }
3135
+ return violations;
3136
+ }
3137
+ function analyzeFile(filePath, source) {
3138
+ const violations = [];
3139
+ const lines = source.split("\n");
3140
+ const lineCount = lines.length;
3141
+ if (lineCount > THRESHOLDS["file-length"]) {
3142
+ violations.push({
3143
+ filePath,
3144
+ rule: "file-length",
3145
+ line: 1,
3146
+ message: `File has ${lineCount} lines (max: ${THRESHOLDS["file-length"]})`
3147
+ });
3148
+ }
3149
+ const functions = extractFunctions(source, filePath);
3150
+ for (const fn of functions) {
3151
+ const fnLineCount = fn.endLine - fn.startLine + 1;
3152
+ if (fnLineCount > THRESHOLDS["function-length"]) {
3153
+ violations.push({
3154
+ filePath,
3155
+ rule: "function-length",
3156
+ line: fn.startLine,
3157
+ message: `Function '${fn.name}' has ${fnLineCount} lines (max: ${THRESHOLDS["function-length"]})`
3158
+ });
3159
+ }
3160
+ if (fn.paramCount > THRESHOLDS["max-params"]) {
3161
+ violations.push({
3162
+ filePath,
3163
+ rule: "max-params",
3164
+ line: fn.startLine,
3165
+ message: `Function '${fn.name}' has ${fn.paramCount} parameters (max: ${THRESHOLDS["max-params"]})`
3166
+ });
3167
+ }
3168
+ const complexity = countComplexity(fn.body);
3169
+ if (complexity > THRESHOLDS["cyclomatic-complexity"]) {
3170
+ violations.push({
3171
+ filePath,
3172
+ rule: "cyclomatic-complexity",
3173
+ line: fn.startLine,
3174
+ message: `Function '${fn.name}' has complexity ${complexity} (max: ${THRESHOLDS["cyclomatic-complexity"]})`
3175
+ });
3176
+ }
3177
+ }
3178
+ const magicViolations = findMagicNumbers(source, filePath);
3179
+ violations.push(...magicViolations);
3180
+ return violations;
3181
+ }
3182
+ async function analyzeCodebase(targetDir) {
3183
+ const files = await findSourceFiles(targetDir);
3184
+ const allViolations = [];
3185
+ let totalLines = 0;
3186
+ const violationsByRule = {
3187
+ "file-length": 0,
3188
+ "function-length": 0,
3189
+ "max-params": 0,
3190
+ "cyclomatic-complexity": 0,
3191
+ "magic-number": 0
3192
+ };
3193
+ const violationsByFile = /* @__PURE__ */ new Map();
3194
+ for (const file of files) {
3195
+ try {
3196
+ const fullPath = `${targetDir}/${file}`;
3197
+ const source = readFileSync2(fullPath, "utf-8");
3198
+ totalLines += source.split("\n").length;
3199
+ const violations = analyzeFile(file, source);
3200
+ for (const v of violations) {
3201
+ violationsByRule[v.rule]++;
3202
+ violationsByFile.set(file, (violationsByFile.get(file) || 0) + 1);
3203
+ }
3204
+ allViolations.push(...violations);
3205
+ } catch {
3206
+ }
3207
+ }
3208
+ const filesWithoutFileLengthViolation = files.length - violationsByRule["file-length"];
3209
+ const fileLengthCompliance = files.length > 0 ? Math.round(filesWithoutFileLengthViolation / files.length * 100) : 100;
3210
+ const calculateRuleCompliance = (violations, fileCount) => {
3211
+ if (fileCount === 0) return 100;
3212
+ const expectedMax = Math.max(1, Math.floor(fileCount / 5));
3213
+ const ratio = Math.min(1, violations / expectedMax);
3214
+ return Math.round((1 - ratio * 0.5) * 100);
3215
+ };
3216
+ const complianceByRule = {
3217
+ "file-length": fileLengthCompliance,
3218
+ "function-length": calculateRuleCompliance(
3219
+ violationsByRule["function-length"],
3220
+ files.length
3221
+ ),
3222
+ "max-params": calculateRuleCompliance(
3223
+ violationsByRule["max-params"],
3224
+ files.length
3225
+ ),
3226
+ "cyclomatic-complexity": calculateRuleCompliance(
3227
+ violationsByRule["cyclomatic-complexity"],
3228
+ files.length
3229
+ ),
3230
+ "magic-number": calculateRuleCompliance(
3231
+ violationsByRule["magic-number"],
3232
+ files.length
3233
+ )
3234
+ };
3235
+ const overallCompliance = Math.round(
3236
+ Object.values(complianceByRule).reduce((a, b) => a + b, 0) / 5
3237
+ );
3238
+ const worstFiles = Array.from(violationsByFile.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10).map(([path, count]) => ({ path, count }));
3239
+ return {
3240
+ filesAnalyzed: files.length,
3241
+ totalLines,
3242
+ violations: allViolations,
3243
+ complianceByRule,
3244
+ overallCompliance,
3245
+ worstFiles
3246
+ };
3247
+ }
3248
+
3249
+ // src/commands/metrics.ts
3250
+ var CONVENTIONAL_COMMIT_PATTERN = /^(✨|🐛|📝|💄|♻️|⚡|✅|📦|👷|🔧|⏪)\s+(feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(\([a-z0-9-]+\))?:\s.+/;
3251
+ async function getCommitsWithAuthors(targetDir, days) {
2562
3252
  try {
2563
- const { stdout } = await execa3(
3253
+ const { stdout } = await execa4(
2564
3254
  "git",
2565
3255
  [
2566
3256
  "log",
2567
3257
  `--since=${days} days ago`,
2568
- "--format=%B---COMMIT_SEPARATOR---",
3258
+ "--format=%H|%an|%ae|%s---BODY---%B---END---",
2569
3259
  "--no-merges"
2570
3260
  ],
2571
3261
  { cwd: targetDir }
2572
3262
  );
2573
- return stdout.split("---COMMIT_SEPARATOR---").filter((m) => m.trim());
3263
+ const commits = [];
3264
+ const entries = stdout.split("---END---").filter((e) => e.trim());
3265
+ for (const entry of entries) {
3266
+ const bodyMarker = entry.indexOf("---BODY---");
3267
+ if (bodyMarker === -1) continue;
3268
+ const headerPart = entry.substring(0, bodyMarker).trim();
3269
+ const bodyPart = entry.substring(bodyMarker + 10).trim();
3270
+ const parts = headerPart.split("|");
3271
+ if (parts.length >= 4) {
3272
+ commits.push({
3273
+ hash: parts[0],
3274
+ authorName: parts[1],
3275
+ authorEmail: parts[2],
3276
+ subject: parts.slice(3).join("|"),
3277
+ // Subject might contain |
3278
+ body: bodyPart
3279
+ });
3280
+ }
3281
+ }
3282
+ return commits;
2574
3283
  } catch {
2575
3284
  return [];
2576
3285
  }
2577
3286
  }
3287
+ function isConventionalCommit(subject) {
3288
+ return CONVENTIONAL_COMMIT_PATTERN.test(subject);
3289
+ }
3290
+ function hasTaskLink(commit) {
3291
+ const fullMessage = `${commit.subject}
3292
+ ${commit.body}`;
3293
+ 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);
3294
+ }
3295
+ function calculateAuthorMetrics(commits) {
3296
+ const authorMap = /* @__PURE__ */ new Map();
3297
+ for (const commit of commits) {
3298
+ const existing = authorMap.get(commit.authorEmail);
3299
+ if (existing) {
3300
+ existing.commits.push(commit);
3301
+ existing.name = commit.authorName;
3302
+ } else {
3303
+ authorMap.set(commit.authorEmail, {
3304
+ name: commit.authorName,
3305
+ email: commit.authorEmail,
3306
+ commits: [commit]
3307
+ });
3308
+ }
3309
+ }
3310
+ const authorMetrics = [];
3311
+ for (const [, author] of authorMap) {
3312
+ const totalCommits = author.commits.length;
3313
+ const withTaskLinks = author.commits.filter(hasTaskLink).length;
3314
+ const conventional = author.commits.filter(
3315
+ (c) => isConventionalCommit(c.subject)
3316
+ ).length;
3317
+ const taskLinkCompliance = totalCommits > 0 ? Math.round(withTaskLinks / totalCommits * 100) : 100;
3318
+ const conventionalCompliance = totalCommits > 0 ? Math.round(conventional / totalCommits * 100) : 100;
3319
+ const overallScore = Math.round(
3320
+ taskLinkCompliance * 0.4 + conventionalCompliance * 0.6
3321
+ );
3322
+ authorMetrics.push({
3323
+ name: author.name,
3324
+ email: author.email,
3325
+ totalCommits,
3326
+ taskLinkCompliance,
3327
+ conventionalCompliance,
3328
+ overallScore
3329
+ });
3330
+ }
3331
+ return authorMetrics.sort((a, b) => b.overallScore - a.overallScore);
3332
+ }
2578
3333
  async function getBranchNames(targetDir) {
2579
3334
  try {
2580
- const { stdout } = await execa3(
3335
+ const { stdout } = await execa4(
2581
3336
  "git",
2582
3337
  ["branch", "-a", "--format=%(refname:short)"],
2583
3338
  { cwd: targetDir }
@@ -2591,22 +3346,11 @@ function isValidBranchName(name) {
2591
3346
  const pattern = /^(main|staging|development|master)$|^(feature|bugfix|hotfix|chore|refactor)\/[a-z0-9-]+$/;
2592
3347
  return pattern.test(name);
2593
3348
  }
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;
3349
+ async function calculateBranchMetrics(targetDir) {
3350
+ const branches = await getBranchNames(targetDir);
2604
3351
  const validBranches = branches.filter(isValidBranchName);
2605
3352
  const invalidBranches = branches.filter((b) => !isValidBranchName(b));
2606
3353
  return {
2607
- totalCommits: commits.length,
2608
- commitsWithTaskLinks,
2609
- taskLinkCompliance: commits.length > 0 ? Math.round(commitsWithTaskLinks / commits.length * 100) : 100,
2610
3354
  branchNames: branches,
2611
3355
  validBranches: validBranches.length,
2612
3356
  invalidBranches: invalidBranches.length,
@@ -2618,56 +3362,215 @@ function formatCompliance(percentage) {
2618
3362
  if (percentage >= 70) return pc4.yellow(`${percentage}%`);
2619
3363
  return pc4.red(`${percentage}%`);
2620
3364
  }
2621
- async function runMetrics(targetDir) {
2622
- p4.intro(pc4.bgCyan(pc4.black(" RaftStack Metrics ")));
3365
+ function formatLeaderboard(authors, title, limit) {
3366
+ if (authors.length === 0) return "";
3367
+ const lines = [pc4.bold(title)];
3368
+ const displayed = authors.slice(0, limit);
3369
+ displayed.forEach((author, index) => {
3370
+ const score = formatCompliance(author.overallScore);
3371
+ const truncatedEmail = author.email.length > 25 ? author.email.substring(0, 22) + "..." : author.email;
3372
+ lines.push(
3373
+ ` ${index + 1}. ${author.name} (${truncatedEmail}) - ${score} - ${author.totalCommits} commits`
3374
+ );
3375
+ });
3376
+ return lines.join("\n");
3377
+ }
3378
+ function formatCodebaseMetrics(metrics) {
3379
+ const ruleNames = {
3380
+ "file-length": `File length (\u2264300)`,
3381
+ "function-length": `Function length (\u226430)`,
3382
+ "max-params": `Max parameters (\u22643)`,
3383
+ "cyclomatic-complexity": `Cyclomatic complexity`,
3384
+ "magic-number": `Magic numbers`
3385
+ };
3386
+ const lines = [
3387
+ pc4.bold("CODEBASE COMPLIANCE"),
3388
+ ` Files analyzed: ${metrics.filesAnalyzed}`,
3389
+ ` Total lines: ${metrics.totalLines.toLocaleString()}`,
3390
+ "",
3391
+ pc4.bold(" RULE COMPLIANCE")
3392
+ ];
3393
+ for (const rule of Object.keys(ruleNames)) {
3394
+ const compliance = metrics.complianceByRule[rule];
3395
+ const label = ruleNames[rule].padEnd(28);
3396
+ lines.push(` ${label}${formatCompliance(compliance)}`);
3397
+ }
3398
+ lines.push("");
3399
+ lines.push(` ${pc4.bold("OVERALL:")} ${formatCompliance(metrics.overallCompliance)}`);
3400
+ if (metrics.worstFiles.length > 0) {
3401
+ lines.push("");
3402
+ lines.push(pc4.bold(" TOP VIOLATIONS"));
3403
+ for (const file of metrics.worstFiles.slice(0, 5)) {
3404
+ lines.push(` ${file.path} (${file.count} violations)`);
3405
+ const fileViolations = metrics.violations.filter(
3406
+ (v) => v.filePath === file.path
3407
+ );
3408
+ const byRule = /* @__PURE__ */ new Map();
3409
+ for (const v of fileViolations) {
3410
+ byRule.set(v.rule, (byRule.get(v.rule) || 0) + 1);
3411
+ }
3412
+ for (const [rule, count] of byRule) {
3413
+ lines.push(` - ${count}x ${rule}`);
3414
+ }
3415
+ }
3416
+ }
3417
+ return lines.join("\n");
3418
+ }
3419
+ async function runMetrics(targetDir, options = {}) {
3420
+ const { git: gitOnly, code: codeOnly, ci: ciMode, threshold = 70 } = options;
3421
+ const showGit = !codeOnly;
3422
+ const showCode = !gitOnly;
3423
+ const days = options.days || (ciMode ? 30 : null);
3424
+ if (!ciMode) {
3425
+ p4.intro(pc4.bgCyan(pc4.black(" RaftStack Metrics ")));
3426
+ }
2623
3427
  if (!await isGitRepo(targetDir)) {
3428
+ if (ciMode) {
3429
+ console.error("Error: Not a git repository");
3430
+ process.exit(1);
3431
+ }
2624
3432
  p4.cancel("Not a git repository");
2625
3433
  process.exit(1);
2626
3434
  }
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)}
3435
+ let selectedDays = days;
3436
+ if (!selectedDays && !ciMode) {
3437
+ const daysOption = await p4.select({
3438
+ message: "Time period to analyze:",
3439
+ options: [
3440
+ { value: 7, label: "Last 7 days" },
3441
+ { value: 14, label: "Last 14 days" },
3442
+ { value: 30, label: "Last 30 days" },
3443
+ { value: 90, label: "Last 90 days" }
3444
+ ]
3445
+ });
3446
+ if (p4.isCancel(daysOption)) {
3447
+ p4.cancel("Operation cancelled");
3448
+ process.exit(0);
3449
+ }
3450
+ selectedDays = daysOption;
3451
+ }
3452
+ const analyzeDays = selectedDays || 30;
3453
+ const spinner5 = ciMode ? null : p4.spinner();
3454
+ spinner5?.start("Analyzing repository...");
3455
+ let overallCompliance = 100;
3456
+ const complianceScores = [];
3457
+ if (showGit) {
3458
+ const [commits, branchMetrics] = await Promise.all([
3459
+ getCommitsWithAuthors(targetDir, analyzeDays),
3460
+ calculateBranchMetrics(targetDir)
3461
+ ]);
3462
+ const authorMetrics = calculateAuthorMetrics(commits);
3463
+ const totalCommits = commits.length;
3464
+ const withTaskLinks = commits.filter(hasTaskLink).length;
3465
+ const conventional = commits.filter(
3466
+ (c) => isConventionalCommit(c.subject)
3467
+ ).length;
3468
+ const taskLinkCompliance = totalCommits > 0 ? Math.round(withTaskLinks / totalCommits * 100) : 100;
3469
+ const conventionalCompliance = totalCommits > 0 ? Math.round(conventional / totalCommits * 100) : 100;
3470
+ complianceScores.push(
3471
+ taskLinkCompliance,
3472
+ conventionalCompliance,
3473
+ branchMetrics.branchCompliance
3474
+ );
3475
+ spinner5?.stop("Git analysis complete");
3476
+ if (ciMode) {
3477
+ console.log("\n=== GIT METRICS ===");
3478
+ console.log(`Commits (last ${analyzeDays} days): ${totalCommits}`);
3479
+ console.log(`Task link compliance: ${taskLinkCompliance}%`);
3480
+ console.log(`Conventional commit compliance: ${conventionalCompliance}%`);
3481
+ console.log(`Branch compliance: ${branchMetrics.branchCompliance}%`);
3482
+ } else {
3483
+ p4.note(
3484
+ `${pc4.bold("Commits")} (last ${analyzeDays} days)
3485
+ Total: ${totalCommits}
3486
+ With task links: ${withTaskLinks} (${formatCompliance(taskLinkCompliance)})
3487
+ Conventional format: ${conventional} (${formatCompliance(conventionalCompliance)})
2650
3488
 
2651
3489
  ${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:
3490
+ Total: ${branchMetrics.branchNames.length}
3491
+ Valid naming: ${branchMetrics.validBranches}
3492
+ Invalid naming: ${branchMetrics.invalidBranches}
3493
+ Compliance: ${formatCompliance(branchMetrics.branchCompliance)}`,
3494
+ "Git Metrics"
3495
+ );
3496
+ if (authorMetrics.length > 0) {
3497
+ const topPerformers = authorMetrics.filter((a) => a.overallScore >= 70);
3498
+ const needsImprovement = authorMetrics.filter((a) => a.overallScore < 70).reverse();
3499
+ let leaderboardText = "";
3500
+ if (topPerformers.length > 0) {
3501
+ leaderboardText += formatLeaderboard(
3502
+ topPerformers,
3503
+ "TOP PERFORMERS",
3504
+ 5
3505
+ );
3506
+ }
3507
+ if (needsImprovement.length > 0) {
3508
+ if (leaderboardText) leaderboardText += "\n\n";
3509
+ leaderboardText += formatLeaderboard(
3510
+ needsImprovement,
3511
+ "NEEDS IMPROVEMENT",
3512
+ 5
3513
+ );
3514
+ }
3515
+ if (leaderboardText) {
3516
+ p4.note(leaderboardText, "Author Leaderboard");
3517
+ }
3518
+ }
3519
+ if (branchMetrics.invalidBranches > 0) {
3520
+ const invalidBranches = branchMetrics.branchNames.filter(
3521
+ (b) => !isValidBranchName(b)
3522
+ );
3523
+ p4.log.warn(
3524
+ `Invalid branch names:
2664
3525
  ${invalidBranches.slice(0, 10).join("\n ")}${invalidBranches.length > 10 ? `
2665
3526
  ... and ${invalidBranches.length - 10} more` : ""}`
3527
+ );
3528
+ }
3529
+ }
3530
+ }
3531
+ if (showCode) {
3532
+ if (!ciMode && showGit) {
3533
+ spinner5?.start("Analyzing codebase...");
3534
+ } else if (!ciMode) {
3535
+ spinner5?.start("Analyzing codebase...");
3536
+ }
3537
+ const codebaseMetrics = await analyzeCodebase(targetDir);
3538
+ complianceScores.push(codebaseMetrics.overallCompliance);
3539
+ spinner5?.stop("Codebase analysis complete");
3540
+ if (ciMode) {
3541
+ console.log("\n=== CODEBASE METRICS ===");
3542
+ console.log(`Files analyzed: ${codebaseMetrics.filesAnalyzed}`);
3543
+ console.log(`Overall compliance: ${codebaseMetrics.overallCompliance}%`);
3544
+ for (const [rule, compliance] of Object.entries(
3545
+ codebaseMetrics.complianceByRule
3546
+ )) {
3547
+ console.log(` ${rule}: ${compliance}%`);
3548
+ }
3549
+ } else {
3550
+ p4.note(formatCodebaseMetrics(codebaseMetrics), "Codebase Analysis");
3551
+ }
3552
+ }
3553
+ if (complianceScores.length > 0) {
3554
+ overallCompliance = Math.round(
3555
+ complianceScores.reduce((a, b) => a + b, 0) / complianceScores.length
2666
3556
  );
2667
3557
  }
2668
- const overallCompliance = Math.round(
2669
- (metrics.taskLinkCompliance + metrics.branchCompliance) / 2
2670
- );
3558
+ if (ciMode) {
3559
+ console.log(`
3560
+ OVERALL COMPLIANCE: ${overallCompliance}%`);
3561
+ console.log(`THRESHOLD: ${threshold}%`);
3562
+ if (overallCompliance < threshold) {
3563
+ console.log(
3564
+ `
3565
+ FAILED: Compliance ${overallCompliance}% is below threshold ${threshold}%`
3566
+ );
3567
+ process.exit(1);
3568
+ } else {
3569
+ console.log(`
3570
+ PASSED: Compliance meets threshold`);
3571
+ process.exit(0);
3572
+ }
3573
+ }
2671
3574
  if (overallCompliance >= 90) {
2672
3575
  p4.outro(pc4.green("\u2713 Excellent compliance! Keep up the good work."));
2673
3576
  } else if (overallCompliance >= 70) {
@@ -2677,10 +3580,60 @@ ${pc4.bold("Branches")}
2677
3580
  }
2678
3581
  }
2679
3582
 
3583
+ // src/commands/install-commands.ts
3584
+ import * as p5 from "@clack/prompts";
3585
+ import pc5 from "picocolors";
3586
+ async function runInstallCommands(targetDir = process.cwd()) {
3587
+ p5.intro(pc5.cyan("RaftStack: Install Claude Code commands and skills"));
3588
+ const spinner5 = p5.spinner();
3589
+ spinner5.start("Installing Claude Code commands and skills...");
3590
+ try {
3591
+ const commandsResult = await generateClaudeCommands(targetDir);
3592
+ const skillsResult = await generateClaudeSkills(targetDir);
3593
+ spinner5.stop("Claude Code commands and skills installed!");
3594
+ const created = [...commandsResult.created, ...skillsResult.created];
3595
+ const backedUp = [...commandsResult.backedUp, ...skillsResult.backedUp];
3596
+ console.log();
3597
+ if (created.length > 0) {
3598
+ p5.log.success(pc5.green("Installed files:"));
3599
+ for (const file of created) {
3600
+ console.log(` ${pc5.dim("+")} ${file}`);
3601
+ }
3602
+ }
3603
+ if (backedUp.length > 0) {
3604
+ console.log();
3605
+ p5.log.info(pc5.dim("Backed up existing files:"));
3606
+ for (const file of backedUp) {
3607
+ console.log(` ${pc5.dim("\u2192")} ${file}.backup`);
3608
+ }
3609
+ }
3610
+ console.log();
3611
+ p5.note(
3612
+ [
3613
+ `${pc5.cyan("/raftstack/init-context")} - Analyze codebase, generate constitution`,
3614
+ `${pc5.cyan("/raftstack/shape")} - Plan features with adaptive depth`,
3615
+ `${pc5.cyan("/raftstack/discover")} - Extract patterns into standards`,
3616
+ `${pc5.cyan("/raftstack/inject")} - Surface relevant context for tasks`,
3617
+ `${pc5.cyan("/raftstack/index")} - Maintain standards registry`
3618
+ ].join("\n"),
3619
+ "Available Commands"
3620
+ );
3621
+ p5.outro(pc5.green("Ready to use! Try /raftstack/init-context to get started."));
3622
+ } catch (error) {
3623
+ spinner5.stop("Error installing commands");
3624
+ p5.log.error(
3625
+ pc5.red(
3626
+ `Error: ${error instanceof Error ? error.message : "Unknown error"}`
3627
+ )
3628
+ );
3629
+ process.exit(1);
3630
+ }
3631
+ }
3632
+
2680
3633
  // package.json
2681
3634
  var package_default = {
2682
3635
  name: "@raftlabs/raftstack",
2683
- version: "1.7.2",
3636
+ version: "1.9.1",
2684
3637
  description: "CLI tool for setting up Git hooks, commit conventions, and GitHub integration",
2685
3638
  type: "module",
2686
3639
  main: "./dist/index.js",
@@ -2697,7 +3650,8 @@ var package_default = {
2697
3650
  files: [
2698
3651
  "dist",
2699
3652
  "templates",
2700
- ".claude/skills"
3653
+ ".claude/skills",
3654
+ ".claude/commands"
2701
3655
  ],
2702
3656
  scripts: {
2703
3657
  build: "tsup",
@@ -2770,8 +3724,23 @@ program.command("init").description("Initialize RaftStack configuration in your
2770
3724
  program.command("setup-protection").description("Configure GitHub branch protection rules via API").action(async () => {
2771
3725
  await runSetupProtection(process.cwd());
2772
3726
  });
2773
- program.command("metrics").description("Analyze repository compliance with RaftStack conventions").action(async () => {
2774
- await runMetrics(process.cwd());
3727
+ 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(
3728
+ "--threshold <n>",
3729
+ "Minimum compliance percentage (default: 70)",
3730
+ "70"
3731
+ ).option("--days <n>", "Time period in days (default: 30 in CI mode)").action(
3732
+ async (options) => {
3733
+ await runMetrics(process.cwd(), {
3734
+ git: options.git,
3735
+ code: options.code,
3736
+ ci: options.ci,
3737
+ threshold: options.threshold ? parseInt(options.threshold, 10) : 70,
3738
+ days: options.days ? parseInt(options.days, 10) : void 0
3739
+ });
3740
+ }
3741
+ );
3742
+ program.command("install-commands").description("Install or update Claude Code commands and skills").action(async () => {
3743
+ await runInstallCommands(process.cwd());
2775
3744
  });
2776
3745
  program.parse();
2777
3746
  //# sourceMappingURL=cli.js.map