ai-cmg 0.0.7 → 0.0.9

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 +94 -21
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { execSync, spawnSync } from 'child_process';
3
- import { readFileSync } from 'fs';
4
- import { dirname, join } from 'path';
2
+ import { execFileSync, execSync, spawnSync } from 'child_process';
3
+ import { readFileSync, existsSync } from 'fs';
4
+ import { dirname, join, resolve } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  import prompts from 'prompts';
7
7
  import clipboardy from 'clipboardy';
@@ -103,15 +103,90 @@ function getGitEnv() {
103
103
  delete env.GIT_CEILING_DIRECTORIES;
104
104
  return env;
105
105
  }
106
- function resolveRepoRoot() {
106
+ function isGitAvailable() {
107
107
  try {
108
- const root = execSync('git rev-parse --show-toplevel', { env: getGitEnv() }).toString().trim();
109
- return root || null;
108
+ execFileSync('git', ['--version'], { env: getGitEnv(), stdio: 'ignore' });
109
+ return true;
110
+ }
111
+ catch {
112
+ return false;
113
+ }
114
+ }
115
+ function resolveGitDir(repoRoot) {
116
+ const gitPath = join(repoRoot, '.git');
117
+ if (!existsSync(gitPath))
118
+ return null;
119
+ try {
120
+ const stat = execSync(`test -d "${gitPath}" && echo "dir" || echo "file"`).toString().trim();
121
+ if (stat === 'dir')
122
+ return gitPath;
123
+ }
124
+ catch {
125
+ // fallthrough to file parsing
126
+ }
127
+ try {
128
+ const content = readFileSync(gitPath, 'utf8').trim();
129
+ const match = content.match(/^gitdir:\s*(.+)$/i);
130
+ if (!match)
131
+ return null;
132
+ const gitDirPath = match[1].trim();
133
+ return gitDirPath.startsWith('/')
134
+ ? gitDirPath
135
+ : resolve(repoRoot, gitDirPath);
110
136
  }
111
137
  catch {
112
138
  return null;
113
139
  }
114
140
  }
141
+ function runGit(args, repoRoot) {
142
+ try {
143
+ return execFileSync('git', ['-C', repoRoot, ...args], { env: getGitEnv() }).toString();
144
+ }
145
+ catch {
146
+ const gitDir = resolveGitDir(repoRoot);
147
+ if (!gitDir)
148
+ throw new Error('git failed');
149
+ return execFileSync('git', ['--git-dir', gitDir, '--work-tree', repoRoot, ...args], {
150
+ env: getGitEnv()
151
+ }).toString();
152
+ }
153
+ }
154
+ function spawnGit(args, repoRoot) {
155
+ const first = spawnSync('git', ['-C', repoRoot, ...args], { stdio: 'inherit', env: getGitEnv() });
156
+ if (first.status === 0)
157
+ return 0;
158
+ const gitDir = resolveGitDir(repoRoot);
159
+ if (!gitDir)
160
+ return first.status ?? 1;
161
+ const second = spawnSync('git', ['--git-dir', gitDir, '--work-tree', repoRoot, ...args], { stdio: 'inherit', env: getGitEnv() });
162
+ return second.status ?? 1;
163
+ }
164
+ function findRepoRootByFs(startDir) {
165
+ let current = resolve(startDir);
166
+ while (true) {
167
+ const gitPath = join(current, '.git');
168
+ if (existsSync(gitPath))
169
+ return current;
170
+ const parent = dirname(current);
171
+ if (parent === current)
172
+ return null;
173
+ current = parent;
174
+ }
175
+ }
176
+ function resolveRepoRoot(baseDir) {
177
+ try {
178
+ const root = execSync('git rev-parse --show-toplevel', {
179
+ env: getGitEnv(),
180
+ cwd: baseDir
181
+ }).toString().trim();
182
+ return root || null;
183
+ }
184
+ catch {
185
+ if (!baseDir)
186
+ return null;
187
+ return findRepoRootByFs(baseDir);
188
+ }
189
+ }
115
190
  function parseMessageArgs(rawArgs) {
116
191
  const hintParts = [];
117
192
  let hint;
@@ -148,10 +223,7 @@ function parseMessageArgs(rawArgs) {
148
223
  function getNameStatus(repoRoot) {
149
224
  const map = new Map();
150
225
  try {
151
- const output = execSync('git diff --cached --name-status', {
152
- cwd: repoRoot,
153
- env: getGitEnv()
154
- }).toString();
226
+ const output = runGit(['diff', '--cached', '--name-status'], repoRoot);
155
227
  output.split('\n').forEach((line) => {
156
228
  if (!line.trim())
157
229
  return;
@@ -168,10 +240,7 @@ function getNameStatus(repoRoot) {
168
240
  }
169
241
  function getChangedFiles(repoRoot) {
170
242
  try {
171
- const output = execSync('git status --porcelain -z', {
172
- cwd: repoRoot,
173
- env: getGitEnv()
174
- }).toString();
243
+ const output = runGit(['status', '--porcelain', '-z'], repoRoot);
175
244
  if (!output)
176
245
  return [];
177
246
  const parts = output.split('\0').filter(Boolean);
@@ -229,7 +298,7 @@ async function promptStageFiles(repoRoot) {
229
298
  if (action === 'cancel')
230
299
  return false;
231
300
  if (action === 'all') {
232
- spawnSync('git', ['add', '.'], { stdio: 'inherit', cwd: repoRoot, env: getGitEnv() });
301
+ spawnGit(['add', '.'], repoRoot);
233
302
  return true;
234
303
  }
235
304
  const fileChoices = candidates.map((entry) => ({
@@ -246,7 +315,7 @@ async function promptStageFiles(repoRoot) {
246
315
  const files = picked.files;
247
316
  if (!files || files.length === 0)
248
317
  return false;
249
- spawnSync('git', ['add', '--', ...files], { stdio: 'inherit', cwd: repoRoot, env: getGitEnv() });
318
+ spawnGit(['add', '--', ...files], repoRoot);
250
319
  return true;
251
320
  }
252
321
  function getExtension(filePath) {
@@ -361,7 +430,11 @@ async function main() {
361
430
  return;
362
431
  console.log('Analyzing staged changes...');
363
432
  try {
364
- const repoRoot = resolveRepoRoot();
433
+ if (!isGitAvailable()) {
434
+ console.log('Error: git is not available in PATH.');
435
+ return;
436
+ }
437
+ const repoRoot = resolveRepoRoot(process.cwd());
365
438
  if (!repoRoot) {
366
439
  console.log('Error: not a git repository.');
367
440
  return;
@@ -369,7 +442,7 @@ async function main() {
369
442
  // 3. Git Diff 가져오기
370
443
  let diff;
371
444
  try {
372
- diff = execSync('git diff --cached', { cwd: repoRoot, env: getGitEnv() }).toString();
445
+ diff = runGit(['diff', '--cached'], repoRoot);
373
446
  }
374
447
  catch (e) {
375
448
  console.log('Error: not a git repository or git is unavailable.');
@@ -379,7 +452,7 @@ async function main() {
379
452
  const staged = await promptStageFiles(repoRoot);
380
453
  if (!staged)
381
454
  return;
382
- diff = execSync('git diff --cached', { cwd: repoRoot, env: getGitEnv() }).toString();
455
+ diff = runGit(['diff', '--cached'], repoRoot);
383
456
  if (!diff.trim()) {
384
457
  console.log('No staged changes. Nothing to commit.');
385
458
  return;
@@ -438,11 +511,11 @@ async function main() {
438
511
  const action = actions[choice - 1].value;
439
512
  switch (action) {
440
513
  case 'commit':
441
- spawnSync('git', ['commit', '-m', message], { stdio: 'inherit', cwd: repoRoot, env: getGitEnv() });
514
+ spawnGit(['commit', '-m', message], repoRoot);
442
515
  console.log('Commit complete.');
443
516
  break;
444
517
  case 'edit':
445
- spawnSync('git', ['commit', '-e', '-m', message], { stdio: 'inherit', cwd: repoRoot, env: getGitEnv() });
518
+ spawnGit(['commit', '-e', '-m', message], repoRoot);
446
519
  console.log('Commit complete.');
447
520
  break;
448
521
  case 'copy':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ai-cmg",
3
- "version": "0.0.7",
3
+ "version": "0.0.9",
4
4
  "description": "AI Commit Message Generator",
5
5
  "type": "module",
6
6
  "bin": {