@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/.claude/commands/raftstack/discover.md +205 -0
- package/.claude/commands/raftstack/help.md +309 -0
- package/.claude/commands/raftstack/index.md +259 -0
- package/.claude/commands/raftstack/init-context.md +221 -0
- package/.claude/commands/raftstack/inject.md +244 -0
- package/.claude/commands/raftstack/shape.md +253 -0
- package/README.md +40 -358
- package/README.npm.md +18 -8
- package/dist/cli.js +1196 -227
- package/dist/cli.js.map +1 -1
- package/package.json +3 -2
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) {
|
|
@@ -1646,14 +1650,65 @@ async function generateClaudeSkills(targetDir, options) {
|
|
|
1646
1650
|
return result;
|
|
1647
1651
|
}
|
|
1648
1652
|
|
|
1649
|
-
// src/generators/
|
|
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
|
|
1707
|
+
import { join as join17 } from "path";
|
|
1653
1708
|
async function hasReact(targetDir) {
|
|
1654
1709
|
try {
|
|
1655
|
-
const pkgPath =
|
|
1656
|
-
if (
|
|
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
|
|
1667
|
-
|
|
1668
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
1788
|
-
|
|
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 (
|
|
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 (
|
|
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.
|
|
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
|
-
|
|
1904
|
-
|
|
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.
|
|
2006
|
+
result.backedUp.push("eslint.config.mjs");
|
|
1908
2007
|
}
|
|
1909
|
-
result.created.push("eslint.config.
|
|
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
|
|
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 =
|
|
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
|
|
2058
|
-
import { join as
|
|
2463
|
+
import { existsSync as existsSync8 } from "fs";
|
|
2464
|
+
import { join as join20 } from "path";
|
|
2059
2465
|
async function isGitRepo(targetDir = process.cwd()) {
|
|
2060
|
-
if (
|
|
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
|
-
|
|
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
|
|
2191
|
-
|
|
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
|
-
|
|
2640
|
+
spinner5.stop("Configuration files generated!");
|
|
2225
2641
|
} catch (error) {
|
|
2226
|
-
|
|
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
|
|
2375
|
-
|
|
2790
|
+
const spinner5 = p3.spinner();
|
|
2791
|
+
spinner5.start("Checking GitHub CLI...");
|
|
2376
2792
|
const ghAvailable = await isGhCliAvailable();
|
|
2377
2793
|
if (!ghAvailable) {
|
|
2378
|
-
|
|
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
|
-
|
|
2392
|
-
|
|
2807
|
+
spinner5.stop("GitHub CLI ready");
|
|
2808
|
+
spinner5.start("Getting repository info...");
|
|
2393
2809
|
const repoInfo = await getGitHubRepoInfo(targetDir);
|
|
2394
2810
|
if (!repoInfo) {
|
|
2395
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2911
|
+
spinner5.stop("Merge strategy configured!");
|
|
2496
2912
|
} catch (error) {
|
|
2497
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2927
|
+
spinner5.stop(`Protected: ${pc3.green(branch)}`);
|
|
2512
2928
|
} catch (error) {
|
|
2513
2929
|
failedBranches.push(branch);
|
|
2514
|
-
|
|
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
|
|
2962
|
+
import { execa as execa4 } from "execa";
|
|
2547
2963
|
import * as p4 from "@clack/prompts";
|
|
2548
2964
|
import pc4 from "picocolors";
|
|
2549
|
-
|
|
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
|
-
["
|
|
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
|
-
|
|
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
|
|
3253
|
+
const { stdout } = await execa4(
|
|
2564
3254
|
"git",
|
|
2565
3255
|
[
|
|
2566
3256
|
"log",
|
|
2567
3257
|
`--since=${days} days ago`,
|
|
2568
|
-
"--format=%B---
|
|
3258
|
+
"--format=%H|%an|%ae|%s---BODY---%B---END---",
|
|
2569
3259
|
"--no-merges"
|
|
2570
3260
|
],
|
|
2571
3261
|
{ cwd: targetDir }
|
|
2572
3262
|
);
|
|
2573
|
-
|
|
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
|
|
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
|
|
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;
|
|
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
|
-
|
|
2622
|
-
|
|
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
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
p4.
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
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: ${
|
|
2653
|
-
Valid naming: ${
|
|
2654
|
-
Invalid naming: ${
|
|
2655
|
-
Compliance: ${formatCompliance(
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
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
|
-
|
|
2669
|
-
(
|
|
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.
|
|
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").
|
|
2774
|
-
|
|
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
|