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.
Files changed (2) hide show
  1. package/dist/index.js +238 -32
  2. 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 args = process.argv.slice(2).join(' ');
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(pc.cyan(`💡 사용자 힌트 감지: "${args}"`));
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(pc.bgMagenta(pc.white(' 🔐 최초 인증 설정 ')));
19
- console.log(pc.gray('팀에서 공유된 Access Token을 입력해주세요. (내 컴퓨터에 안전하게 저장됩니다)'));
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(pc.green(' 인증 정보가 저장되었습니다.\n'));
222
+ console.log('Token saved.\n');
31
223
  }
32
224
  if (!authToken)
33
225
  return;
34
- console.log(pc.cyan('ℹ️ Staged 변경 사항 분석 중...'));
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(pc.red(' Git 저장소가 아니거나 git 명령어를 찾을 없습니다.'));
234
+ console.log('Error: not a git repository or git is unavailable.');
43
235
  return;
44
236
  }
45
237
  if (!diff.trim()) {
46
- console.log(pc.yellow('⚠️ 커밋할 변경 사항이 없습니다. "git add"를 먼저 실행해주세요.'));
47
- return;
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(pc.red('\n⛔️ 인증 실패! 토큰이 만료되었거나 틀렸습니다.'));
61
- console.log(pc.gray('기존 설정 파일을 삭제합니다. 다시 실행하여 토큰을 입력해주세요.'));
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('\n' + pc.bgGreen(pc.black(' 🤖 AI 생성 커밋 메시지 ')) + '\n');
71
- console.log(pc.bold(message));
72
- console.log('\n' + pc.dim('-----------------------------------') + '\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: 'select',
75
- name: 'action',
76
- message: '어떻게 하시겠습니까?',
77
- choices: [
78
- { title: '🚀 이대로 커밋하기', value: 'commit' },
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 action = result.action;
85
- if (!action)
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(pc.green(' 커밋 완료!'));
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(pc.green(' 커밋 완료!'));
300
+ console.log('Commit complete.');
95
301
  break;
96
302
  case 'copy':
97
303
  clipboardy.writeSync(message);
98
- console.log(pc.green(' 복사 완료!'));
304
+ console.log('Copied to clipboard.');
99
305
  break;
100
306
  case 'cancel':
101
- console.log(pc.gray('취소됨'));
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(pc.red('오류 발생:'), message);
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.1",
3
+ "version": "0.0.2",
4
4
  "description": "AI Commit Message Generator",
5
5
  "type": "module",
6
6
  "bin": {
7
- "ai-commit": "dist/index.js"
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": {