@xelth/eck-snapshot 5.4.0 → 5.4.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xelth/eck-snapshot",
3
- "version": "5.4.0",
3
+ "version": "5.4.1",
4
4
  "description": "A powerful CLI tool to create and restore single-file text snapshots of Git repositories and directories. Optimized for AI context and LLM workflows.",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -10,13 +10,13 @@ import ora from 'ora';
10
10
  import micromatch from 'micromatch';
11
11
  import chalk from 'chalk';
12
12
 
13
- import {
14
- parseSize, formatSize, matchesPattern, checkGitRepository,
15
- scanDirectoryRecursively, loadGitignore, readFileWithSizeCheck,
16
- generateDirectoryTree, loadConfig, displayProjectInfo, loadProjectEckManifest,
17
- ensureSnapshotsInGitignore, initializeEckManifest, generateTimestamp,
18
- getShortRepoName, SecretScanner
19
- } from '../../utils/fileUtils.js';
13
+ import {
14
+ parseSize, formatSize, matchesPattern, checkGitRepository,
15
+ scanDirectoryRecursively, loadGitignore, readFileWithSizeCheck,
16
+ generateDirectoryTree, loadConfig, displayProjectInfo, loadProjectEckManifest,
17
+ ensureSnapshotsInGitignore, initializeEckManifest, generateTimestamp,
18
+ getShortRepoName, SecretScanner
19
+ } from '../../utils/fileUtils.js';
20
20
  import { detectProjectType, getProjectSpecificFiltering } from '../../utils/projectDetector.js';
21
21
  import { estimateTokensWithPolynomial, generateTrainingCommand } from '../../utils/tokenEstimator.js';
22
22
  import { loadSetupConfig, getProfile } from '../../config.js';
@@ -312,6 +312,17 @@ async function estimateProjectTokens(projectPath, config, projectType = null) {
312
312
  }
313
313
 
314
314
  async function processProjectFiles(repoPath, options, config, projectType = null) {
315
+ // Merge project-specific filtering rules (e.g., Cargo.lock for Rust)
316
+ if (projectType) {
317
+ const projectSpecific = await getProjectSpecificFiltering(projectType);
318
+ config = {
319
+ ...config,
320
+ dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
321
+ filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
322
+ extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
323
+ };
324
+ }
325
+
315
326
  const originalCwd = process.cwd();
316
327
  console.log(`\nšŸ“ø Processing files for: ${path.basename(repoPath)}`);
317
328
 
@@ -703,59 +714,59 @@ export async function createRepoSnapshot(repoPath, options) {
703
714
 
704
715
  // Helper to write snapshot file
705
716
  const writeSnapshot = async (suffix, isAgentMode) => {
706
- // CHANGE: Force agent to FALSE for the main snapshot header.
707
- // The snapshot is read by the Human/Senior Arch, not the Agent itself.
708
- // The Agent reads CLAUDE.md.
709
- const opts = { ...options, agent: false, jag: isJag, jas: isJas, jao: isJao };
710
- const header = await generateEnhancedAIHeader({ stats, repoName, mode: 'file', eckManifest, options: opts, repoPath: processedRepoPath }, isGitRepo);
711
-
712
- // Compact filename format: eck{ShortName}{timestamp}_{hash}_{suffix}.md
713
- // getShortRepoName ensures Capitalized Start/End (e.g. SnaOt)
714
- const shortHash = gitHash ? gitHash.substring(0, 7) : '';
715
- const shortRepoName = getShortRepoName(repoName);
716
-
717
- let fname = `eck${shortRepoName}${timestamp}`;
718
- if (shortHash) fname += `_${shortHash}`;
719
-
720
- // Add mode suffix
721
- if (options.skeleton) {
722
- fname += '_sk';
723
- } else if (suffix) {
724
- fname += suffix;
725
- }
726
-
727
- fname += `.${fileExtension}`;
728
- const fpath = path.join(outputPath, fname);
729
- const fullContent = header + fileBody;
730
- await fs.writeFile(fpath, fullContent);
731
- console.log(`šŸ“„ Generated Snapshot: ${fname}`);
732
-
733
- // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
734
- // Only create .eck/lastsnapshot/ entries for the main snapshot
735
- if (!isAgentMode) {
736
- try {
737
- const snapDir = path.join(originalCwd, '.eck', 'lastsnapshot');
738
- await fs.mkdir(snapDir, { recursive: true });
739
-
740
- // 1. Clean up OLD snapshots in this specific folder
741
- // We keep AnswerToSA.md, but remove old snapshots and legacy answer.md
742
- const existingFiles = await fs.readdir(snapDir);
743
- for (const file of existingFiles) {
744
- if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
745
- await fs.unlink(path.join(snapDir, file));
746
- }
747
- }
748
-
749
- // 2. Save the NEW specific named file
750
- await fs.writeFile(path.join(snapDir, fname), fullContent);
751
-
752
- console.log(chalk.cyan(`šŸ“‹ Active snapshot updated in .eck/lastsnapshot/: ${fname}`));
753
- } catch (e) {
754
- // Non-critical failure
755
- console.warn(chalk.yellow(`āš ļø Could not update .eck/lastsnapshot/: ${e.message}`));
756
- }
757
- }
758
- // --------------------------------------------
717
+ // CHANGE: Force agent to FALSE for the main snapshot header.
718
+ // The snapshot is read by the Human/Senior Arch, not the Agent itself.
719
+ // The Agent reads CLAUDE.md.
720
+ const opts = { ...options, agent: false, jag: isJag, jas: isJas, jao: isJao };
721
+ const header = await generateEnhancedAIHeader({ stats, repoName, mode: 'file', eckManifest, options: opts, repoPath: processedRepoPath }, isGitRepo);
722
+
723
+ // Compact filename format: eck{ShortName}{timestamp}_{hash}_{suffix}.md
724
+ // getShortRepoName ensures Capitalized Start/End (e.g. SnaOt)
725
+ const shortHash = gitHash ? gitHash.substring(0, 7) : '';
726
+ const shortRepoName = getShortRepoName(repoName);
727
+
728
+ let fname = `eck${shortRepoName}${timestamp}`;
729
+ if (shortHash) fname += `_${shortHash}`;
730
+
731
+ // Add mode suffix
732
+ if (options.skeleton) {
733
+ fname += '_sk';
734
+ } else if (suffix) {
735
+ fname += suffix;
736
+ }
737
+
738
+ fname += `.${fileExtension}`;
739
+ const fpath = path.join(outputPath, fname);
740
+ const fullContent = header + fileBody;
741
+ await fs.writeFile(fpath, fullContent);
742
+ console.log(`šŸ“„ Generated Snapshot: ${fname}`);
743
+
744
+ // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
745
+ // Only create .eck/lastsnapshot/ entries for the main snapshot
746
+ if (!isAgentMode) {
747
+ try {
748
+ const snapDir = path.join(originalCwd, '.eck', 'lastsnapshot');
749
+ await fs.mkdir(snapDir, { recursive: true });
750
+
751
+ // 1. Clean up OLD snapshots in this specific folder
752
+ // We keep AnswerToSA.md, but remove old snapshots and legacy answer.md
753
+ const existingFiles = await fs.readdir(snapDir);
754
+ for (const file of existingFiles) {
755
+ if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
756
+ await fs.unlink(path.join(snapDir, file));
757
+ }
758
+ }
759
+
760
+ // 2. Save the NEW specific named file
761
+ await fs.writeFile(path.join(snapDir, fname), fullContent);
762
+
763
+ console.log(chalk.cyan(`šŸ“‹ Active snapshot updated in .eck/lastsnapshot/: ${fname}`));
764
+ } catch (e) {
765
+ // Non-critical failure
766
+ console.warn(chalk.yellow(`āš ļø Could not update .eck/lastsnapshot/: ${e.message}`));
767
+ }
768
+ }
769
+ // --------------------------------------------
759
770
 
760
771
  return fpath;
761
772
  };
@@ -2,9 +2,10 @@ import fs from 'fs/promises';
2
2
  import path from 'path';
3
3
  import ora from 'ora';
4
4
  import chalk from 'chalk';
5
- import { getGitAnchor, getChangedFiles, getGitDiffOutput } from '../../utils/gitUtils.js';
5
+ import { getGitAnchor, getChangedFiles } from '../../utils/gitUtils.js';
6
6
  import { loadSetupConfig } from '../../config.js';
7
7
  import { readFileWithSizeCheck, parseSize, formatSize, matchesPattern, loadGitignore, generateTimestamp, getShortRepoName } from '../../utils/fileUtils.js';
8
+ import { detectProjectType, getProjectSpecificFiltering } from '../../utils/projectDetector.js';
8
9
  import { fileURLToPath } from 'url';
9
10
 
10
11
  const __filename = fileURLToPath(import.meta.url);
@@ -16,18 +17,22 @@ async function generateSnapshotContent(repoPath, changedFiles, anchor, config, g
16
17
  let includedCount = 0;
17
18
  const fileList = [];
18
19
 
19
- // Check for Agent Report in .eck/lastsnapshot/AnswerToSA.md (STRICT LOCATION)
20
- const reportPath = path.join(repoPath, '.eck', 'lastsnapshot', 'AnswerToSA.md');
21
- let agentReport = null;
22
- try {
23
- agentReport = await fs.readFile(reportPath, 'utf-8');
24
- if (!changedFiles.includes('.eck/lastsnapshot/AnswerToSA.md')) {
25
- changedFiles.push('.eck/lastsnapshot/AnswerToSA.md');
26
- }
27
- } catch (e) { /* No report */ }
20
+ // Check for Agent Report in .eck/lastsnapshot/AnswerToSA.md (STRICT LOCATION)
21
+ const reportPath = path.join(repoPath, '.eck', 'lastsnapshot', 'AnswerToSA.md');
22
+ let agentReport = null;
23
+ try {
24
+ agentReport = await fs.readFile(reportPath, 'utf-8');
25
+ if (!changedFiles.includes('.eck/lastsnapshot/AnswerToSA.md')) {
26
+ changedFiles.push('.eck/lastsnapshot/AnswerToSA.md');
27
+ }
28
+ } catch (e) { /* No report */ }
28
29
 
29
30
  for (const filePath of changedFiles) {
30
- if (config.dirsToIgnore.some(d => filePath.startsWith(d))) continue;
31
+ if (config.dirsToIgnore?.some(d => filePath.startsWith(d))) continue;
32
+ const fileName = path.basename(filePath);
33
+ const fileExt = path.extname(filePath);
34
+ if (config.filesToIgnore?.includes(fileName)) continue;
35
+ if (fileExt && config.extensionsToIgnore?.includes(fileExt)) continue;
31
36
  if (gitignore.ignores(filePath) && filePath !== '.eck/lastsnapshot/AnswerToSA.md') continue;
32
37
 
33
38
  try {
@@ -55,15 +60,12 @@ async function generateSnapshotContent(repoPath, changedFiles, anchor, config, g
55
60
 
56
61
  header = reportSection + header;
57
62
 
58
- const diffOutput = await getGitDiffOutput(repoPath, anchor);
59
- const diffSection = `\n--- GIT DIFF (For Context) ---\n\n\`\`\`diff\n${diffOutput}\n\`\`\``;
60
-
61
- return {
62
- fullContent: header + contentOutput + diffSection,
63
- includedCount,
64
- anchor,
65
- agentReport
66
- };
63
+ return {
64
+ fullContent: header + contentOutput,
65
+ includedCount,
66
+ anchor,
67
+ agentReport
68
+ };
67
69
  }
68
70
 
69
71
  export async function updateSnapshot(repoPath, options) {
@@ -80,11 +82,24 @@ export async function updateSnapshot(repoPath, options) {
80
82
  return;
81
83
  }
82
84
 
83
- const setupConfig = await loadSetupConfig();
84
- const config = { ...setupConfig.fileFiltering, ...setupConfig.performance, ...options };
85
- const gitignore = await loadGitignore(repoPath);
86
-
87
- const { fullContent, includedCount, agentReport } = await generateSnapshotContent(repoPath, changedFiles, anchor, config, gitignore);
85
+ const setupConfig = await loadSetupConfig();
86
+ let config = { ...setupConfig.fileFiltering, ...setupConfig.performance, ...options };
87
+
88
+ // Detect project type and merge project-specific filters
89
+ const projectDetection = await detectProjectType(repoPath);
90
+ if (projectDetection.type) {
91
+ const projectSpecific = await getProjectSpecificFiltering(projectDetection.type);
92
+ config = {
93
+ ...config,
94
+ dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
95
+ filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
96
+ extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
97
+ };
98
+ }
99
+
100
+ const gitignore = await loadGitignore(repoPath);
101
+
102
+ const { fullContent, includedCount, agentReport } = await generateSnapshotContent(repoPath, changedFiles, anchor, config, gitignore);
88
103
 
89
104
  // Determine sequence number
90
105
  let seqNum = 1;
@@ -101,43 +116,43 @@ export async function updateSnapshot(repoPath, options) {
101
116
  await fs.writeFile(counterPath, `${anchor.substring(0, 7)}:${seqNum}`);
102
117
  } catch (e) {}
103
118
 
104
- const timestamp = generateTimestamp();
105
- const shortRepoName = getShortRepoName(path.basename(repoPath));
106
- const outputFilename = `eck${shortRepoName}${timestamp}_${anchor.substring(0, 7)}_up${seqNum}.md`;
107
- const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
108
-
109
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
110
- await fs.writeFile(outputPath, fullContent);
111
-
112
- spinner.succeed(`Update snapshot created: .eck/snapshots/${outputFilename}`);
113
-
114
- // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
115
- try {
116
- const snapDir = path.join(repoPath, '.eck', 'lastsnapshot');
117
- await fs.mkdir(snapDir, { recursive: true });
118
-
119
- // 1. Clean up OLD snapshots
120
- const existingFiles = await fs.readdir(snapDir);
121
- for (const file of existingFiles) {
122
- if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
123
- await fs.unlink(path.join(snapDir, file));
124
- }
125
- }
126
-
127
- // 2. Save new file
128
- await fs.writeFile(path.join(snapDir, outputFilename), fullContent);
129
- console.log(chalk.cyan(`šŸ“‹ Active snapshot updated in .eck/lastsnapshot/: ${outputFilename}`));
130
- } catch (e) {
131
- // Non-critical failure
132
- }
133
- // --------------------------------------------
134
-
135
- // Check if agent report was included
136
- if (agentReport) {
137
- console.log(chalk.green('šŸ“Ø Included Agent Report (.eck/lastsnapshot/AnswerToSA.md)'));
138
- }
139
-
140
- console.log(`šŸ“¦ Included ${includedCount} changed files.`);
119
+ const timestamp = generateTimestamp();
120
+ const shortRepoName = getShortRepoName(path.basename(repoPath));
121
+ const outputFilename = `eck${shortRepoName}${timestamp}_${anchor.substring(0, 7)}_up${seqNum}.md`;
122
+ const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
123
+
124
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
125
+ await fs.writeFile(outputPath, fullContent);
126
+
127
+ spinner.succeed(`Update snapshot created: .eck/snapshots/${outputFilename}`);
128
+
129
+ // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
130
+ try {
131
+ const snapDir = path.join(repoPath, '.eck', 'lastsnapshot');
132
+ await fs.mkdir(snapDir, { recursive: true });
133
+
134
+ // 1. Clean up OLD snapshots
135
+ const existingFiles = await fs.readdir(snapDir);
136
+ for (const file of existingFiles) {
137
+ if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
138
+ await fs.unlink(path.join(snapDir, file));
139
+ }
140
+ }
141
+
142
+ // 2. Save new file
143
+ await fs.writeFile(path.join(snapDir, outputFilename), fullContent);
144
+ console.log(chalk.cyan(`šŸ“‹ Active snapshot updated in .eck/lastsnapshot/: ${outputFilename}`));
145
+ } catch (e) {
146
+ // Non-critical failure
147
+ }
148
+ // --------------------------------------------
149
+
150
+ // Check if agent report was included
151
+ if (agentReport) {
152
+ console.log(chalk.green('šŸ“Ø Included Agent Report (.eck/lastsnapshot/AnswerToSA.md)'));
153
+ }
154
+
155
+ console.log(`šŸ“¦ Included ${includedCount} changed files.`);
141
156
 
142
157
  } catch (error) {
143
158
  spinner.fail(`Update failed: ${error.message}`);
@@ -159,11 +174,24 @@ export async function updateSnapshotJson(repoPath) {
159
174
  return;
160
175
  }
161
176
 
162
- const setupConfig = await loadSetupConfig();
163
- const config = { ...setupConfig.fileFiltering, ...setupConfig.performance };
164
- const gitignore = await loadGitignore(repoPath);
165
-
166
- const { fullContent, includedCount, agentReport } = await generateSnapshotContent(repoPath, changedFiles, anchor, config, gitignore);
177
+ const setupConfig = await loadSetupConfig();
178
+ let config = { ...setupConfig.fileFiltering, ...setupConfig.performance };
179
+
180
+ // Detect project type and merge project-specific filters
181
+ const projectDetection = await detectProjectType(repoPath);
182
+ if (projectDetection.type) {
183
+ const projectSpecific = await getProjectSpecificFiltering(projectDetection.type);
184
+ config = {
185
+ ...config,
186
+ dirsToIgnore: [...(config.dirsToIgnore || []), ...(projectSpecific.dirsToIgnore || [])],
187
+ filesToIgnore: [...(config.filesToIgnore || []), ...(projectSpecific.filesToIgnore || [])],
188
+ extensionsToIgnore: [...(config.extensionsToIgnore || []), ...(projectSpecific.extensionsToIgnore || [])]
189
+ };
190
+ }
191
+
192
+ const gitignore = await loadGitignore(repoPath);
193
+
194
+ const { fullContent, includedCount, agentReport } = await generateSnapshotContent(repoPath, changedFiles, anchor, config, gitignore);
167
195
 
168
196
  let seqNum = 1;
169
197
  const counterPath = path.join(repoPath, '.eck', 'update_seq');
@@ -179,39 +207,39 @@ export async function updateSnapshotJson(repoPath) {
179
207
  await fs.writeFile(counterPath, `${anchor.substring(0, 7)}:${seqNum}`);
180
208
  } catch (e) {}
181
209
 
182
- const timestamp = generateTimestamp();
183
- const shortRepoName = getShortRepoName(path.basename(repoPath));
184
- const outputFilename = `eck${shortRepoName}${timestamp}_${anchor.substring(0, 7)}_up${seqNum}.md`;
185
- const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
186
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
187
- await fs.writeFile(outputPath, fullContent);
188
-
189
- // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
190
- try {
191
- const snapDir = path.join(repoPath, '.eck', 'lastsnapshot');
192
- await fs.mkdir(snapDir, { recursive: true });
193
-
194
- // 1. Clean up OLD snapshots
195
- const existingFiles = await fs.readdir(snapDir);
196
- for (const file of existingFiles) {
197
- if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
198
- await fs.unlink(path.join(snapDir, file));
199
- }
200
- }
201
-
202
- // 2. Save new file
203
- await fs.writeFile(path.join(snapDir, outputFilename), fullContent);
204
- } catch (e) {
205
- // Non-critical failure
206
- }
207
- // --------------------------------------------
208
-
209
- console.log(JSON.stringify({
210
- status: "success",
211
- snapshot_file: `.eck/snapshots/${outputFilename}`,
212
- files_count: includedCount,
213
- timestamp: timestamp
214
- }));
210
+ const timestamp = generateTimestamp();
211
+ const shortRepoName = getShortRepoName(path.basename(repoPath));
212
+ const outputFilename = `eck${shortRepoName}${timestamp}_${anchor.substring(0, 7)}_up${seqNum}.md`;
213
+ const outputPath = path.join(repoPath, '.eck', 'snapshots', outputFilename);
214
+ await fs.mkdir(path.dirname(outputPath), { recursive: true });
215
+ await fs.writeFile(outputPath, fullContent);
216
+
217
+ // --- FEATURE: Active Snapshot (.eck/lastsnapshot/) ---
218
+ try {
219
+ const snapDir = path.join(repoPath, '.eck', 'lastsnapshot');
220
+ await fs.mkdir(snapDir, { recursive: true });
221
+
222
+ // 1. Clean up OLD snapshots
223
+ const existingFiles = await fs.readdir(snapDir);
224
+ for (const file of existingFiles) {
225
+ if ((file.startsWith('eck') && file.endsWith('.md')) || file === 'answer.md') {
226
+ await fs.unlink(path.join(snapDir, file));
227
+ }
228
+ }
229
+
230
+ // 2. Save new file
231
+ await fs.writeFile(path.join(snapDir, outputFilename), fullContent);
232
+ } catch (e) {
233
+ // Non-critical failure
234
+ }
235
+ // --------------------------------------------
236
+
237
+ console.log(JSON.stringify({
238
+ status: "success",
239
+ snapshot_file: `.eck/snapshots/${outputFilename}`,
240
+ files_count: includedCount,
241
+ timestamp: timestamp
242
+ }));
215
243
 
216
244
  } catch (error) {
217
245
  console.log(JSON.stringify({ status: "error", message: error.message }));
package/src/config.js CHANGED
@@ -50,16 +50,6 @@ function validateConfigSchema(config) {
50
50
  warnings.push('Missing "aiInstructions" section');
51
51
  }
52
52
 
53
- // Legacy support
54
- if (!config.filesToIgnore || !Array.isArray(config.filesToIgnore)) {
55
- warnings.push('filesToIgnore missing or not an array - using defaults');
56
- config.filesToIgnore = DEFAULT_CONFIG.filesToIgnore;
57
- }
58
- if (!config.dirsToIgnore || !Array.isArray(config.dirsToIgnore)) {
59
- warnings.push('dirsToIgnore missing or not an array - using defaults');
60
- config.dirsToIgnore = DEFAULT_CONFIG.dirsToIgnore;
61
- }
62
-
63
53
  if (warnings.length > 0) {
64
54
  console.warn('\nāš ļø Config Validation Warnings:');
65
55
  warnings.forEach(w => console.warn(` - ${w}`));
@@ -34,9 +34,16 @@ export async function getChangedFiles(repoPath, anchorHash) {
34
34
  }
35
35
  }
36
36
 
37
- export async function getGitDiffOutput(repoPath, anchorHash) {
37
+ export async function getGitDiffOutput(repoPath, anchorHash, excludeFiles = []) {
38
38
  try {
39
- const { stdout } = await execa('git', ['diff', anchorHash, 'HEAD'], { cwd: repoPath });
39
+ const args = ['diff', anchorHash, 'HEAD'];
40
+ if (excludeFiles.length > 0) {
41
+ args.push('--');
42
+ for (const file of excludeFiles) {
43
+ args.push(`:(exclude)${file}`);
44
+ }
45
+ }
46
+ const { stdout } = await execa('git', args, { cwd: repoPath });
40
47
  return stdout;
41
48
  } catch (e) {
42
49
  return '';