@traisetech/autopilot 2.4.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.
- package/CHANGELOG.md +25 -9
- package/README.md +215 -106
- package/bin/autopilot.js +1 -1
- package/docs/CONFIGURATION.md +103 -103
- package/docs/DESIGN_PRINCIPLES.md +114 -114
- package/docs/TEAM-MODE.md +51 -51
- package/docs/TROUBLESHOOTING.md +21 -21
- package/package.json +75 -69
- package/src/commands/config.js +110 -110
- package/src/commands/dashboard.mjs +151 -151
- package/src/commands/doctor.js +127 -153
- package/src/commands/init.js +8 -9
- package/src/commands/insights.js +237 -237
- package/src/commands/leaderboard.js +116 -116
- package/src/commands/pause.js +18 -18
- package/src/commands/preset.js +121 -121
- package/src/commands/resume.js +17 -17
- package/src/commands/start.js +41 -41
- package/src/commands/status.js +73 -39
- package/src/commands/stop.js +58 -50
- package/src/commands/undo.js +84 -84
- package/src/config/defaults.js +23 -16
- package/src/config/ignore.js +14 -31
- package/src/config/loader.js +80 -80
- package/src/core/commit.js +45 -52
- package/src/core/commitMessageGenerator.js +130 -0
- package/src/core/configValidator.js +92 -0
- package/src/core/events.js +110 -110
- package/src/core/focus.js +2 -1
- package/src/core/gemini.js +15 -15
- package/src/core/git.js +29 -2
- package/src/core/history.js +69 -69
- package/src/core/notifier.js +61 -0
- package/src/core/retryQueue.js +152 -0
- package/src/core/safety.js +224 -210
- package/src/core/state.js +69 -71
- package/src/core/watcher.js +193 -66
- package/src/index.js +70 -70
- package/src/utils/banner.js +6 -6
- package/src/utils/crypto.js +18 -18
- package/src/utils/identity.js +41 -41
- package/src/utils/logger.js +86 -68
- package/src/utils/paths.js +62 -62
- package/src/utils/process.js +141 -141
package/src/config/loader.js
CHANGED
|
@@ -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
|
+
};
|
package/src/core/commit.js
CHANGED
|
@@ -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
|
|
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 = '
|
|
29
|
+
message = 'update: minor changes';
|
|
30
30
|
} else if (mode === 'simple') {
|
|
31
31
|
message = 'chore: auto-commit changes';
|
|
32
|
-
} else if (
|
|
32
|
+
} else if (aiProvider !== 'none' && diffContent && diffContent.trim() && (aiApiKey || aiProvider === 'grok' || config.ai?.enabled)) {
|
|
33
33
|
// AI Mode
|
|
34
34
|
try {
|
|
35
|
-
|
|
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 (
|
|
40
|
-
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
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
|
|
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.
|
|
197
|
+
} else if (fileNames.some(f => f.includes('package.json'))) {
|
|
212
198
|
type = 'chore';
|
|
213
|
-
const versionChange = analysis.additions.find(a => a.file.
|
|
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('
|
|
243
|
-
if (fileNames.some(f => f.includes('
|
|
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('
|
|
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('
|
|
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('
|
|
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
|
+
};
|