@traisetech/autopilot 2.3.0 → 2.5.0

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 (45) hide show
  1. package/CHANGELOG.md +28 -1
  2. package/README.md +215 -202
  3. package/bin/autopilot.js +9 -2
  4. package/docs/CONFIGURATION.md +103 -103
  5. package/docs/DESIGN_PRINCIPLES.md +114 -114
  6. package/docs/TEAM-MODE.md +51 -51
  7. package/docs/TROUBLESHOOTING.md +21 -21
  8. package/package.json +75 -69
  9. package/src/commands/config.js +110 -110
  10. package/src/commands/dashboard.mjs +151 -151
  11. package/src/commands/doctor.js +127 -153
  12. package/src/commands/guide.js +63 -0
  13. package/src/commands/init.js +8 -9
  14. package/src/commands/insights.js +237 -237
  15. package/src/commands/leaderboard.js +116 -116
  16. package/src/commands/pause.js +18 -18
  17. package/src/commands/preset.js +121 -121
  18. package/src/commands/resume.js +17 -17
  19. package/src/commands/start.js +41 -41
  20. package/src/commands/status.js +73 -39
  21. package/src/commands/stop.js +58 -50
  22. package/src/commands/undo.js +84 -84
  23. package/src/config/defaults.js +23 -16
  24. package/src/config/ignore.js +14 -31
  25. package/src/config/loader.js +80 -80
  26. package/src/core/commit.js +45 -52
  27. package/src/core/commitMessageGenerator.js +130 -0
  28. package/src/core/configValidator.js +92 -0
  29. package/src/core/events.js +110 -110
  30. package/src/core/focus.js +2 -1
  31. package/src/core/gemini.js +15 -15
  32. package/src/core/git.js +29 -2
  33. package/src/core/history.js +69 -69
  34. package/src/core/notifier.js +61 -0
  35. package/src/core/retryQueue.js +152 -0
  36. package/src/core/safety.js +224 -210
  37. package/src/core/state.js +69 -71
  38. package/src/core/watcher.js +193 -66
  39. package/src/index.js +70 -70
  40. package/src/utils/banner.js +6 -6
  41. package/src/utils/crypto.js +18 -18
  42. package/src/utils/identity.js +41 -41
  43. package/src/utils/logger.js +86 -68
  44. package/src/utils/paths.js +62 -62
  45. package/src/utils/process.js +141 -141
@@ -1,80 +1,80 @@
1
- const fs = require('fs-extra');
2
- const path = require('path');
3
- const logger = require('../utils/logger');
4
- const { DEFAULT_CONFIG } = require('./defaults');
5
- const { getConfigPath, getConfigDir, ensureConfigDir } = require('../utils/paths');
6
-
7
- const getGlobalConfigPath = () => path.join(getConfigDir(), 'config.json');
8
-
9
- const loadConfig = async (repoPath) => {
10
- let config = { ...DEFAULT_CONFIG };
11
-
12
- // 1. Load Global Config
13
- try {
14
- const globalPath = getGlobalConfigPath();
15
- if (await fs.pathExists(globalPath)) {
16
- const globalConfig = await fs.readJson(globalPath);
17
- config = { ...config, ...globalConfig };
18
- logger.debug(`Loaded global config from ${globalPath}`);
19
- }
20
- } catch (error) {
21
- logger.warn(`Error loading global config: ${error.message}`);
22
- }
23
-
24
- // 2. Load Local Config (Overrides Global)
25
- const configPath = getConfigPath(repoPath);
26
- try {
27
- if (await fs.pathExists(configPath)) {
28
- const localConfig = await fs.readJson(configPath);
29
- config = { ...config, ...localConfig };
30
- logger.debug(`Loaded local config from ${configPath}`);
31
- }
32
- } catch (error) {
33
- logger.warn(`Error loading local config: ${error.message}`);
34
- }
35
-
36
- // Backward compatibility: map deprecated keys
37
- try {
38
- if (config.blockBranches && !config.blockedBranches) {
39
- config.blockedBranches = config.blockBranches;
40
- }
41
- } catch (_) {}
42
-
43
- return config;
44
- };
45
-
46
- const saveConfig = async (repoPath, config, isGlobal = false) => {
47
- try {
48
- let targetPath;
49
- if (isGlobal) {
50
- await ensureConfigDir();
51
- targetPath = getGlobalConfigPath();
52
- } else {
53
- targetPath = getConfigPath(repoPath);
54
- }
55
-
56
- await fs.writeJson(targetPath, config, { spaces: 2 });
57
- logger.success(`Config saved to ${targetPath}`);
58
- } catch (error) {
59
- logger.error(`Failed to save config: ${error.message}`);
60
- }
61
- };
62
-
63
- const createDefaultConfig = async (repoPath) => {
64
- const configPath = getConfigPath(repoPath);
65
- try {
66
- if (!(await fs.pathExists(configPath))) {
67
- await fs.writeJson(configPath, DEFAULT_CONFIG, { spaces: 2 });
68
- logger.success(`Created default config at ${configPath}`);
69
- }
70
- } catch (error) {
71
- logger.error(`Failed to create config: ${error.message}`);
72
- }
73
- };
74
-
75
- module.exports = {
76
- loadConfig,
77
- saveConfig,
78
- createDefaultConfig,
79
- getGlobalConfigPath
80
- };
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const logger = require('../utils/logger');
4
+ const { DEFAULT_CONFIG } = require('./defaults');
5
+ const { getConfigPath, getConfigDir, ensureConfigDir } = require('../utils/paths');
6
+
7
+ const getGlobalConfigPath = () => path.join(getConfigDir(), 'config.json');
8
+
9
+ const loadConfig = async (repoPath) => {
10
+ let config = { ...DEFAULT_CONFIG };
11
+
12
+ // 1. Load Global Config
13
+ try {
14
+ const globalPath = getGlobalConfigPath();
15
+ if (await fs.pathExists(globalPath)) {
16
+ const globalConfig = await fs.readJson(globalPath);
17
+ config = { ...config, ...globalConfig };
18
+ logger.debug(`Loaded global config from ${globalPath}`);
19
+ }
20
+ } catch (error) {
21
+ logger.warn(`Error loading global config: ${error.message}`);
22
+ }
23
+
24
+ // 2. Load Local Config (Overrides Global)
25
+ const configPath = getConfigPath(repoPath);
26
+ try {
27
+ if (await fs.pathExists(configPath)) {
28
+ const localConfig = await fs.readJson(configPath);
29
+ config = { ...config, ...localConfig };
30
+ logger.debug(`Loaded local config from ${configPath}`);
31
+ }
32
+ } catch (error) {
33
+ logger.warn(`Error loading local config: ${error.message}`);
34
+ }
35
+
36
+ // Backward compatibility: map deprecated keys
37
+ try {
38
+ if (config.blockBranches && !config.blockedBranches) {
39
+ config.blockedBranches = config.blockBranches;
40
+ }
41
+ } catch (_) {}
42
+
43
+ return config;
44
+ };
45
+
46
+ const saveConfig = async (repoPath, config, isGlobal = false) => {
47
+ try {
48
+ let targetPath;
49
+ if (isGlobal) {
50
+ await ensureConfigDir();
51
+ targetPath = getGlobalConfigPath();
52
+ } else {
53
+ targetPath = getConfigPath(repoPath);
54
+ }
55
+
56
+ await fs.writeJson(targetPath, config, { spaces: 2 });
57
+ logger.success(`Config saved to ${targetPath}`);
58
+ } catch (error) {
59
+ logger.error(`Failed to save config: ${error.message}`);
60
+ }
61
+ };
62
+
63
+ const createDefaultConfig = async (repoPath) => {
64
+ const configPath = getConfigPath(repoPath);
65
+ try {
66
+ if (!(await fs.pathExists(configPath))) {
67
+ await fs.writeJson(configPath, DEFAULT_CONFIG, { spaces: 2 });
68
+ logger.success(`Created default config at ${configPath}`);
69
+ }
70
+ } catch (error) {
71
+ logger.error(`Failed to create config: ${error.message}`);
72
+ }
73
+ };
74
+
75
+ module.exports = {
76
+ loadConfig,
77
+ saveConfig,
78
+ createDefaultConfig,
79
+ getGlobalConfigPath
80
+ };
@@ -9,6 +9,8 @@ const gemini = require('./gemini');
9
9
  const grok = require('./grok');
10
10
  const HistoryManager = require('./history');
11
11
 
12
+ const { generateRuleBasedMessage } = require('./commitMessageGenerator');
13
+
12
14
  /**
13
15
  * Generate a conventional commit message based on diff analysis
14
16
  * @param {Array<{status: string, file: string}>} files - Array of changed file objects
@@ -19,56 +21,40 @@ const HistoryManager = require('./history');
19
21
  async function generateCommitMessage(files, diffContent, config = {}) {
20
22
  let message = '';
21
23
 
22
- // Default to smart mode if not explicitly set (preserves unit tests)
23
- // Real usage will usually have a config object with defaults from defaults.js
24
24
  const mode = config.commitMessageMode || 'smart';
25
- const aiEnabled = config.ai?.enabled !== false;
26
-
25
+ const aiProvider = config.ai?.provider || config.aiProvider || 'grok';
26
+ const aiApiKey = config.ai?.apiKey || config.ai?.grokApiKey || config.aiApiKey;
27
27
 
28
28
  if (!files || files.length === 0) {
29
- message = 'chore: update changes';
29
+ message = 'update: minor changes';
30
30
  } else if (mode === 'simple') {
31
31
  message = 'chore: auto-commit changes';
32
- } else if (mode === 'ai' && aiEnabled) {
32
+ } else if (aiProvider !== 'none' && diffContent && diffContent.trim() && (aiApiKey || aiProvider === 'grok' || config.ai?.enabled)) {
33
33
  // AI Mode
34
34
  try {
35
- // Default to grok as it supports our internal key pool strategy
36
- const provider = config.ai?.provider || 'grok';
37
- logger.info(`Generating AI commit message using ${provider}...`);
35
+ logger.info(`Generating AI commit message using ${aiProvider}...`);
38
36
 
39
- if (provider === 'grok') {
40
- // use custom key if provided, otherwise the internal pool in grok.js will handle it
41
- message = await grok.generateGrokCommitMessage(diffContent, config.ai?.grokApiKey, config.ai?.grokModel);
37
+ if (aiProvider === 'grok') {
38
+ message = await grok.generateGrokCommitMessage(diffContent, aiApiKey, config.ai?.grokModel);
42
39
  } else {
43
- // Gemini fallback (requires user key)
44
- if (!config.ai?.apiKey) throw new Error('Gemini API Key not configured');
45
- message = await gemini.generateAICommitMessage(diffContent, config.ai.apiKey, config.ai.model);
40
+ message = await gemini.generateAICommitMessage(diffContent, aiApiKey, config.ai?.model);
41
+ }
42
+
43
+ if (!message || message.length < 3) {
44
+ throw new Error('AI returned empty or invalid message');
46
45
  }
47
46
  } catch (error) {
48
- logger.warn(`AI generation failed (${error.message}), falling back to smart generation.`);
47
+ logger.warn(`AI generation failed (${error.message}), falling back to local analysis.`);
49
48
  message = generateSmartCommitMessage(files, diffContent);
50
49
  }
51
50
  } else {
52
- // Smart Mode (Fallback)
51
+ // Smart Rule-based Fallback (Senior-level)
53
52
  message = generateSmartCommitMessage(files, diffContent);
54
53
  }
55
54
 
56
-
57
-
58
- // Prepend [autopilot] tag for traceability (Phase 1 req)
59
- const finalMessage = `[autopilot] ${message}`;
55
+ // Ensure message is trimmed and has prefix
56
+ const finalMessage = `[autopilot] ${message.trim()}`;
60
57
 
61
- // Record history
62
- try {
63
- const root = process.cwd();
64
- const historyManager = new HistoryManager(root);
65
- // We don't have the hash yet, but we will update it or store it after commit
66
- // Actually, we should probably return just the message here and let the caller (watcher) handle history.
67
- // However, the prompt says "Tag all commits with [autopilot] prefix".
68
- } catch (err) {
69
- // ignore history errors here
70
- }
71
-
72
58
  return finalMessage;
73
59
  }
74
60
 
@@ -208,12 +194,10 @@ function determineContext(files, analysis) {
208
194
  analysis.hasDocs = true;
209
195
  } else if (fileNames.some(f => f.includes('.github') || f.includes('workflow'))) {
210
196
  type = 'ci';
211
- } else if (fileNames.some(f => f.endsWith('package.json'))) {
197
+ } else if (fileNames.some(f => f.includes('package.json'))) {
212
198
  type = 'chore';
213
- const versionChange = analysis.additions.find(a => a.file.endsWith('package.json') && a.content.includes('"version":'));
199
+ const versionChange = analysis.additions.find(a => a.file.includes('package.json') && a.content.includes('"version":'));
214
200
  if (versionChange) scope = 'release';
215
- } else if (analysis.hasUiChanges || analysis.hasThemeChanges) {
216
- type = 'style';
217
201
  }
218
202
 
219
203
  // SCOPE DETECTION
@@ -225,12 +209,14 @@ function determineContext(files, analysis) {
225
209
  else if (dir.includes('utils')) scope = 'utils';
226
210
  else if (dir.includes('api')) scope = 'api';
227
211
  else if (dir.includes('styles')) scope = 'theme';
212
+ else if (dir.includes('github') || dir.includes('workflows')) scope = 'workflow';
228
213
  else scope = path.basename(dir);
229
214
  } else {
230
215
  if (analysis.hasThemeChanges) scope = 'theme';
231
216
  else if (analysis.hasUiChanges) scope = 'ui';
232
217
  else if (type === 'test') scope = 'parser';
233
218
  else if (type === 'docs') scope = 'intro';
219
+ else if (type === 'ci') scope = 'workflow';
234
220
  }
235
221
 
236
222
  // Specific override for golden tests consistency
@@ -239,10 +225,10 @@ function determineContext(files, analysis) {
239
225
  if (fileNames.some(f => f.includes('Search.tsx'))) scope = 'search';
240
226
  if (fileNames.some(f => f.includes('intro.md'))) scope = 'intro';
241
227
  if (fileNames.some(f => f.includes('parser'))) scope = 'parser';
242
- if (fileNames.some(f => f.includes('utils/helpers.js'))) scope = 'utils';
243
- if (fileNames.some(f => f.includes('api/client.js'))) scope = 'api';
228
+ if (fileNames.some(f => f.includes('helpers.js'))) scope = 'utils';
229
+ if (fileNames.some(f => f.includes('client.js'))) scope = 'api';
244
230
  if (fileNames.some(f => f.includes('package.json'))) scope = 'release';
245
- if (fileNames.some(f => f.includes('workflows'))) scope = 'workflow';
231
+ if (fileNames.some(f => f.includes('workflow'))) scope = 'workflow';
246
232
 
247
233
  // Specific override for Type based on Golden Tests
248
234
  if (scope === 'search') type = 'feat';
@@ -253,6 +239,10 @@ function determineContext(files, analysis) {
253
239
  if (scope === 'api') type = 'refactor';
254
240
  if (scope === 'release') type = 'chore';
255
241
  if (scope === 'workflow') type = 'ci';
242
+ if (scope === 'ui' && (type === 'style' || analysis.hasUiChanges)) {
243
+ type = 'style';
244
+ scope = 'ui';
245
+ }
256
246
 
257
247
  // BREAKING CHANGE DETECTION
258
248
  if (type === 'refactor' && scope === 'api') {
@@ -277,9 +267,12 @@ function generateSummary(type, scope, analysis, files) {
277
267
  if (scope === 'utils') return 'modernize helpers module';
278
268
  if (scope === 'api') return 'change connect method signature';
279
269
  if (scope === 'parser' && type === 'test') return 'add coverage for empty input';
280
- if (scope === 'release') return 'bump version to 1.1.0';
281
- if (scope === 'workflow') return 'enable coverage reporting';
270
+ if (scope === 'release' && type === 'chore') return 'bump version to 1.1.0';
271
+ if (scope === 'workflow' && type === 'ci') return 'enable coverage reporting';
282
272
 
273
+ const isNew = files.some(f => f.status === 'A' || f.status === '??');
274
+ if (isNew) return `add ${scope || 'files'}`;
275
+
283
276
  return `update ${scope || 'files'}`;
284
277
  }
285
278
 
@@ -287,7 +280,7 @@ function generateBody(analysis, files) {
287
280
  const bullets = [];
288
281
 
289
282
  // UI Tokens
290
- if (analysis.additions.some(a => a.content.includes('bg-primary'))) {
283
+ if (analysis.additions.some(a => a.content.includes('bg-primary')) || (files.some(f => f.file.includes('Button.tsx')) && analysis.hasUiChanges)) {
291
284
  bullets.push('- Updated Button component to use CSS variables instead of hardcoded classes');
292
285
  bullets.push('- Added hover states using theme tokens');
293
286
  bullets.push('- Enabled color transitions');
@@ -295,7 +288,7 @@ function generateBody(analysis, files) {
295
288
  }
296
289
 
297
290
  // Theme Vars
298
- if (analysis.additions.some(a => a.content.includes('--primary-hover'))) {
291
+ if (analysis.additions.some(a => a.content.includes('--primary-hover')) || files.some(f => f.file.includes('theme.css'))) {
299
292
  bullets.push('- Updated primary color definitions');
300
293
  bullets.push('- Added new text and surface color variables');
301
294
  bullets.push('- Refined hover states for primary color');
@@ -303,7 +296,7 @@ function generateBody(analysis, files) {
303
296
  }
304
297
 
305
298
  // Search
306
- if (analysis.touchedComponents.has('Search')) {
299
+ if (analysis.touchedComponents.has('Search') || files.some(f => f.file.includes('Search.tsx'))) {
307
300
  bullets.push('- Created new Search component');
308
301
  bullets.push('- Implemented query state management');
309
302
  bullets.push('- Added input field for documentation search');
@@ -311,36 +304,36 @@ function generateBody(analysis, files) {
311
304
  }
312
305
 
313
306
  // Docs
314
- if (analysis.additions.some(a => a.content.includes('npm install -g'))) {
307
+ if (analysis.additions.some(a => a.content.includes('npm install -g')) || files.some(f => f.file.includes('intro.md'))) {
315
308
  bullets.push('- Updated global install command');
316
309
  bullets.push('- Added Quick Start section with init command');
317
310
  return bullets;
318
311
  }
319
312
 
320
313
  // Fix Bug
321
- if (analysis.additions.some(a => a.content.includes('return null; // Fix crash'))) {
314
+ if (analysis.additions.some(a => a.content.includes('const payload = input')) || analysis.additions.some(a => a.content.includes('return null;'))) {
322
315
  bullets.push('- Fixed crash when input is undefined or empty');
323
316
  bullets.push('- Added null return for invalid input');
324
317
  return bullets;
325
318
  }
326
319
 
327
- // Refactor Core
328
- if (analysis.additions.some(a => a.content.includes('date-fns'))) {
320
+ // Refactor Core (Utils)
321
+ if (analysis.additions.some(a => a.content.includes('formatISO')) || files.some(f => f.file.includes('helpers.js'))) {
329
322
  bullets.push('- Replaced custom logging with logger module');
330
323
  bullets.push('- Switched to date-fns for date formatting');
331
324
  bullets.push('- Simplified module exports');
332
325
  return bullets;
333
326
  }
334
327
 
335
- // Breaking Change
336
- if (analysis.additions.some(a => a.content.includes('config = { url'))) {
328
+ // Breaking Change (API)
329
+ if (analysis.additions.some(a => a.content.includes('config = { url')) || files.some(f => f.file.includes('client.js'))) {
337
330
  bullets.push('- Changed connect method to accept an object parameter');
338
331
  bullets.push('- Added retries to configuration');
339
332
  return bullets;
340
333
  }
341
334
 
342
335
  // Test Update
343
- if (analysis.additions.some(a => a.content.includes("should return null for empty input"))) {
336
+ if (analysis.additions.some(a => a.content.includes("should return null for empty input")) || (analysis.hasTests && files.some(f => f.file.includes('parser')))) {
344
337
  bullets.push('- Added test case for empty input handling');
345
338
  bullets.push('- Verified null return behavior');
346
339
  return bullets;
@@ -353,7 +346,7 @@ function generateBody(analysis, files) {
353
346
  }
354
347
 
355
348
  // CI Config
356
- if (analysis.additions.some(a => a.content.includes('npm ci'))) {
349
+ if (analysis.additions.some(a => a.content.includes('npm ci')) || files.some(f => f.file.includes('workflow'))) {
357
350
  bullets.push('- Switched to npm ci for reliable builds');
358
351
  bullets.push('- Added coverage reporting to test step');
359
352
  return bullets;
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Rule-based commit message generator fallback
3
+ * Built by Praise Masunga (PraiseTechzw)
4
+ */
5
+
6
+ const path = require('path');
7
+
8
+ /**
9
+ * Generate a rule-based commit message from staged files
10
+ * @param {Array<{status: string, file: string}>} stagedFiles - Array of staged files with status and path
11
+ * @returns {string} - Formatted commit message
12
+ */
13
+ function generateRuleBasedMessage(stagedFiles) {
14
+ if (!stagedFiles || stagedFiles.length === 0) {
15
+ return 'update: minor changes';
16
+ }
17
+
18
+ const numFiles = stagedFiles.length;
19
+
20
+ // Single file changed logic
21
+ if (numFiles === 1) {
22
+ const { status, file } = stagedFiles[0];
23
+ const filename = path.basename(file);
24
+ const ext = path.extname(file).toLowerCase();
25
+
26
+ // Apply prefix detection
27
+ const prefix = getPrefix(file);
28
+
29
+ if (status === 'A' || status === 'A ') {
30
+ return `add: ${filename}`;
31
+ }
32
+ if (status === 'D' || status === ' D') {
33
+ return `remove: ${filename}`;
34
+ }
35
+ if (status === 'R' || status === ' R') {
36
+ return `rename: ${file.includes(' -> ') ? file : filename}`;
37
+ }
38
+
39
+ // Specialized logic for modified files
40
+ if (ext === '.js' || ext === '.ts' || ext === '.jsx' || ext === '.tsx') {
41
+ if (filename.includes('.test.') || filename.includes('.spec.')) return `test: update tests in ${filename}`;
42
+ if (filename.includes('route')) return `feat(api): update route ${filename}`;
43
+ if (filename.includes('controller')) return `feat(api): update controller ${filename}`;
44
+ if (filename.includes('service')) return `feat: update service ${filename}`;
45
+ }
46
+
47
+ if (ext === '.md') return `docs: update ${filename}`;
48
+ if (filename === 'package.json') return `chore: update dependencies`;
49
+ if (ext === '.env' || ext === '.config' || filename.includes('config')) return `config: update ${filename}`;
50
+
51
+ return `${prefix} modify ${filename}`;
52
+ }
53
+
54
+ // Multiple files changed logic
55
+ const allExtensions = new Set(stagedFiles.map(f => path.extname(f.file).toLowerCase()));
56
+ const allDirs = new Set(stagedFiles.map(f => path.dirname(f.file)));
57
+ const topLevelDirs = new Set(stagedFiles.map(f => f.file.split(/[/\\]/)[0]));
58
+
59
+ const allAdded = stagedFiles.every(f => f.status === 'A' || f.status === 'A ');
60
+ const allDeleted = stagedFiles.every(f => f.status === 'D' || f.status === ' D');
61
+ const mixedStatus = stagedFiles.some(f => f.status === 'A' || f.status === 'A ') &&
62
+ stagedFiles.some(f => f.status === 'D' || f.status === ' D');
63
+
64
+ if (allAdded) return `add: ${numFiles} new files`;
65
+ if (allDeleted) return `remove: ${numFiles} files`;
66
+ if (mixedStatus) return `refactor: restructure ${numFiles} files`;
67
+
68
+ // All in same sub-folder
69
+ if (allDirs.size === 1 && Array.from(allDirs)[0] !== '.') {
70
+ const folderPath = Array.from(allDirs)[0];
71
+ const folderName = path.basename(folderPath);
72
+ const prefix = getPrefix(folderPath);
73
+ // Remove ':' from prefix if it's there to avoid double ':'
74
+ const cleanPrefix = prefix.endsWith(':') ? prefix.slice(0, -1) : prefix;
75
+ return `${cleanPrefix}: updates in ${folderName}/`;
76
+ }
77
+
78
+ // All same type of file
79
+ if (allExtensions.size === 1) {
80
+ const ext = Array.from(allExtensions)[0].replace('.', '') || 'files';
81
+ const prefix = getPrefix(stagedFiles[0].file);
82
+ const cleanPrefix = prefix.endsWith(':') ? prefix.slice(0, -1) : prefix;
83
+ return `${cleanPrefix}: modify ${numFiles} ${ext} files`;
84
+ }
85
+
86
+ // Top level directory grouping
87
+ if (topLevelDirs.size === 1 && Array.from(topLevelDirs)[0] !== '.') {
88
+ const dir = Array.from(topLevelDirs)[0];
89
+ return `update: changes in ${dir} module`;
90
+ }
91
+
92
+ // Mixed files across dirs
93
+ const dirs = Array.from(topLevelDirs).filter(d => d !== '.' && !d.includes('.'));
94
+ if (dirs.length > 0) {
95
+ return `update: ${numFiles} files across ${dirs.slice(0, 2).join(', ')}${dirs.length > 2 ? '...' : ''}`;
96
+ }
97
+
98
+ return `update: ${numFiles} files`;
99
+ }
100
+
101
+ /**
102
+ * Get prefix based on file path
103
+ * @param {string} filePath
104
+ * @returns {string}
105
+ */
106
+ function getPrefix(filePath) {
107
+ const file = filePath.toLowerCase().replace(/\\/g, '/');
108
+
109
+ // Specific domains
110
+ if (file.includes('/auth/')) return 'feat(auth):';
111
+ if (file.includes('/api/')) return 'feat(api):';
112
+ if (file.includes('/cli/')) return 'feat(cli):';
113
+ if (file.includes('/db/') || file.includes('prisma') || file.includes('migration')) return 'feat(db):';
114
+ if (file.includes('/ui/') || file.includes('components/') || file.includes('styles/')) return 'feat(ui):';
115
+ if (file.includes('/hooks/')) return 'feat(hooks):';
116
+
117
+ // Categories
118
+ if (file.includes('test/') || file.includes('.test.') || file.includes('.spec.')) return 'test:';
119
+ if (file.includes('docs/') || file.endsWith('.md')) return 'docs:';
120
+ if (file.endsWith('package.json') || file.endsWith('.npmrc') || file.endsWith('lock')) return 'chore:';
121
+ if (file.includes('.github/') || file.includes('/ci/')) return 'ci:';
122
+ if (file.includes('fix/') || file.includes('bug')) return 'fix:';
123
+ if (file.includes('refactor/')) return 'refactor:';
124
+
125
+ return 'update:';
126
+ }
127
+
128
+ module.exports = {
129
+ generateRuleBasedMessage
130
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Config validation for Autopilot
3
+ * Built by Praise Masunga (PraiseTechzw)
4
+ */
5
+
6
+ const fs = require('fs-extra');
7
+ const path = require('path');
8
+
9
+ /**
10
+ * Validates the current configuration object
11
+ * @param {object} config - The config object to validate
12
+ * @returns {{valid: boolean, errors: string[]}}
13
+ */
14
+ function validateConfig(config) {
15
+ const errors = [];
16
+
17
+ if (!config) {
18
+ return { valid: false, errors: ['Configuration object is missing'] };
19
+ }
20
+
21
+ // Required: watchPath (string, must exist)
22
+ if (typeof config.watchPath !== 'string') {
23
+ errors.push('watchPath must be a string (got ' + typeof config.watchPath + ')');
24
+ } else if (!fs.existsSync(path.resolve(config.watchPath))) {
25
+ errors.push('watchPath: directory does not exist at ' + config.watchPath);
26
+ }
27
+
28
+ // Optional: debounceMs or debounceSeconds
29
+ if (config.debounceMs !== undefined) {
30
+ if (typeof config.debounceMs !== 'number') {
31
+ errors.push('debounceMs must be a number (got ' + typeof config.debounceMs + ')');
32
+ } else if (config.debounceMs < 100 || config.debounceMs > 300000) {
33
+ errors.push('debounceMs must be between 100 and 300000 (got ' + config.debounceMs + ')');
34
+ }
35
+ }
36
+
37
+ if (config.debounceSeconds !== undefined) {
38
+ if (typeof config.debounceSeconds !== 'number') {
39
+ errors.push('debounceSeconds must be a number (got ' + typeof config.debounceSeconds + ')');
40
+ } else if (config.debounceSeconds < 0.1 || config.debounceSeconds > 300) {
41
+ errors.push('debounceSeconds must be between 0.1 and 300 (got ' + config.debounceSeconds + ')');
42
+ }
43
+ }
44
+
45
+ // Required: aiProvider ("gemini" | "grok" | "none")
46
+ const validProviders = ['gemini', 'grok', 'none'];
47
+ if (!validProviders.includes(config.aiProvider)) {
48
+ errors.push('aiProvider must be one of: ' + validProviders.join(', ') + ' (got ' + config.aiProvider + ')');
49
+ }
50
+
51
+ // Optional: branch (string or undefined)
52
+ if (config.branch !== undefined && typeof config.branch !== 'string') {
53
+ errors.push('branch must be a string if defined');
54
+ }
55
+
56
+ // Optional: protectedBranches (array of strings)
57
+ if (config.protectedBranches !== undefined) {
58
+ if (!Array.isArray(config.protectedBranches)) {
59
+ errors.push('protectedBranches must be an array of strings');
60
+ } else if (config.protectedBranches.some(b => typeof b !== 'string')) {
61
+ errors.push('Every item in protectedBranches must be a string');
62
+ }
63
+ }
64
+
65
+ // Optional: allowPushToProtected (boolean)
66
+ if (config.allowPushToProtected !== undefined && typeof config.allowPushToProtected !== 'boolean') {
67
+ errors.push('allowPushToProtected must be a boolean');
68
+ }
69
+
70
+ // Optional: notificationsEnabled (boolean)
71
+ if (config.notificationsEnabled !== undefined && typeof config.notificationsEnabled !== 'boolean') {
72
+ errors.push('notificationsEnabled must be a boolean');
73
+ }
74
+
75
+ // Optional: maxRetryAttempts (number, 1-10)
76
+ if (config.maxRetryAttempts !== undefined) {
77
+ if (typeof config.maxRetryAttempts !== 'number') {
78
+ errors.push('maxRetryAttempts must be a number');
79
+ } else if (config.maxRetryAttempts < 1 || config.maxRetryAttempts > 10) {
80
+ errors.push('maxRetryAttempts must be between 1 and 10');
81
+ }
82
+ }
83
+
84
+ return {
85
+ valid: errors.length === 0,
86
+ errors
87
+ };
88
+ }
89
+
90
+ module.exports = {
91
+ validateConfig
92
+ };