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 +43 -0
- package/bin/commitsense.js +2 -0
- package/dist/analysis/analyzer.d.ts +16 -0
- package/dist/analysis/analyzer.js +40 -0
- package/dist/analysis/classifier.d.ts +8 -0
- package/dist/analysis/classifier.js +27 -0
- package/dist/analysis/codeAnalyzer.d.ts +4 -0
- package/dist/analysis/codeAnalyzer.js +61 -0
- package/dist/analysis/scope.d.ts +6 -0
- package/dist/analysis/scope.js +59 -0
- package/dist/analysis/simpleAnalyzer.d.ts +4 -0
- package/dist/analysis/simpleAnalyzer.js +78 -0
- package/dist/classifier.d.ts +7 -0
- package/dist/classifier.js +76 -0
- package/dist/git.d.ts +10 -0
- package/dist/git.js +36 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +116 -0
- package/dist/scope.d.ts +6 -0
- package/dist/scope.js +54 -0
- package/dist/utils/git.d.ts +10 -0
- package/dist/utils/git.js +36 -0
- package/docs/ROADMAP.md +119 -0
- package/package.json +51 -0
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,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,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,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,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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
});
|
package/dist/scope.d.ts
ADDED
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
|
+
}
|
package/docs/ROADMAP.md
ADDED
|
@@ -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
|
+
}
|