@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.
- package/dist/cli.js +1066 -203
- package/dist/cli.js.map +1 -1
- 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
|
|
500
|
-
|
|
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
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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
|
|
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: '
|
|
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
|
-
|
|
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
|
|
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 =
|
|
690
|
+
const configPath = join5(targetDir, "commitlint.config.js");
|
|
720
691
|
const writeResult = await writeFileSafe(
|
|
721
692
|
configPath,
|
|
722
|
-
|
|
693
|
+
getCommitlintConfig(asanaBaseUrl),
|
|
723
694
|
{ backup: true }
|
|
724
695
|
);
|
|
725
696
|
if (writeResult.created) {
|
|
726
|
-
result.created.push(".
|
|
697
|
+
result.created.push("commitlint.config.js");
|
|
727
698
|
if (writeResult.backedUp) {
|
|
728
699
|
result.backedUp.push(writeResult.backedUp);
|
|
729
700
|
}
|
|
730
701
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
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 (
|
|
738
|
-
result.created.push("
|
|
739
|
-
if (
|
|
740
|
-
result.backedUp.push(
|
|
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
|
|
1667
|
-
|
|
1668
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
1788
|
-
|
|
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 (
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
1904
|
-
|
|
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.
|
|
1955
|
+
result.backedUp.push("eslint.config.mjs");
|
|
1908
1956
|
}
|
|
1909
|
-
result.created.push("eslint.config.
|
|
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
|
|
2413
|
+
import { join as join19 } from "path";
|
|
2059
2414
|
async function isGitRepo(targetDir = process.cwd()) {
|
|
2060
|
-
if (existsSync7(
|
|
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
|
-
|
|
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
|
|
2910
|
+
import { execa as execa4 } from "execa";
|
|
2547
2911
|
import * as p4 from "@clack/prompts";
|
|
2548
2912
|
import pc4 from "picocolors";
|
|
2549
|
-
|
|
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
|
-
["
|
|
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
|
-
|
|
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
|
|
3201
|
+
const { stdout } = await execa4(
|
|
2564
3202
|
"git",
|
|
2565
3203
|
[
|
|
2566
3204
|
"log",
|
|
2567
3205
|
`--since=${days} days ago`,
|
|
2568
|
-
"--format=%B---
|
|
3206
|
+
"--format=%H|%an|%ae|%s---BODY---%B---END---",
|
|
2569
3207
|
"--no-merges"
|
|
2570
3208
|
],
|
|
2571
3209
|
{ cwd: targetDir }
|
|
2572
3210
|
);
|
|
2573
|
-
|
|
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
|
|
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
|
|
2595
|
-
|
|
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
|
-
|
|
2622
|
-
|
|
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
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
p4.
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
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: ${
|
|
2653
|
-
Valid naming: ${
|
|
2654
|
-
Invalid naming: ${
|
|
2655
|
-
Compliance: ${formatCompliance(
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
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
|
-
|
|
2669
|
-
(
|
|
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.
|
|
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").
|
|
2774
|
-
|
|
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
|