commit-sense-cli 1.0.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/README.md ADDED
@@ -0,0 +1,43 @@
1
+ # CommitSense
2
+
3
+ CommitSense helps developers write accurate, meaningful, and semantically correct Conventional Commits by analyzing diffs and suggesting the appropriate commit type and scope.
4
+
5
+ ## Features
6
+
7
+ - **Fast & Local**: Runs entirely on your machine.
8
+ - **Smart Suggestions**: Analyzes staged files to suggest commit types (feat, fix, chore, etc.).
9
+ - **Advanced Analysis** :
10
+ - **Dependency Checks**: Detects `chore` for `package.json` updates.
11
+ - **Code Aware**: Detects `feat` for new exports and `refactor` for removed exports.
12
+ - **Scope Detection**: Intelligent scope suggestion based on directory structure (prioritizing feature folders over utility folders).
13
+ - **Zero Config**: Works out of the box for most projects.
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install -g commit-sense
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Stage your changes:
24
+
25
+ ```bash
26
+ git add .
27
+ ```
28
+
29
+ Run CommitSense instead of `git commit`:
30
+
31
+ ```bash
32
+ commit-sense
33
+ ```
34
+
35
+ Follow the interactive prompts to confirm or edit the commit message.
36
+
37
+ ## Development
38
+
39
+ 1. Clone the repo
40
+ 2. Install dependencies: `npm install`
41
+ 3. Build: `npm run build`
42
+ 4. Run locally: `npm run dev` (or `node bin/commitsense.js`)
43
+
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../dist/index.js');
@@ -0,0 +1,16 @@
1
+ import { CommitType } from './classifier';
2
+ export interface AnalysisResult {
3
+ type: CommitType;
4
+ confidence: number;
5
+ reason: string;
6
+ }
7
+ export interface FileChange {
8
+ path: string;
9
+ diff: string;
10
+ }
11
+ export interface Analyzer {
12
+ analyze(fileChanges: FileChange[]): Promise<AnalysisResult[]>;
13
+ }
14
+ export declare class DependencyAnalyzer implements Analyzer {
15
+ analyze(fileChanges: FileChange[]): Promise<AnalysisResult[]>;
16
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DependencyAnalyzer = void 0;
4
+ class DependencyAnalyzer {
5
+ async analyze(fileChanges) {
6
+ const results = [];
7
+ for (const file of fileChanges) {
8
+ if (file.path.endsWith('package.json')) {
9
+ const diff = file.diff;
10
+ // Simple regex to check for dependency changes
11
+ // Look for lines adding/changing dependencies
12
+ // + "react": "^18.0.0"
13
+ const addedDeps = (diff.match(/^\+.*"(dependencies|devDependencies|peerDependencies)"/m));
14
+ const changedLine = (diff.match(/^\+.*:\s*".*"/mg));
15
+ if (addedDeps || changedLine) {
16
+ // Check if it is a devDependency (chore) or runtime dependency (fix/feat/chore depending on convention)
17
+ // For now, let's treat all dependency updates as 'chore' but with high confidence,
18
+ // unless we can detect it's a critical fix?
19
+ // Standard convention:
20
+ // valid: chore(deps): update react
21
+ // So 'chore' is safe.
22
+ results.push({
23
+ type: 'chore',
24
+ confidence: 0.9,
25
+ reason: `Detected dependency change in ${file.path}`
26
+ });
27
+ }
28
+ }
29
+ if (file.path.endsWith('package-lock.json') || file.path.endsWith('yarn.lock') || file.path.endsWith('pnpm-lock.yaml')) {
30
+ results.push({
31
+ type: 'chore',
32
+ confidence: 0.9,
33
+ reason: `Detected lockfile change in ${file.path}`
34
+ });
35
+ }
36
+ }
37
+ return results;
38
+ }
39
+ }
40
+ exports.DependencyAnalyzer = DependencyAnalyzer;
@@ -0,0 +1,8 @@
1
+ import { FileChange } from './analyzer';
2
+ export type CommitType = 'feat' | 'fix' | 'chore' | 'docs' | 'style' | 'refactor' | 'test' | 'ci';
3
+ export interface CommitClassification {
4
+ type: CommitType;
5
+ confidence: number;
6
+ reason: string;
7
+ }
8
+ export declare function classifyChanges(fileChanges: FileChange[]): Promise<CommitClassification>;
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyChanges = classifyChanges;
4
+ const analyzer_1 = require("./analyzer");
5
+ const codeAnalyzer_1 = require("./codeAnalyzer");
6
+ const simpleAnalyzer_1 = require("./simpleAnalyzer");
7
+ async function classifyChanges(fileChanges) {
8
+ const analyzers = [
9
+ new analyzer_1.DependencyAnalyzer(),
10
+ new codeAnalyzer_1.CodeAnalyzer(),
11
+ new simpleAnalyzer_1.SimpleAnalyzer()
12
+ ];
13
+ let bestResult = {
14
+ type: 'feat',
15
+ confidence: 0,
16
+ reason: 'Default'
17
+ };
18
+ for (const analyzer of analyzers) {
19
+ const results = await analyzer.analyze(fileChanges);
20
+ for (const result of results) {
21
+ if (result.confidence > bestResult.confidence) {
22
+ bestResult = result;
23
+ }
24
+ }
25
+ }
26
+ return bestResult;
27
+ }
@@ -0,0 +1,4 @@
1
+ import { Analyzer, AnalysisResult, FileChange } from './analyzer';
2
+ export declare class CodeAnalyzer implements Analyzer {
3
+ analyze(fileChanges: FileChange[]): Promise<AnalysisResult[]>;
4
+ }
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CodeAnalyzer = void 0;
4
+ class CodeAnalyzer {
5
+ async analyze(fileChanges) {
6
+ const results = [];
7
+ for (const file of fileChanges) {
8
+ // Only analyze source code files
9
+ if (!file.path.match(/\.(ts|js|jsx|tsx)$/))
10
+ continue;
11
+ if (file.path.endsWith('.d.ts'))
12
+ continue; // Skip type definitions for now? Or maybe they are important for API?
13
+ const diff = file.diff;
14
+ // Regex for detecting exported members
15
+ // This is a naive heuristic but works for MVP
16
+ const removedExportRegex = /^-.*export\s+(const|let|var|function|class|interface|type|enum)\s+([a-zA-Z0-9_$]+)/m;
17
+ const addedExportRegex = /^\+.*export\s+(const|let|var|function|class|interface|type|enum)\s+([a-zA-Z0-9_$]+)/m;
18
+ // Check for BREAKING CHANGES (removed exports)
19
+ // We need to be careful: if a line is modified, it appears as - then +, so we need to see if the same name is added back.
20
+ const removedExports = new Set();
21
+ const addedExports = new Set();
22
+ const lines = diff.split('\n');
23
+ for (const line of lines) {
24
+ const removedMatch = line.match(removedExportRegex);
25
+ if (removedMatch) {
26
+ removedExports.add(removedMatch[2]);
27
+ }
28
+ const addedMatch = line.match(addedExportRegex);
29
+ if (addedMatch) {
30
+ addedExports.add(addedMatch[2]);
31
+ }
32
+ }
33
+ // identify truly removed exports (not just modified lines)
34
+ for (const removed of removedExports) {
35
+ if (!addedExports.has(removed)) {
36
+ results.push({
37
+ type: 'refactor', // defaulting to refactor or feat? logic says removing api is breaking, usually requires 'feat!' or just footer. for type, maybe 'refactor' or 'chore'?
38
+ // Actually, breaking changes can be any type, but usually feat or fix.
39
+ // For the purpose of *type* classification:
40
+ // Removing API -> refactor (if cleanup) or feat (if breaking change logic)
41
+ // Let's stick to 'refactor' for now, but with proper breaking change detection we might want to flag it in footer.
42
+ confidence: 0.8,
43
+ reason: `Detected removal of exported symbol '${removed}' in ${file.path}`
44
+ });
45
+ }
46
+ }
47
+ // Identify NEW exports
48
+ for (const added of addedExports) {
49
+ if (!removedExports.has(added)) {
50
+ results.push({
51
+ type: 'feat',
52
+ confidence: 0.9,
53
+ reason: `Detected new exported symbol '${added}' in ${file.path}`
54
+ });
55
+ }
56
+ }
57
+ }
58
+ return results;
59
+ }
60
+ }
61
+ exports.CodeAnalyzer = CodeAnalyzer;
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Detects the scope of the commit based on the staged files.
3
+ * @param stagedFiles List of staged files
4
+ * @returns The detected scope, or an empty string if no clear scope found.
5
+ */
6
+ export declare function detectScope(stagedFiles: string[]): string;
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.detectScope = detectScope;
7
+ const path_1 = __importDefault(require("path"));
8
+ /**
9
+ * Detects the scope of the commit based on the staged files.
10
+ * @param stagedFiles List of staged files
11
+ * @returns The detected scope, or an empty string if no clear scope found.
12
+ */
13
+ function detectScope(stagedFiles) {
14
+ if (stagedFiles.length === 0)
15
+ return '';
16
+ const scopes = {};
17
+ for (const file of stagedFiles) {
18
+ const dirname = path_1.default.dirname(file);
19
+ if (dirname === '.')
20
+ continue; // Root files don't suggest specific scope usually
21
+ const parts = dirname.split(path_1.default.sep);
22
+ // Use the first directory as the scope (e.g., 'src' in 'src/utils/foo.ts')
23
+ // Or maybe the last significant folder?
24
+ // Let's try to be smart: if it's 'src/components/Button', scope is 'components' or 'Button'?
25
+ // Convention varies. Let's try the *first* folder for now (e.g. 'auth' in 'packages/auth')
26
+ // or the *last* folder if it's nested deep?
27
+ // Strategy:
28
+ // 1. If 'packages/*', use the package name.
29
+ // 2. If 'src/*', use the immediate subdirectory.
30
+ let candidate = '';
31
+ if (parts[0] === 'packages' && parts.length > 1) {
32
+ candidate = parts[1];
33
+ }
34
+ else if (parts[0] === 'src' && parts.length > 1) {
35
+ candidate = parts[1];
36
+ }
37
+ else {
38
+ candidate = parts[0];
39
+ }
40
+ if (candidate) {
41
+ scopes[candidate] = (scopes[candidate] || 0) + 1;
42
+ }
43
+ }
44
+ // Filter out blacklisted scopes if there are other candidates
45
+ const BLACKLIST = ['utils', 'helpers', 'common', 'shared', 'types', 'interfaces'];
46
+ // Calculate counts
47
+ let maxScope = '';
48
+ let maxCount = 0;
49
+ // First pass: try to find non-blacklisted scopes
50
+ const nonBlacklistedScopes = Object.entries(scopes).filter(([s]) => !BLACKLIST.includes(s));
51
+ const candidates = nonBlacklistedScopes.length > 0 ? nonBlacklistedScopes : Object.entries(scopes);
52
+ for (const [scope, count] of candidates) {
53
+ if (count > maxCount) {
54
+ maxCount = count;
55
+ maxScope = scope;
56
+ }
57
+ }
58
+ return maxScope;
59
+ }
@@ -0,0 +1,4 @@
1
+ import { Analyzer, AnalysisResult, FileChange } from './analyzer';
2
+ export declare class SimpleAnalyzer implements Analyzer {
3
+ analyze(fileChanges: FileChange[]): Promise<AnalysisResult[]>;
4
+ }
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.SimpleAnalyzer = void 0;
4
+ const EXTENSION_MAPPING = {
5
+ '.md': 'docs',
6
+ '.txt': 'docs',
7
+ '.spec.ts': 'test',
8
+ '.test.ts': 'test',
9
+ '.spec.js': 'test',
10
+ '.test.js': 'test',
11
+ '.json': 'chore',
12
+ '.yaml': 'chore',
13
+ '.yml': 'chore',
14
+ '.css': 'style',
15
+ '.scss': 'style',
16
+ '.less': 'style',
17
+ };
18
+ const FILENAME_MAPPING = {
19
+ 'package.json': 'chore',
20
+ 'package-lock.json': 'chore',
21
+ 'tsconfig.json': 'chore',
22
+ '.gitignore': 'chore',
23
+ '.eslintrc': 'chore',
24
+ '.prettierrc': 'chore',
25
+ 'README.md': 'docs',
26
+ 'LICENSE': 'chore',
27
+ 'Dockerfile': 'ci',
28
+ '.github': 'ci',
29
+ };
30
+ class SimpleAnalyzer {
31
+ async analyze(fileChanges) {
32
+ const typeCounts = {};
33
+ const total = fileChanges.length;
34
+ for (const file of fileChanges) {
35
+ const path = file.path;
36
+ const basename = path.split('/').pop() || '';
37
+ const ext = '.' + path.split('.').pop();
38
+ let type = 'feat'; // Default assumption
39
+ if (path.includes('test/') || path.includes('tests/')) {
40
+ type = 'test';
41
+ }
42
+ else if (path.startsWith('.github/') || path.includes('/.github/')) {
43
+ type = 'ci';
44
+ }
45
+ else if (path.startsWith('docs/')) {
46
+ type = 'docs';
47
+ }
48
+ else if (FILENAME_MAPPING[basename]) {
49
+ type = FILENAME_MAPPING[basename];
50
+ }
51
+ else {
52
+ for (const extKey in EXTENSION_MAPPING) {
53
+ if (path.endsWith(extKey)) {
54
+ type = EXTENSION_MAPPING[extKey];
55
+ break;
56
+ }
57
+ }
58
+ }
59
+ typeCounts[type] = (typeCounts[type] || 0) + 1;
60
+ }
61
+ // Return the majority vote as a single result with confidence
62
+ let maxType = 'feat';
63
+ let maxCount = 0;
64
+ for (const [t, count] of Object.entries(typeCounts)) {
65
+ if (count > maxCount) {
66
+ maxCount = count;
67
+ maxType = t;
68
+ }
69
+ }
70
+ const confidence = total > 0 ? (maxCount / total) * 0.5 : 0; // Lower confidence for simple extension matching (max 0.5)
71
+ return [{
72
+ type: maxType,
73
+ confidence,
74
+ reason: `Detected ${maxType} based on file extensions (${maxCount}/${total} files).`
75
+ }];
76
+ }
77
+ }
78
+ exports.SimpleAnalyzer = SimpleAnalyzer;
@@ -0,0 +1,7 @@
1
+ export type CommitType = 'feat' | 'fix' | 'chore' | 'docs' | 'style' | 'refactor' | 'test' | 'ci';
2
+ export interface CommitClassification {
3
+ type: CommitType;
4
+ confidence: number;
5
+ reason: string;
6
+ }
7
+ export declare function classifyChanges(stagedFiles: string[]): CommitClassification;
@@ -0,0 +1,76 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.classifyChanges = classifyChanges;
4
+ const EXTENSION_MAPPING = {
5
+ '.md': 'docs',
6
+ '.txt': 'docs',
7
+ '.spec.ts': 'test',
8
+ '.test.ts': 'test',
9
+ '.spec.js': 'test',
10
+ '.test.js': 'test',
11
+ '.json': 'chore',
12
+ '.yaml': 'chore',
13
+ '.yml': 'chore',
14
+ '.css': 'style',
15
+ '.scss': 'style',
16
+ '.less': 'style',
17
+ };
18
+ const FILENAME_MAPPING = {
19
+ 'package.json': 'chore',
20
+ 'package-lock.json': 'chore',
21
+ 'tsconfig.json': 'chore',
22
+ '.gitignore': 'chore',
23
+ '.eslintrc': 'chore',
24
+ '.prettierrc': 'chore',
25
+ 'README.md': 'docs',
26
+ 'LICENSE': 'chore',
27
+ 'Dockerfile': 'ci',
28
+ '.github': 'ci',
29
+ };
30
+ function classifyChanges(stagedFiles) {
31
+ const typeCounts = {};
32
+ for (const file of stagedFiles) {
33
+ const basename = file.split('/').pop() || '';
34
+ const ext = '.' + file.split('.').pop();
35
+ let type = 'feat'; // Default assumption
36
+ if (file.includes('test/') || file.includes('tests/')) {
37
+ type = 'test';
38
+ }
39
+ else if (file.startsWith('.github/') || file.includes('/.github/')) {
40
+ type = 'ci';
41
+ }
42
+ else if (file.startsWith('docs/')) {
43
+ type = 'docs';
44
+ }
45
+ else if (FILENAME_MAPPING[basename]) {
46
+ type = FILENAME_MAPPING[basename];
47
+ }
48
+ else {
49
+ // Check extensions, prioritize longer matches (e.g. .spec.ts over .ts if we had .ts)
50
+ for (const extKey in EXTENSION_MAPPING) {
51
+ if (file.endsWith(extKey)) {
52
+ type = EXTENSION_MAPPING[extKey];
53
+ break;
54
+ }
55
+ }
56
+ }
57
+ typeCounts[type] = (typeCounts[type] || 0) + 1;
58
+ }
59
+ // Find majority
60
+ let maxType = 'feat';
61
+ let maxCount = 0;
62
+ for (const [t, count] of Object.entries(typeCounts)) {
63
+ if (count > maxCount) {
64
+ maxCount = count;
65
+ maxType = t;
66
+ }
67
+ }
68
+ // Simple confidence logic
69
+ const total = stagedFiles.length;
70
+ const confidence = total > 0 ? maxCount / total : 0;
71
+ return {
72
+ type: maxType,
73
+ confidence,
74
+ reason: `Detected ${maxType} based on ${maxCount}/${total} files.`
75
+ };
76
+ }
package/dist/git.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Get a list of currently staged files.
3
+ * @returns Array of file paths (relative to repo root)
4
+ */
5
+ export declare function getStagedFiles(): Promise<string[]>;
6
+ /**
7
+ * Get the diff of a specific staged file.
8
+ * @param filePath Relative path to the file
9
+ */
10
+ export declare function getStagedFileDiff(filePath: string): Promise<string>;
package/dist/git.js ADDED
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getStagedFiles = getStagedFiles;
7
+ exports.getStagedFileDiff = getStagedFileDiff;
8
+ const simple_git_1 = __importDefault(require("simple-git"));
9
+ const git = (0, simple_git_1.default)();
10
+ /**
11
+ * Get a list of currently staged files.
12
+ * @returns Array of file paths (relative to repo root)
13
+ */
14
+ async function getStagedFiles() {
15
+ try {
16
+ const diff = await git.diff(['--cached', '--name-only', '--diff-filter=ACMR']);
17
+ return diff.split('\n').filter(Boolean).map(f => f.trim());
18
+ }
19
+ catch (error) {
20
+ console.error('Error getting staged files:', error);
21
+ process.exit(1);
22
+ }
23
+ }
24
+ /**
25
+ * Get the diff of a specific staged file.
26
+ * @param filePath Relative path to the file
27
+ */
28
+ async function getStagedFileDiff(filePath) {
29
+ try {
30
+ return await git.diff(['--cached', filePath]);
31
+ }
32
+ catch (error) {
33
+ console.error(`Error getting diff for ${filePath}:`, error);
34
+ return '';
35
+ }
36
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,116 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const chalk_1 = __importDefault(require("chalk"));
7
+ const prompts_1 = __importDefault(require("prompts"));
8
+ const git_1 = require("./utils/git");
9
+ const classifier_1 = require("./analysis/classifier");
10
+ const scope_1 = require("./analysis/scope");
11
+ const child_process_1 = require("child_process");
12
+ async function main() {
13
+ console.log(chalk_1.default.cyan('CommitSense - Smart Commit Wizard'));
14
+ // 1. Get staged files
15
+ const stagedFiles = await (0, git_1.getStagedFiles)();
16
+ if (stagedFiles.length === 0) {
17
+ console.log(chalk_1.default.yellow('No staged files found. Please stage your changes first.'));
18
+ process.exit(0);
19
+ }
20
+ console.log(chalk_1.default.gray(`Found ${stagedFiles.length} staged file(s):`));
21
+ stagedFiles.slice(0, 5).forEach(f => console.log(chalk_1.default.gray(` - ${f}`)));
22
+ if (stagedFiles.length > 5)
23
+ console.log(chalk_1.default.gray(` ... and ${stagedFiles.length - 5} more.`));
24
+ // 2. Analyze
25
+ const fileChanges = await Promise.all(stagedFiles.map(async (file) => ({
26
+ path: file,
27
+ diff: await (0, git_1.getStagedFileDiff)(file)
28
+ })));
29
+ const classification = await (0, classifier_1.classifyChanges)(fileChanges);
30
+ const suggestedScope = (0, scope_1.detectScope)(stagedFiles);
31
+ // 3. Prompt
32
+ const response = await (0, prompts_1.default)([
33
+ {
34
+ type: 'select',
35
+ name: 'type',
36
+ message: 'Select commit type:',
37
+ choices: [
38
+ { title: 'feat', value: 'feat', description: 'A new feature' },
39
+ { title: 'fix', value: 'fix', description: 'A bug fix' },
40
+ { title: 'chore', value: 'chore', description: 'Build process or auxiliary tool changes' },
41
+ { title: 'docs', value: 'docs', description: 'Documentation only changes' },
42
+ { title: 'style', value: 'style', description: 'Markup, white-space, formatting, missing semi-colons...' },
43
+ { title: 'refactor', value: 'refactor', description: 'A code change that neither fixes a bug or adds a feature' },
44
+ { title: 'perf', value: 'perf', description: 'A code change that improves performance' },
45
+ { title: 'test', value: 'test', description: 'Adding missing tests' },
46
+ { title: 'ci', value: 'ci', description: 'CI related changes' },
47
+ ],
48
+ initial: ['feat', 'fix', 'chore', 'docs', 'style', 'refactor', 'perf', 'test', 'ci'].indexOf(classification.type)
49
+ },
50
+ {
51
+ type: 'text',
52
+ name: 'scope',
53
+ message: 'Scope (optional):',
54
+ initial: suggestedScope
55
+ },
56
+ {
57
+ type: 'text',
58
+ name: 'description',
59
+ message: 'Short description:',
60
+ validate: (value) => value.length > 0 ? true : 'Description is required'
61
+ },
62
+ {
63
+ type: 'text',
64
+ name: 'body',
65
+ message: 'Long description (optional):',
66
+ },
67
+ {
68
+ type: 'text',
69
+ name: 'footer',
70
+ message: 'Footer (optional, e.g. BREAKING CHANGE):',
71
+ },
72
+ {
73
+ type: 'confirm',
74
+ name: 'confirm',
75
+ message: (prev, values) => {
76
+ const scopePart = values.scope ? `(${values.scope})` : '';
77
+ const title = `${values.type}${scopePart}: ${values.description}`;
78
+ console.log(chalk_1.default.green('\nPreview:'));
79
+ console.log(chalk_1.default.bold(title));
80
+ if (values.body)
81
+ console.log(`\n${values.body}`);
82
+ if (values.footer)
83
+ console.log(`\n${values.footer}`);
84
+ return 'Commit now?';
85
+ },
86
+ initial: true
87
+ }
88
+ ]);
89
+ if (!response.confirm) {
90
+ console.log(chalk_1.default.yellow('Commit cancelled.'));
91
+ process.exit(0);
92
+ }
93
+ // 4. Construct Commit Message
94
+ const scopePart = response.scope ? `(${response.scope})` : '';
95
+ let commitMsg = `${response.type}${scopePart}: ${response.description}`;
96
+ if (response.body) {
97
+ commitMsg += `\n\n${response.body}`;
98
+ }
99
+ if (response.footer) {
100
+ commitMsg += `\n\n${response.footer}`;
101
+ }
102
+ // 5. Execute Commit
103
+ const child = (0, child_process_1.spawn)('git', ['commit', '-m', commitMsg], { stdio: 'inherit' });
104
+ child.on('close', (code) => {
105
+ if (code === 0) {
106
+ console.log(chalk_1.default.green('Commit successful!'));
107
+ }
108
+ else {
109
+ console.log(chalk_1.default.red(`Commit failed with code ${code}`));
110
+ }
111
+ });
112
+ }
113
+ main().catch(err => {
114
+ console.error(err);
115
+ process.exit(1);
116
+ });
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Detects the scope of the commit based on the staged files.
3
+ * @param stagedFiles List of staged files
4
+ * @returns The detected scope, or an empty string if no clear scope found.
5
+ */
6
+ export declare function detectScope(stagedFiles: string[]): string;
package/dist/scope.js ADDED
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.detectScope = detectScope;
7
+ const path_1 = __importDefault(require("path"));
8
+ /**
9
+ * Detects the scope of the commit based on the staged files.
10
+ * @param stagedFiles List of staged files
11
+ * @returns The detected scope, or an empty string if no clear scope found.
12
+ */
13
+ function detectScope(stagedFiles) {
14
+ if (stagedFiles.length === 0)
15
+ return '';
16
+ const scopes = {};
17
+ for (const file of stagedFiles) {
18
+ const dirname = path_1.default.dirname(file);
19
+ if (dirname === '.')
20
+ continue; // Root files don't suggest specific scope usually
21
+ const parts = dirname.split(path_1.default.sep);
22
+ // Use the first directory as the scope (e.g., 'src' in 'src/utils/foo.ts')
23
+ // Or maybe the last significant folder?
24
+ // Let's try to be smart: if it's 'src/components/Button', scope is 'components' or 'Button'?
25
+ // Convention varies. Let's try the *first* folder for now (e.g. 'auth' in 'packages/auth')
26
+ // or the *last* folder if it's nested deep?
27
+ // Strategy:
28
+ // 1. If 'packages/*', use the package name.
29
+ // 2. If 'src/*', use the immediate subdirectory.
30
+ let candidate = '';
31
+ if (parts[0] === 'packages' && parts.length > 1) {
32
+ candidate = parts[1];
33
+ }
34
+ else if (parts[0] === 'src' && parts.length > 1) {
35
+ candidate = parts[1];
36
+ }
37
+ else {
38
+ candidate = parts[0];
39
+ }
40
+ if (candidate) {
41
+ scopes[candidate] = (scopes[candidate] || 0) + 1;
42
+ }
43
+ }
44
+ // Find the most frequent scope
45
+ let maxScope = '';
46
+ let maxCount = 0;
47
+ for (const [scope, count] of Object.entries(scopes)) {
48
+ if (count > maxCount) {
49
+ maxCount = count;
50
+ maxScope = scope;
51
+ }
52
+ }
53
+ return maxScope;
54
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Get a list of currently staged files.
3
+ * @returns Array of file paths (relative to repo root)
4
+ */
5
+ export declare function getStagedFiles(): Promise<string[]>;
6
+ /**
7
+ * Get the diff of a specific staged file.
8
+ * @param filePath Relative path to the file
9
+ */
10
+ export declare function getStagedFileDiff(filePath: string): Promise<string>;
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.getStagedFiles = getStagedFiles;
7
+ exports.getStagedFileDiff = getStagedFileDiff;
8
+ const simple_git_1 = __importDefault(require("simple-git"));
9
+ const git = (0, simple_git_1.default)();
10
+ /**
11
+ * Get a list of currently staged files.
12
+ * @returns Array of file paths (relative to repo root)
13
+ */
14
+ async function getStagedFiles() {
15
+ try {
16
+ const diff = await git.diff(['--cached', '--name-only', '--diff-filter=ACMR']);
17
+ return diff.split('\n').filter(Boolean).map(f => f.trim());
18
+ }
19
+ catch (error) {
20
+ console.error('Error getting staged files:', error);
21
+ process.exit(1);
22
+ }
23
+ }
24
+ /**
25
+ * Get the diff of a specific staged file.
26
+ * @param filePath Relative path to the file
27
+ */
28
+ async function getStagedFileDiff(filePath) {
29
+ try {
30
+ return await git.diff(['--cached', filePath]);
31
+ }
32
+ catch (error) {
33
+ console.error('Error getting diff for:', filePath, error);
34
+ return '';
35
+ }
36
+ }
@@ -0,0 +1,119 @@
1
+ # CommitSense Roadmap
2
+
3
+ ## Vision
4
+
5
+ CommitSense helps developers write accurate, meaningful, and
6
+ semantically correct Conventional Commits by analyzing diffs and
7
+ suggesting the appropriate commit type and scope.
8
+
9
+ ------------------------------------------------------------------------
10
+
11
+ ## Phase 1 -- MVP (Local npm CLI)
12
+
13
+ ### Goals
14
+
15
+ - Fast execution (\<300ms)
16
+ - Deterministic rule-based classification
17
+ - Zero-config setup
18
+ - Git hook integration
19
+
20
+ ### Core Features
21
+
22
+ - Read staged diff
23
+ - Detect changed file types
24
+ - Classify commit type (feat, fix, chore, docs, test, style, refactor)
25
+ - Detect scope from directory structure
26
+ - Suggest improved Conventional Commit message
27
+ - Interactive confirmation before commit
28
+
29
+ ### Tech Stack
30
+
31
+ - Node.js + TypeScript
32
+ - simple-git or native child_process
33
+ - diff parser
34
+ - Optional lightweight AST (Babel) for export detection
35
+
36
+ ------------------------------------------------------------------------
37
+
38
+ ## Phase 2 -- Advanced Classification
39
+
40
+ ### Goals
41
+
42
+ - Improve semantic accuracy
43
+ - Detect breaking changes
44
+ - Detect behavior changes vs refactor
45
+
46
+ ### Features
47
+
48
+ - Detect added/removed exports
49
+ - Detect API contract changes
50
+ - Detect dependency version updates
51
+ - Breaking change suggestion (feat!)
52
+ - Scope mismatch detection
53
+
54
+ ------------------------------------------------------------------------
55
+
56
+ ## Phase 3 -- GitHub Action
57
+
58
+ ### Goals
59
+
60
+ - CI integration
61
+ - PR-level validation
62
+
63
+ ### Features
64
+
65
+ - Validate all commits in PR
66
+ - Comment suggested corrections
67
+ - Commit quality score
68
+ - Flag risky commits
69
+
70
+ ------------------------------------------------------------------------
71
+
72
+ ## Phase 4 -- SaaS (CommitSense Cloud)
73
+
74
+ ### Goals
75
+
76
+ - Team-level insights
77
+ - Release hygiene monitoring
78
+ - Commit analytics dashboard
79
+
80
+ ### Features
81
+
82
+ - Commit quality trends
83
+ - Breaking change history
84
+ - Module-level commit insights
85
+ - Team commit discipline score
86
+ - Slack/Discord notifications
87
+
88
+ ------------------------------------------------------------------------
89
+
90
+ ## Long-Term Expansion Ideas
91
+
92
+ - AI-assisted diff explanation (optional premium feature)
93
+ - Automatic semantic version bump suggestion
94
+ - Release notes auto-generation
95
+ - Architecture-aware scope validation
96
+
97
+ ------------------------------------------------------------------------
98
+
99
+ ## Success Metrics
100
+
101
+ - \< 1 second runtime locally
102
+ - Low false-positive rate
103
+ - Easy installation via npm
104
+ - High adoption in open-source projects
105
+
106
+ ------------------------------------------------------------------------
107
+
108
+ ## First Milestone Checklist
109
+
110
+ - [ ] Initialize TypeScript CLI project
111
+ - [ ] Implement staged diff reader
112
+ - [ ] Implement basic commit type classifier
113
+ - [ ] Implement scope detection logic
114
+ - [ ] Add interactive CLI prompt
115
+ - [ ] Publish v0.1.0 to npm
116
+
117
+ ------------------------------------------------------------------------
118
+
119
+ Built with clarity, speed, and developer experience in mind.
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "commit-sense-cli",
3
+ "version": "1.0.1",
4
+ "description": "",
5
+ "main": "dist/index.js",
6
+ "bin": {
7
+ "commit-sense-cli": "bin/commitsense.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "bin",
12
+ "README.md",
13
+ "LICENSE",
14
+ "docs"
15
+ ],
16
+ "directories": {
17
+ "doc": "docs"
18
+ },
19
+ "scripts": {
20
+ "test": "jest",
21
+ "build": "tsc",
22
+ "dev": "ts-node src/index.ts"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/abhishekpanda0620/commit-sense.git"
27
+ },
28
+ "keywords": [],
29
+ "author": "",
30
+ "license": "ISC",
31
+ "type": "commonjs",
32
+ "bugs": {
33
+ "url": "https://github.com/abhishekpanda0620/commit-sense/issues"
34
+ },
35
+ "homepage": "https://github.com/abhishekpanda0620/commit-sense#readme",
36
+ "dependencies": {
37
+ "@types/node": "^25.2.3",
38
+ "chalk": "^5.6.2",
39
+ "commander": "^14.0.3",
40
+ "prompts": "^2.4.2",
41
+ "simple-git": "^3.30.0",
42
+ "ts-node": "^10.9.2",
43
+ "typescript": "^5.9.3"
44
+ },
45
+ "devDependencies": {
46
+ "@types/jest": "^30.0.0",
47
+ "@types/prompts": "^2.4.9",
48
+ "jest": "^30.2.0",
49
+ "ts-jest": "^29.4.6"
50
+ }
51
+ }