ai-cmg 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +238 -32
- package/package.json +4 -3
package/dist/index.js
CHANGED
|
@@ -1,22 +1,214 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { execSync, spawnSync } from 'child_process';
|
|
3
|
+
import { readFileSync } from 'fs';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
3
6
|
import prompts from 'prompts';
|
|
4
|
-
import pc from 'picocolors';
|
|
5
7
|
import clipboardy from 'clipboardy';
|
|
6
8
|
import Conf from 'conf';
|
|
7
9
|
const WORKER_URL = 'https://commit.d-code.workers.dev';
|
|
8
10
|
const config = new Conf({ projectName: 'ai-cmg' });
|
|
11
|
+
const MEDIA_EXTENSIONS = new Set([
|
|
12
|
+
'.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.bmp', '.tiff',
|
|
13
|
+
'.mp4', '.mov', '.webm', '.mp3', '.wav', '.flac', '.ogg',
|
|
14
|
+
'.pdf', '.zip', '.tar', '.gz', '.7z', '.rar', '.woff', '.woff2', '.ttf', '.eot',
|
|
15
|
+
]);
|
|
16
|
+
const LOCK_FILES = new Set([
|
|
17
|
+
'package-lock.json',
|
|
18
|
+
'npm-shrinkwrap.json',
|
|
19
|
+
'yarn.lock',
|
|
20
|
+
'pnpm-lock.yaml',
|
|
21
|
+
'bun.lockb',
|
|
22
|
+
]);
|
|
23
|
+
const GENERATED_PREFIXES = ['dist/', 'build/', '.next/', 'coverage/', 'node_modules/'];
|
|
24
|
+
const MAX_BLOCK_LINES = 400;
|
|
25
|
+
const MAX_BLOCK_CHARS = 20000;
|
|
26
|
+
function getPackageVersion() {
|
|
27
|
+
try {
|
|
28
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
29
|
+
const currentDir = dirname(currentFile);
|
|
30
|
+
const packageJsonPath = join(currentDir, '..', 'package.json');
|
|
31
|
+
const raw = readFileSync(packageJsonPath, 'utf8');
|
|
32
|
+
const parsed = JSON.parse(raw);
|
|
33
|
+
return parsed.version ?? null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function getNameStatus() {
|
|
40
|
+
const map = new Map();
|
|
41
|
+
try {
|
|
42
|
+
const output = execSync('git diff --cached --name-status').toString();
|
|
43
|
+
output.split('\n').forEach((line) => {
|
|
44
|
+
if (!line.trim())
|
|
45
|
+
return;
|
|
46
|
+
const [status, ...rest] = line.split('\t');
|
|
47
|
+
const filePath = rest[rest.length - 1];
|
|
48
|
+
if (filePath)
|
|
49
|
+
map.set(filePath, status);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// ignore status errors; diff will still be used
|
|
54
|
+
}
|
|
55
|
+
return map;
|
|
56
|
+
}
|
|
57
|
+
function getChangedFiles() {
|
|
58
|
+
try {
|
|
59
|
+
const output = execSync('git status --porcelain -z').toString();
|
|
60
|
+
if (!output)
|
|
61
|
+
return [];
|
|
62
|
+
const parts = output.split('\0').filter(Boolean);
|
|
63
|
+
const entries = [];
|
|
64
|
+
for (let i = 0; i < parts.length; i += 1) {
|
|
65
|
+
const entry = parts[i];
|
|
66
|
+
const status = entry.slice(0, 2);
|
|
67
|
+
let path = entry.slice(3);
|
|
68
|
+
if (status.startsWith('R') || status.startsWith('C')) {
|
|
69
|
+
const newPath = parts[i + 1];
|
|
70
|
+
if (newPath) {
|
|
71
|
+
path = newPath;
|
|
72
|
+
i += 1;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
if (path)
|
|
76
|
+
entries.push({ path, status: status.trim() });
|
|
77
|
+
}
|
|
78
|
+
const unique = new Map();
|
|
79
|
+
entries.forEach((entry) => unique.set(entry.path, entry));
|
|
80
|
+
return Array.from(unique.values());
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return [];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
async function promptStageFiles() {
|
|
87
|
+
const candidates = getChangedFiles();
|
|
88
|
+
if (candidates.length === 0) {
|
|
89
|
+
console.log('No local changes to stage.');
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
console.log('No staged changes detected.');
|
|
93
|
+
console.log('Choose how to stage changes:');
|
|
94
|
+
const options = [
|
|
95
|
+
{ label: 'Stage all files (git add .)', value: 'all' },
|
|
96
|
+
{ label: 'Stage selected files', value: 'select' },
|
|
97
|
+
{ label: 'Cancel', value: 'cancel' }
|
|
98
|
+
];
|
|
99
|
+
options.forEach((option, index) => {
|
|
100
|
+
console.log(`${index + 1}. ${option.label}`);
|
|
101
|
+
});
|
|
102
|
+
const selection = await prompts({
|
|
103
|
+
type: 'number',
|
|
104
|
+
name: 'choice',
|
|
105
|
+
message: 'Enter a number (1-3):',
|
|
106
|
+
min: 1,
|
|
107
|
+
max: options.length
|
|
108
|
+
});
|
|
109
|
+
const choice = selection.choice;
|
|
110
|
+
if (!choice || choice < 1 || choice > options.length)
|
|
111
|
+
return false;
|
|
112
|
+
const action = options[choice - 1].value;
|
|
113
|
+
if (action === 'cancel')
|
|
114
|
+
return false;
|
|
115
|
+
if (action === 'all') {
|
|
116
|
+
spawnSync('git', ['add', '.'], { stdio: 'inherit' });
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
const fileChoices = candidates.map((entry) => ({
|
|
120
|
+
title: `${entry.status.padEnd(2, ' ')} ${entry.path}`,
|
|
121
|
+
value: entry.path
|
|
122
|
+
}));
|
|
123
|
+
const picked = await prompts({
|
|
124
|
+
type: 'multiselect',
|
|
125
|
+
name: 'files',
|
|
126
|
+
message: 'Select files to stage (space to toggle, enter to submit):',
|
|
127
|
+
choices: fileChoices,
|
|
128
|
+
min: 1
|
|
129
|
+
});
|
|
130
|
+
const files = picked.files;
|
|
131
|
+
if (!files || files.length === 0)
|
|
132
|
+
return false;
|
|
133
|
+
spawnSync('git', ['add', '--', ...files], { stdio: 'inherit' });
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
function getExtension(filePath) {
|
|
137
|
+
const lower = filePath.toLowerCase();
|
|
138
|
+
const idx = lower.lastIndexOf('.');
|
|
139
|
+
return idx >= 0 ? lower.slice(idx) : '';
|
|
140
|
+
}
|
|
141
|
+
function shouldOmitFile(path, block) {
|
|
142
|
+
const lowerPath = path.toLowerCase();
|
|
143
|
+
if (LOCK_FILES.has(lowerPath))
|
|
144
|
+
return { omit: true, reason: 'lockfile' };
|
|
145
|
+
if (MEDIA_EXTENSIONS.has(getExtension(lowerPath)))
|
|
146
|
+
return { omit: true, reason: 'media/binary asset' };
|
|
147
|
+
if (GENERATED_PREFIXES.some((prefix) => lowerPath.startsWith(prefix))) {
|
|
148
|
+
return { omit: true, reason: 'generated artifact' };
|
|
149
|
+
}
|
|
150
|
+
if (block.includes('GIT binary patch') || block.includes('Binary files')) {
|
|
151
|
+
return { omit: true, reason: 'binary diff' };
|
|
152
|
+
}
|
|
153
|
+
const lines = block.split('\n').length;
|
|
154
|
+
if (lines > MAX_BLOCK_LINES || block.length > MAX_BLOCK_CHARS) {
|
|
155
|
+
return { omit: true, reason: 'large diff' };
|
|
156
|
+
}
|
|
157
|
+
return { omit: false };
|
|
158
|
+
}
|
|
159
|
+
function summarizeDiff(diff) {
|
|
160
|
+
const statusMap = getNameStatus();
|
|
161
|
+
const blocks = diff.split(/^diff --git /m);
|
|
162
|
+
const keptBlocks = [];
|
|
163
|
+
const omitted = [];
|
|
164
|
+
for (const block of blocks) {
|
|
165
|
+
if (!block.trim())
|
|
166
|
+
continue;
|
|
167
|
+
const headerLine = block.split('\n')[0] ?? '';
|
|
168
|
+
const match = headerLine.match(/^a\/(.+?) b\/(.+?)$/);
|
|
169
|
+
if (!match) {
|
|
170
|
+
keptBlocks.push(`diff --git ${block}`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
const aPath = match[1];
|
|
174
|
+
const bPath = match[2];
|
|
175
|
+
const filePath = bPath === '/dev/null' ? aPath : bPath;
|
|
176
|
+
const status = statusMap.get(filePath);
|
|
177
|
+
const originalBlock = `diff --git ${block}`;
|
|
178
|
+
const { omit, reason } = shouldOmitFile(filePath, originalBlock);
|
|
179
|
+
if (omit && reason) {
|
|
180
|
+
omitted.push({ path: filePath, reason, status });
|
|
181
|
+
keptBlocks.push(`diff --git a/${aPath} b/${bPath}\n# content omitted (${reason})\n`);
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
keptBlocks.push(originalBlock);
|
|
185
|
+
}
|
|
186
|
+
const summaryLines = omitted.map((item) => {
|
|
187
|
+
const statusLabel = item.status ? `${item.status} ` : '';
|
|
188
|
+
return `- ${statusLabel}${item.path} (${item.reason})`;
|
|
189
|
+
});
|
|
190
|
+
const summaryHeader = summaryLines.length
|
|
191
|
+
? `# Omitted file contents (summarized)\n${summaryLines.join('\n')}\n\n`
|
|
192
|
+
: '';
|
|
193
|
+
return `${summaryHeader}${keptBlocks.join('\n')}`.trim();
|
|
194
|
+
}
|
|
9
195
|
async function main() {
|
|
10
196
|
// 1. 사용자 힌트(Arguments) 가져오기
|
|
11
|
-
const
|
|
197
|
+
const rawArgs = process.argv.slice(2);
|
|
198
|
+
if (rawArgs.includes('--version') || rawArgs.includes('-v')) {
|
|
199
|
+
const version = getPackageVersion();
|
|
200
|
+
console.log(version ?? 'unknown');
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const args = rawArgs.filter((arg) => arg !== '--version' && arg !== '-v').join(' ');
|
|
12
204
|
if (args) {
|
|
13
|
-
console.log(
|
|
205
|
+
console.log(`Hint detected: "${args}"`);
|
|
14
206
|
}
|
|
15
207
|
// 2. 인증 토큰 확인 (없으면 물어봄)
|
|
16
208
|
let authToken = config.get('authToken');
|
|
17
209
|
if (!authToken) {
|
|
18
|
-
console.log(
|
|
19
|
-
console.log(
|
|
210
|
+
console.log('Initial authentication setup');
|
|
211
|
+
console.log('Enter the shared access token. It will be stored locally.');
|
|
20
212
|
const response = await prompts({
|
|
21
213
|
type: 'password',
|
|
22
214
|
name: 'token',
|
|
@@ -27,11 +219,11 @@ async function main() {
|
|
|
27
219
|
return;
|
|
28
220
|
authToken = response.token;
|
|
29
221
|
config.set('authToken', authToken);
|
|
30
|
-
console.log(
|
|
222
|
+
console.log('Token saved.\n');
|
|
31
223
|
}
|
|
32
224
|
if (!authToken)
|
|
33
225
|
return;
|
|
34
|
-
console.log(
|
|
226
|
+
console.log('Analyzing staged changes...');
|
|
35
227
|
try {
|
|
36
228
|
// 3. Git Diff 가져오기
|
|
37
229
|
let diff;
|
|
@@ -39,13 +231,20 @@ async function main() {
|
|
|
39
231
|
diff = execSync('git diff --cached').toString();
|
|
40
232
|
}
|
|
41
233
|
catch (e) {
|
|
42
|
-
console.log(
|
|
234
|
+
console.log('Error: not a git repository or git is unavailable.');
|
|
43
235
|
return;
|
|
44
236
|
}
|
|
45
237
|
if (!diff.trim()) {
|
|
46
|
-
|
|
47
|
-
|
|
238
|
+
const staged = await promptStageFiles();
|
|
239
|
+
if (!staged)
|
|
240
|
+
return;
|
|
241
|
+
diff = execSync('git diff --cached').toString();
|
|
242
|
+
if (!diff.trim()) {
|
|
243
|
+
console.log('No staged changes. Nothing to commit.');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
48
246
|
}
|
|
247
|
+
const filteredDiff = summarizeDiff(diff);
|
|
49
248
|
// 4. API 요청 (Diff + Hint + Auth)
|
|
50
249
|
const response = await fetch(WORKER_URL, {
|
|
51
250
|
method: 'POST',
|
|
@@ -53,12 +252,12 @@ async function main() {
|
|
|
53
252
|
'Content-Type': 'application/json',
|
|
54
253
|
'X-AUTH-TOKEN': authToken
|
|
55
254
|
},
|
|
56
|
-
body: JSON.stringify({ diff, hint: args }) // 힌트도 같이 전송
|
|
255
|
+
body: JSON.stringify({ diff: filteredDiff, hint: args }) // 힌트도 같이 전송
|
|
57
256
|
});
|
|
58
257
|
// 인증 실패 처리 (401)
|
|
59
258
|
if (response.status === 401) {
|
|
60
|
-
console.log(
|
|
61
|
-
console.log(
|
|
259
|
+
console.log('\nAuthentication failed. The token is invalid or expired.');
|
|
260
|
+
console.log('Stored token will be cleared. Run again to enter a new token.');
|
|
62
261
|
config.delete('authToken');
|
|
63
262
|
return;
|
|
64
263
|
}
|
|
@@ -67,44 +266,51 @@ async function main() {
|
|
|
67
266
|
}
|
|
68
267
|
const { message } = await response.json();
|
|
69
268
|
// 5. 결과 출력 및 액션 선택
|
|
70
|
-
console.log('\
|
|
71
|
-
console.log(
|
|
72
|
-
console.log('\n
|
|
269
|
+
console.log('\nCommit message:\n');
|
|
270
|
+
console.log(message);
|
|
271
|
+
console.log('\n-----------------------------------\n');
|
|
272
|
+
const actions = [
|
|
273
|
+
{ label: 'Commit with this message', value: 'commit' },
|
|
274
|
+
{ label: 'Edit in editor and commit', value: 'edit' },
|
|
275
|
+
{ label: 'Copy to clipboard', value: 'copy' },
|
|
276
|
+
{ label: 'Cancel', value: 'cancel' }
|
|
277
|
+
];
|
|
278
|
+
console.log('Select an action:');
|
|
279
|
+
actions.forEach((action, index) => {
|
|
280
|
+
console.log(`${index + 1}. ${action.label}`);
|
|
281
|
+
});
|
|
73
282
|
const result = await prompts({
|
|
74
|
-
type: '
|
|
75
|
-
name: '
|
|
76
|
-
message: '
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
{ title: '✏️ 에디터에서 수정 후 커밋', value: 'edit' },
|
|
80
|
-
{ title: '📋 클립보드에 복사', value: 'copy' },
|
|
81
|
-
{ title: '❌ 취소', value: 'cancel' }
|
|
82
|
-
]
|
|
283
|
+
type: 'number',
|
|
284
|
+
name: 'choice',
|
|
285
|
+
message: 'Enter a number (1-4):',
|
|
286
|
+
min: 1,
|
|
287
|
+
max: actions.length
|
|
83
288
|
});
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
289
|
+
const choice = result.choice;
|
|
290
|
+
if (!choice || choice < 1 || choice > actions.length)
|
|
86
291
|
return;
|
|
292
|
+
const action = actions[choice - 1].value;
|
|
87
293
|
switch (action) {
|
|
88
294
|
case 'commit':
|
|
89
295
|
spawnSync('git', ['commit', '-m', message], { stdio: 'inherit' });
|
|
90
|
-
console.log(
|
|
296
|
+
console.log('Commit complete.');
|
|
91
297
|
break;
|
|
92
298
|
case 'edit':
|
|
93
299
|
spawnSync('git', ['commit', '-e', '-m', message], { stdio: 'inherit' });
|
|
94
|
-
console.log(
|
|
300
|
+
console.log('Commit complete.');
|
|
95
301
|
break;
|
|
96
302
|
case 'copy':
|
|
97
303
|
clipboardy.writeSync(message);
|
|
98
|
-
console.log(
|
|
304
|
+
console.log('Copied to clipboard.');
|
|
99
305
|
break;
|
|
100
306
|
case 'cancel':
|
|
101
|
-
console.log(
|
|
307
|
+
console.log('Cancelled.');
|
|
102
308
|
break;
|
|
103
309
|
}
|
|
104
310
|
}
|
|
105
311
|
catch (error) {
|
|
106
312
|
const message = error instanceof Error ? error.message : String(error);
|
|
107
|
-
console.error(
|
|
313
|
+
console.error('Error:', message);
|
|
108
314
|
}
|
|
109
315
|
}
|
|
110
316
|
main();
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-cmg",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "AI Commit Message Generator",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"ai-
|
|
7
|
+
"ai-cmg": "dist/index.js",
|
|
8
|
+
"ai-commit": "dist/index.js",
|
|
9
|
+
"d-commit": "dist/index.js"
|
|
8
10
|
},
|
|
9
11
|
"main": "./dist/index.js",
|
|
10
12
|
"types": "./dist/index.d.ts",
|
|
@@ -20,7 +22,6 @@
|
|
|
20
22
|
"dependencies": {
|
|
21
23
|
"clipboardy": "^5.0.2",
|
|
22
24
|
"conf": "^15.0.2",
|
|
23
|
-
"picocolors": "^1.1.1",
|
|
24
25
|
"prompts": "^2.4.2"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|