devbrain-cli 0.1.0 → 0.2.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/dist/bin.js +50 -1
- package/dist/commands/history.command.d.ts +10 -0
- package/dist/commands/history.command.js +66 -0
- package/dist/commands/hook.command.d.ts +14 -0
- package/dist/commands/hook.command.js +40 -0
- package/dist/commands/init.command.d.ts +1 -0
- package/dist/commands/init.command.js +51 -15
- package/dist/commands/learn.command.d.ts +14 -4
- package/dist/commands/learn.command.js +148 -43
- package/dist/commands/status.command.d.ts +12 -0
- package/dist/commands/status.command.js +82 -0
- package/node_modules/@devbrain/core/dist/analysis/analyzers/source-code.analyzer.d.ts +24 -0
- package/node_modules/@devbrain/core/dist/analysis/analyzers/source-code.analyzer.js +126 -0
- package/node_modules/@devbrain/core/dist/context/context.service.d.ts +6 -7
- package/node_modules/@devbrain/core/dist/context/context.service.js +102 -16
- package/node_modules/@devbrain/core/dist/engine/dependency.resolver.d.ts +12 -0
- package/node_modules/@devbrain/core/dist/engine/dependency.resolver.js +101 -0
- package/node_modules/@devbrain/core/dist/engine/history.manager.d.ts +24 -0
- package/node_modules/@devbrain/core/dist/engine/history.manager.js +64 -0
- package/node_modules/@devbrain/core/dist/engine/lock.manager.d.ts +21 -0
- package/node_modules/@devbrain/core/dist/engine/lock.manager.js +89 -0
- package/node_modules/@devbrain/core/dist/engine/logger.service.d.ts +16 -0
- package/node_modules/@devbrain/core/dist/engine/logger.service.js +49 -0
- package/node_modules/@devbrain/core/dist/engine/memory.engine.d.ts +30 -0
- package/node_modules/@devbrain/core/dist/engine/memory.engine.js +200 -0
- package/node_modules/@devbrain/core/dist/git/git.service.d.ts +42 -0
- package/node_modules/@devbrain/core/dist/git/git.service.js +192 -0
- package/node_modules/@devbrain/core/dist/index.d.ts +7 -0
- package/node_modules/@devbrain/core/dist/index.js +7 -0
- package/node_modules/@devbrain/core/dist/memory/memory.service.d.ts +6 -3
- package/node_modules/@devbrain/core/dist/memory/memory.service.js +186 -6
- package/node_modules/@devbrain/core/package.json +2 -2
- package/node_modules/@devbrain/shared/dist/constants.d.ts +10 -2
- package/node_modules/@devbrain/shared/dist/constants.js +11 -2
- package/node_modules/@devbrain/shared/dist/types.d.ts +34 -0
- package/node_modules/@devbrain/shared/package.json +1 -1
- package/package.json +3 -3
package/dist/bin.js
CHANGED
|
@@ -3,6 +3,9 @@ import { Command } from 'commander';
|
|
|
3
3
|
import { InitCommand } from './commands/init.command.js';
|
|
4
4
|
import { LearnCommand } from './commands/learn.command.js';
|
|
5
5
|
import { ContextCommand } from './commands/context.command.js';
|
|
6
|
+
import { StatusCommand } from './commands/status.command.js';
|
|
7
|
+
import { HistoryCommand } from './commands/history.command.js';
|
|
8
|
+
import { HookCommand } from './commands/hook.command.js';
|
|
6
9
|
import { DEVBRAIN_VERSION } from '@devbrain/shared';
|
|
7
10
|
const program = new Command();
|
|
8
11
|
program
|
|
@@ -23,11 +26,17 @@ program
|
|
|
23
26
|
.command('learn')
|
|
24
27
|
.description('Scan repository files and update documentation memory deterministically')
|
|
25
28
|
.option('-d, --debug', 'Enable verbose debug outputs and print stack traces')
|
|
29
|
+
.option('-f, --force', 'Force a full scanned refresh of memory')
|
|
30
|
+
.option('--silent', 'Execute silently in the background')
|
|
26
31
|
.option('--no-gitignore', 'Skip matching .gitignore exclude rules')
|
|
27
32
|
.action(async (options) => {
|
|
28
33
|
const cwd = process.cwd();
|
|
29
34
|
const cmd = new LearnCommand();
|
|
30
|
-
await cmd.execute(cwd, !!options.debug, {
|
|
35
|
+
await cmd.execute(cwd, !!options.debug, {
|
|
36
|
+
force: !!options.force,
|
|
37
|
+
silent: !!options.silent,
|
|
38
|
+
respectGitignore: options.gitignore !== false,
|
|
39
|
+
});
|
|
31
40
|
});
|
|
32
41
|
program
|
|
33
42
|
.command('context')
|
|
@@ -39,6 +48,46 @@ program
|
|
|
39
48
|
const cmd = new ContextCommand();
|
|
40
49
|
await cmd.execute(cwd, !!options.debug, { raw: !!options.raw });
|
|
41
50
|
});
|
|
51
|
+
program
|
|
52
|
+
.command('status')
|
|
53
|
+
.description('Display autonomous Git integration status and repository statistics')
|
|
54
|
+
.option('-d, --debug', 'Enable verbose debug outputs and print stack traces')
|
|
55
|
+
.action(async (options) => {
|
|
56
|
+
const cwd = process.cwd();
|
|
57
|
+
const cmd = new StatusCommand();
|
|
58
|
+
await cmd.execute(cwd, !!options.debug, {});
|
|
59
|
+
});
|
|
60
|
+
program
|
|
61
|
+
.command('history')
|
|
62
|
+
.description('Display commit history processing logs and version metrics')
|
|
63
|
+
.option('-d, --debug', 'Enable verbose debug outputs and print stack traces')
|
|
64
|
+
.action(async (options) => {
|
|
65
|
+
const cwd = process.cwd();
|
|
66
|
+
const cmd = new HistoryCommand();
|
|
67
|
+
await cmd.execute(cwd, !!options.debug, {});
|
|
68
|
+
});
|
|
69
|
+
// Git Hook Subcommand group
|
|
70
|
+
const hookGroup = program
|
|
71
|
+
.command('hook')
|
|
72
|
+
.description('Manage DevBrain autonomous Git integration hooks');
|
|
73
|
+
hookGroup
|
|
74
|
+
.command('install')
|
|
75
|
+
.description('Install the post-commit git hook')
|
|
76
|
+
.option('-d, --debug', 'Enable verbose debug outputs and print stack traces')
|
|
77
|
+
.action(async (options) => {
|
|
78
|
+
const cwd = process.cwd();
|
|
79
|
+
const cmd = new HookCommand();
|
|
80
|
+
await cmd.execute(cwd, !!options.debug, { action: 'install' });
|
|
81
|
+
});
|
|
82
|
+
hookGroup
|
|
83
|
+
.command('remove')
|
|
84
|
+
.description('Remove the post-commit git hook')
|
|
85
|
+
.option('-d, --debug', 'Enable verbose debug outputs and print stack traces')
|
|
86
|
+
.action(async (options) => {
|
|
87
|
+
const cwd = process.cwd();
|
|
88
|
+
const cmd = new HookCommand();
|
|
89
|
+
await cmd.execute(cwd, !!options.debug, { action: 'remove' });
|
|
90
|
+
});
|
|
42
91
|
// Handle unknown commands
|
|
43
92
|
program.on('command:*', () => {
|
|
44
93
|
console.error('Invalid command: %s\nUse --help for a list of available commands.', program.args.join(' '));
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { CommandContext } from '@devbrain/shared';
|
|
2
|
+
import { CommandPipeline } from '../pipeline/command.pipeline.js';
|
|
3
|
+
export declare class HistoryCommand extends CommandPipeline {
|
|
4
|
+
private historyManager;
|
|
5
|
+
protected validate(context: CommandContext, _args: any): Promise<void>;
|
|
6
|
+
protected createContext(context: CommandContext, _args: any): Promise<void>;
|
|
7
|
+
protected executeService(context: CommandContext, _args: any): Promise<any>;
|
|
8
|
+
protected writeOutput(_context: CommandContext, _output: any, _args: any): Promise<void>;
|
|
9
|
+
protected reportResult(context: CommandContext, output: any, _args: any): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { DEVBRAIN_DIR_NAME, CONFIG_FILE_NAME, VERSION_FILE_NAME } from '@devbrain/shared';
|
|
3
|
+
import { HistoryManager } from '@devbrain/core';
|
|
4
|
+
import { CommandPipeline } from '../pipeline/command.pipeline.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
export class HistoryCommand extends CommandPipeline {
|
|
7
|
+
historyManager;
|
|
8
|
+
async validate(context, _args) {
|
|
9
|
+
const configPath = join(context.cwd, DEVBRAIN_DIR_NAME, CONFIG_FILE_NAME);
|
|
10
|
+
if (!(await this.fsService.exists(configPath))) {
|
|
11
|
+
throw new Error('DevBrain is not initialized. Run "devbrain init" first.');
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async createContext(context, _args) {
|
|
15
|
+
this.historyManager = new HistoryManager(context.cwd, this.fsService);
|
|
16
|
+
}
|
|
17
|
+
async executeService(context, _args) {
|
|
18
|
+
const history = await this.historyManager.loadHistory();
|
|
19
|
+
const versionPath = join(context.cwd, DEVBRAIN_DIR_NAME, VERSION_FILE_NAME);
|
|
20
|
+
let versionInfo = null;
|
|
21
|
+
if (await this.fsService.exists(versionPath)) {
|
|
22
|
+
try {
|
|
23
|
+
versionInfo = await this.fsService.readJson(versionPath);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
// Ignore
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
history,
|
|
31
|
+
versionInfo,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
async writeOutput(_context, _output, _args) {
|
|
35
|
+
// No-op
|
|
36
|
+
}
|
|
37
|
+
async reportResult(context, output, _args) {
|
|
38
|
+
this.logger.header('DevBrain Commit Processing History');
|
|
39
|
+
const history = output.history;
|
|
40
|
+
console.log(`- Latest Processed Commit: ${chalk.yellow(history.lastProcessedCommit || 'none')}`);
|
|
41
|
+
console.log(`- Memory Schema Version: ${chalk.green(output.versionInfo?.version || '0.1.0')}`);
|
|
42
|
+
console.log(`- Total Commits Learned: ${chalk.cyan(history.commits.length)}`);
|
|
43
|
+
console.log('');
|
|
44
|
+
if (history.commits.length === 0) {
|
|
45
|
+
console.log(chalk.gray('No commit learning logs recorded. Run a commit or "devbrain learn" to populate history.'));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
console.log(chalk.bold('Learned Commits Log:'));
|
|
49
|
+
// Display up to 10 recent commits
|
|
50
|
+
const displayedCommits = history.commits.slice(0, 10);
|
|
51
|
+
for (const commit of displayedCommits) {
|
|
52
|
+
const hash = chalk.yellow(commit.commitHash);
|
|
53
|
+
const date = chalk.gray(new Date(commit.timestamp).toLocaleString());
|
|
54
|
+
const msg = commit.commitMessage.split('\n')[0];
|
|
55
|
+
const statusIcon = commit.status === 'success'
|
|
56
|
+
? chalk.green('✔')
|
|
57
|
+
: commit.status === 'skipped' ? chalk.yellow('ℹ') : chalk.red('✖');
|
|
58
|
+
console.log(` ${statusIcon} ${hash} [${date}] - ${msg}`);
|
|
59
|
+
}
|
|
60
|
+
if (history.commits.length > 10) {
|
|
61
|
+
console.log(chalk.gray(`\n...and ${history.commits.length - 10} more older entries.`));
|
|
62
|
+
}
|
|
63
|
+
console.log('');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
//# sourceMappingURL=history.command.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CommandContext } from '@devbrain/shared';
|
|
2
|
+
import { CommandPipeline } from '../pipeline/command.pipeline.js';
|
|
3
|
+
interface HookArgs {
|
|
4
|
+
action: 'install' | 'remove';
|
|
5
|
+
}
|
|
6
|
+
export declare class HookCommand extends CommandPipeline<HookArgs> {
|
|
7
|
+
private gitService;
|
|
8
|
+
protected validate(context: CommandContext, _args: HookArgs): Promise<void>;
|
|
9
|
+
protected createContext(context: CommandContext, _args: HookArgs): Promise<void>;
|
|
10
|
+
protected executeService(context: CommandContext, args: HookArgs): Promise<void>;
|
|
11
|
+
protected writeOutput(_context: CommandContext, _output: any, _args: HookArgs): Promise<void>;
|
|
12
|
+
protected reportResult(context: CommandContext, _output: any, args: HookArgs): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { DEVBRAIN_DIR_NAME, CONFIG_FILE_NAME } from '@devbrain/shared';
|
|
3
|
+
import { GitService } from '@devbrain/core';
|
|
4
|
+
import { CommandPipeline } from '../pipeline/command.pipeline.js';
|
|
5
|
+
export class HookCommand extends CommandPipeline {
|
|
6
|
+
gitService;
|
|
7
|
+
async validate(context, _args) {
|
|
8
|
+
const configPath = join(context.cwd, DEVBRAIN_DIR_NAME, CONFIG_FILE_NAME);
|
|
9
|
+
if (!(await this.fsService.exists(configPath))) {
|
|
10
|
+
throw new Error('DevBrain is not initialized. Run "devbrain init" first.');
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
async createContext(context, _args) {
|
|
14
|
+
this.gitService = new GitService(context.cwd, this.fsService);
|
|
15
|
+
}
|
|
16
|
+
async executeService(context, args) {
|
|
17
|
+
const isGit = await this.gitService.isGitRepository();
|
|
18
|
+
if (!isGit) {
|
|
19
|
+
throw new Error('This workspace is not a valid Git repository.');
|
|
20
|
+
}
|
|
21
|
+
if (args.action === 'install') {
|
|
22
|
+
await this.gitService.installHook('post-commit');
|
|
23
|
+
}
|
|
24
|
+
else if (args.action === 'remove') {
|
|
25
|
+
await this.gitService.removeHook('post-commit');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async writeOutput(_context, _output, _args) {
|
|
29
|
+
// No-op
|
|
30
|
+
}
|
|
31
|
+
async reportResult(context, _output, args) {
|
|
32
|
+
if (args.action === 'install') {
|
|
33
|
+
this.logger.success('Successfully installed post-commit hook for DevBrain.');
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
this.logger.success('Successfully removed post-commit hook for DevBrain.');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=hook.command.js.map
|
|
@@ -7,6 +7,7 @@ interface InitArgs {
|
|
|
7
7
|
* Command pipeline for "devbrain init".
|
|
8
8
|
*/
|
|
9
9
|
export declare class InitCommand extends CommandPipeline<InitArgs> {
|
|
10
|
+
private hookInstalled;
|
|
10
11
|
protected validate(context: CommandContext, args: InitArgs): Promise<void>;
|
|
11
12
|
protected createContext(context: CommandContext, _args: InitArgs): Promise<void>;
|
|
12
13
|
protected executeService(context: CommandContext, _args: InitArgs): Promise<void>;
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { join, basename } from 'node:path';
|
|
2
|
-
import { ValidationError, DEVBRAIN_VERSION, DEFAULT_IGNORE_PATTERNS, DEVBRAIN_DIR_NAME, CONFIG_FILE_NAME, DEFAULT_MEMORY_DIR, } from '@devbrain/shared';
|
|
2
|
+
import { ValidationError, DEVBRAIN_VERSION, DEFAULT_IGNORE_PATTERNS, DEVBRAIN_DIR_NAME, CONFIG_FILE_NAME, VERSION_FILE_NAME, CUSTOM_NOTES_FILE_NAME, DEFAULT_MEMORY_DIR, } from '@devbrain/shared';
|
|
3
|
+
import { GitService } from '@devbrain/core';
|
|
3
4
|
import { CommandPipeline } from '../pipeline/command.pipeline.js';
|
|
4
5
|
/**
|
|
5
6
|
* Command pipeline for "devbrain init".
|
|
6
7
|
*/
|
|
7
8
|
export class InitCommand extends CommandPipeline {
|
|
9
|
+
hookInstalled = false;
|
|
8
10
|
async validate(context, args) {
|
|
9
11
|
const configPath = join(context.cwd, DEVBRAIN_DIR_NAME, CONFIG_FILE_NAME);
|
|
10
12
|
if (!args.force && (await this.fsService.exists(configPath))) {
|
|
@@ -12,7 +14,6 @@ export class InitCommand extends CommandPipeline {
|
|
|
12
14
|
}
|
|
13
15
|
}
|
|
14
16
|
async createContext(context, _args) {
|
|
15
|
-
// Setup default config parameters
|
|
16
17
|
const projectName = basename(context.cwd);
|
|
17
18
|
context.config = {
|
|
18
19
|
version: DEVBRAIN_VERSION,
|
|
@@ -28,30 +29,65 @@ export class InitCommand extends CommandPipeline {
|
|
|
28
29
|
format: 'markdown',
|
|
29
30
|
directory: DEFAULT_MEMORY_DIR,
|
|
30
31
|
},
|
|
32
|
+
auto_update: true,
|
|
33
|
+
enabled_hooks: ['post-commit'],
|
|
34
|
+
ignored_directories: [],
|
|
35
|
+
ignored_extensions: [],
|
|
36
|
+
maximum_incremental_files: 50,
|
|
37
|
+
documentation_generation: true,
|
|
38
|
+
context_generation: true,
|
|
39
|
+
verbose_logging: false,
|
|
40
|
+
safe_mode: true,
|
|
31
41
|
};
|
|
32
42
|
}
|
|
33
43
|
async executeService(context, _args) {
|
|
34
44
|
const devbrainPath = join(context.cwd, DEVBRAIN_DIR_NAME);
|
|
35
45
|
const configPath = join(devbrainPath, CONFIG_FILE_NAME);
|
|
36
|
-
const
|
|
37
|
-
|
|
46
|
+
const versionPath = join(devbrainPath, VERSION_FILE_NAME);
|
|
47
|
+
const customNotesPath = join(devbrainPath, CUSTOM_NOTES_FILE_NAME);
|
|
48
|
+
// Create directories & write config
|
|
38
49
|
await this.fsService.writeJson(configPath, context.config);
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
'',
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
50
|
+
// Create version.json
|
|
51
|
+
const versionData = {
|
|
52
|
+
version: DEVBRAIN_VERSION,
|
|
53
|
+
memorySchema: 2,
|
|
54
|
+
lastMigration: new Date().toISOString().split('T')[0],
|
|
55
|
+
};
|
|
56
|
+
await this.fsService.writeJson(versionPath, versionData);
|
|
57
|
+
// Write starter custom_notes.md if not present
|
|
58
|
+
if (!(await this.fsService.exists(customNotesPath))) {
|
|
59
|
+
const defaultCustomNotes = [
|
|
60
|
+
'# Custom Developer Notes',
|
|
61
|
+
'',
|
|
62
|
+
'Use this file to document manual project conventions, architecture diagrams, decision logs, or any other notes.',
|
|
63
|
+
'This file will NEVER be overwritten by DevBrain.',
|
|
64
|
+
'',
|
|
65
|
+
].join('\n');
|
|
66
|
+
await this.fsService.write(customNotesPath, defaultCustomNotes);
|
|
67
|
+
}
|
|
68
|
+
// Git Hook Integration
|
|
69
|
+
try {
|
|
70
|
+
const gitService = new GitService(context.cwd, this.fsService);
|
|
71
|
+
if (await gitService.isGitRepository()) {
|
|
72
|
+
await gitService.installHook('post-commit');
|
|
73
|
+
this.hookInstalled = true;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
catch (err) {
|
|
77
|
+
this.logger.warn(`Failed to configure Git hooks: ${err.message || err}`);
|
|
78
|
+
}
|
|
49
79
|
}
|
|
50
80
|
async writeOutput(_context, _output, _args) {
|
|
51
|
-
// No-op
|
|
81
|
+
// No-op
|
|
52
82
|
}
|
|
53
83
|
async reportResult(context, _output, _args) {
|
|
54
84
|
this.logger.success(`Initialized DevBrain project memory directory at ${join(context.cwd, DEVBRAIN_DIR_NAME)}`);
|
|
85
|
+
if (this.hookInstalled) {
|
|
86
|
+
this.logger.success('Automatically installed post-commit Git hook.');
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
this.logger.warn('Git hooks not installed (directory is not a Git repository).');
|
|
90
|
+
}
|
|
55
91
|
}
|
|
56
92
|
}
|
|
57
93
|
//# sourceMappingURL=init.command.js.map
|
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
import { CommandContext } from '@devbrain/shared';
|
|
2
2
|
import { CommandPipeline } from '../pipeline/command.pipeline.js';
|
|
3
3
|
interface LearnArgs {
|
|
4
|
+
force?: boolean;
|
|
5
|
+
silent?: boolean;
|
|
4
6
|
respectGitignore?: boolean;
|
|
5
7
|
}
|
|
6
8
|
/**
|
|
7
9
|
* Command pipeline for "devbrain learn".
|
|
8
10
|
*/
|
|
9
11
|
export declare class LearnCommand extends CommandPipeline<LearnArgs> {
|
|
10
|
-
private
|
|
12
|
+
private lockManager;
|
|
13
|
+
private loggerService;
|
|
14
|
+
private gitService;
|
|
15
|
+
private historyManager;
|
|
16
|
+
private memoryEngine;
|
|
17
|
+
private memoryService;
|
|
18
|
+
private contextService;
|
|
19
|
+
private stats;
|
|
11
20
|
protected validate(context: CommandContext, _args: LearnArgs): Promise<void>;
|
|
12
|
-
protected createContext(context: CommandContext,
|
|
13
|
-
|
|
21
|
+
protected createContext(context: CommandContext, _args: LearnArgs): Promise<void>;
|
|
22
|
+
execute(cwd: string, debug: boolean, args: LearnArgs): Promise<void>;
|
|
23
|
+
protected executeService(context: CommandContext, args: LearnArgs): Promise<any>;
|
|
14
24
|
protected writeOutput(_context: CommandContext, _output: any, _args: LearnArgs): Promise<void>;
|
|
15
|
-
protected reportResult(context: CommandContext,
|
|
25
|
+
protected reportResult(context: CommandContext, output: any, args: LearnArgs): Promise<void>;
|
|
16
26
|
}
|
|
17
27
|
export {};
|
|
@@ -1,72 +1,177 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
2
|
import { ValidationError, DEVBRAIN_DIR_NAME, CONFIG_FILE_NAME, } from '@devbrain/shared';
|
|
3
|
-
import {
|
|
3
|
+
import { GitService, LockManager, LoggerService, HistoryManager, DependencyResolver, MemoryEngine, MemoryService, ContextService, RepositoryScanner, } from '@devbrain/core';
|
|
4
4
|
import { CommandPipeline } from '../pipeline/command.pipeline.js';
|
|
5
5
|
import ora from 'ora';
|
|
6
6
|
/**
|
|
7
7
|
* Command pipeline for "devbrain learn".
|
|
8
8
|
*/
|
|
9
9
|
export class LearnCommand extends CommandPipeline {
|
|
10
|
-
|
|
10
|
+
lockManager;
|
|
11
|
+
loggerService;
|
|
12
|
+
gitService;
|
|
13
|
+
historyManager;
|
|
14
|
+
memoryEngine;
|
|
15
|
+
memoryService;
|
|
16
|
+
contextService;
|
|
17
|
+
stats = {
|
|
18
|
+
strategy: 'full',
|
|
19
|
+
filesCount: 0,
|
|
20
|
+
commitMessage: '',
|
|
21
|
+
commitHash: '',
|
|
22
|
+
};
|
|
11
23
|
async validate(context, _args) {
|
|
12
24
|
const configPath = join(context.cwd, DEVBRAIN_DIR_NAME, CONFIG_FILE_NAME);
|
|
13
25
|
if (!(await this.fsService.exists(configPath))) {
|
|
14
26
|
throw new ValidationError('DevBrain is not initialized in this repository.', 'Configuration file config.json not found.', 'Run "devbrain init" before executing "devbrain learn".');
|
|
15
27
|
}
|
|
16
28
|
}
|
|
17
|
-
async createContext(context,
|
|
18
|
-
|
|
29
|
+
async createContext(context, _args) {
|
|
30
|
+
this.lockManager = new LockManager(context.cwd, this.fsService);
|
|
31
|
+
this.loggerService = new LoggerService(context.cwd, this.fsService);
|
|
32
|
+
this.gitService = new GitService(context.cwd, this.fsService);
|
|
33
|
+
this.historyManager = new HistoryManager(context.cwd, this.fsService);
|
|
34
|
+
this.memoryEngine = new MemoryEngine(context.cwd, context.config, this.fsService);
|
|
35
|
+
this.memoryService = new MemoryService(this.fsService);
|
|
36
|
+
this.contextService = new ContextService(this.fsService);
|
|
37
|
+
}
|
|
38
|
+
async execute(cwd, debug, args) {
|
|
39
|
+
// Override execute method to gracefully handle lock and hook errors without throwing
|
|
40
|
+
const isSilent = !!args.silent;
|
|
19
41
|
try {
|
|
20
|
-
|
|
21
|
-
const respectGitignore = args.respectGitignore ?? context.config?.scanner.respectGitignore;
|
|
22
|
-
this.filesList = await scanner.scan(context.cwd, {
|
|
23
|
-
ignore: context.config?.scanner.ignore,
|
|
24
|
-
followSymlinks: context.config?.scanner.followSymlinks,
|
|
25
|
-
respectGitignore,
|
|
26
|
-
});
|
|
27
|
-
spinner.succeed(`Scanned repository: found ${this.filesList.length} files.`);
|
|
42
|
+
await super.execute(cwd, debug, args);
|
|
28
43
|
}
|
|
29
44
|
catch (error) {
|
|
30
|
-
|
|
31
|
-
|
|
45
|
+
// Log errors in silent hook mode instead of exiting with 1
|
|
46
|
+
const logService = new LoggerService(cwd, this.fsService);
|
|
47
|
+
await logService.error(`Execution error: ${error.message || error}`, 'git-hook.log');
|
|
48
|
+
if (isSilent) {
|
|
49
|
+
console.error(`DevBrain Hook error logged: ${error.message || error}`);
|
|
50
|
+
process.exit(0); // Never interrupt Git commit!
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
this.handleError(error, debug);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
32
56
|
}
|
|
33
57
|
}
|
|
34
|
-
async executeService(context,
|
|
35
|
-
const
|
|
58
|
+
async executeService(context, args) {
|
|
59
|
+
const isSilent = !!args.silent;
|
|
60
|
+
const forceFull = !!args.force;
|
|
61
|
+
// 1. Acquire Lock
|
|
62
|
+
const acquired = await this.lockManager.acquireLock();
|
|
63
|
+
if (!acquired) {
|
|
64
|
+
const msg = 'Another learning process is currently active or lock file exists.';
|
|
65
|
+
await this.loggerService.warn(msg, isSilent ? 'git-hook.log' : 'devbrain.log');
|
|
66
|
+
if (isSilent) {
|
|
67
|
+
return { skipped: true, reason: 'locked' };
|
|
68
|
+
}
|
|
69
|
+
throw new Error(msg + ' Ensure no other devbrain command is running, or remove .devbrain/.lock manually.');
|
|
70
|
+
}
|
|
71
|
+
const spinner = isSilent ? null : ora('DevBrain is updating project memory...').start();
|
|
36
72
|
try {
|
|
37
|
-
//
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
// 2. Fetch Git Metadata
|
|
74
|
+
const isGitRepo = await this.gitService.isGitRepository();
|
|
75
|
+
const lastCommit = isGitRepo ? await this.historyManager.getLastProcessedCommit() : null;
|
|
76
|
+
const currentCommit = isGitRepo ? await this.gitService.getCurrentCommitHash() : null;
|
|
77
|
+
if (currentCommit) {
|
|
78
|
+
this.stats.commitHash = currentCommit;
|
|
79
|
+
this.stats.commitMessage = await this.gitService.getCommitMessage(currentCommit);
|
|
80
|
+
}
|
|
81
|
+
// Check if memory.json exists
|
|
82
|
+
const memoryExists = await this.fsService.exists(join(context.cwd, DEVBRAIN_DIR_NAME, 'memory.json'));
|
|
83
|
+
let analysis;
|
|
84
|
+
// 3. Determine Incremental vs Full
|
|
85
|
+
if (forceFull || !isGitRepo || !lastCommit || !memoryExists) {
|
|
86
|
+
// Run Full Scan
|
|
87
|
+
this.stats.strategy = 'full';
|
|
88
|
+
if (spinner)
|
|
89
|
+
spinner.text = 'Scanning entire project (full scan)...';
|
|
90
|
+
analysis = await this.memoryEngine.runFullScan();
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
// Check if HEAD has actually changed
|
|
94
|
+
if (lastCommit === currentCommit) {
|
|
95
|
+
if (spinner) {
|
|
96
|
+
spinner.succeed('Project memory is already up to date with HEAD commit.');
|
|
97
|
+
}
|
|
98
|
+
await this.lockManager.releaseLock();
|
|
99
|
+
return { skipped: true, reason: 'up-to-date' };
|
|
100
|
+
}
|
|
101
|
+
// Run Incremental Scan
|
|
102
|
+
this.stats.strategy = 'incremental';
|
|
103
|
+
if (spinner)
|
|
104
|
+
spinner.text = 'Running incremental git-diff analysis...';
|
|
105
|
+
const changes = await this.gitService.getChangedFiles(lastCommit, currentCommit || 'HEAD');
|
|
106
|
+
// Scan directory files for context mapping
|
|
107
|
+
const scanner = new RepositoryScanner(this.fsService);
|
|
108
|
+
const allFiles = await scanner.scan(context.cwd, {
|
|
109
|
+
ignore: context.config?.scanner.ignore,
|
|
110
|
+
followSymlinks: context.config?.scanner.followSymlinks,
|
|
111
|
+
respectGitignore: args.respectGitignore ?? context.config?.scanner.respectGitignore,
|
|
112
|
+
});
|
|
113
|
+
const allChanged = [
|
|
114
|
+
...changes.modified,
|
|
115
|
+
...changes.deleted,
|
|
116
|
+
...changes.renamed.map((r) => r.to),
|
|
117
|
+
...changes.renamed.map((r) => r.from),
|
|
118
|
+
];
|
|
119
|
+
// Resolve blast radius dependencies
|
|
120
|
+
const resolver = new DependencyResolver();
|
|
121
|
+
const { resolvedFiles, isFullScan } = resolver.resolveBlastRadius(allChanged, allFiles);
|
|
122
|
+
if (isFullScan) {
|
|
123
|
+
this.stats.strategy = 'full (config change)';
|
|
124
|
+
if (spinner)
|
|
125
|
+
spinner.text = 'Core config changed: scanning entire project...';
|
|
126
|
+
analysis = await this.memoryEngine.runFullScan();
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
if (spinner)
|
|
130
|
+
spinner.text = `Analyzing ${resolvedFiles.length} files in blast radius...`;
|
|
131
|
+
analysis = await this.memoryEngine.incrementalLearn(resolvedFiles, changes.deleted, changes.renamed);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
this.stats.filesCount = analysis.files.length;
|
|
135
|
+
// 4. Update documentation formats if enabled
|
|
56
136
|
const memoryDir = join(context.cwd, context.config.memory.directory);
|
|
57
|
-
|
|
58
|
-
|
|
137
|
+
if (context.config.documentation_generation !== false) {
|
|
138
|
+
if (spinner)
|
|
139
|
+
spinner.text = 'Regenerating project memory documentation...';
|
|
140
|
+
await this.memoryService.generateMemory(analysis, memoryDir);
|
|
141
|
+
}
|
|
142
|
+
if (context.config.context_generation !== false) {
|
|
143
|
+
if (spinner)
|
|
144
|
+
spinner.text = 'Regenerating AI context payload...';
|
|
145
|
+
await this.contextService.generateContext(analysis, memoryDir);
|
|
146
|
+
}
|
|
147
|
+
// 5. Append commit entry to history
|
|
148
|
+
if (isGitRepo && currentCommit) {
|
|
149
|
+
const historyEntry = {
|
|
150
|
+
commitHash: currentCommit,
|
|
151
|
+
timestamp: new Date().toISOString(),
|
|
152
|
+
status: 'success',
|
|
153
|
+
filesAnalyzed: this.stats.strategy.startsWith('full') ? ['*'] : [],
|
|
154
|
+
commitMessage: this.stats.commitMessage,
|
|
155
|
+
};
|
|
156
|
+
await this.historyManager.addCommitEntry(historyEntry);
|
|
157
|
+
}
|
|
158
|
+
if (spinner) {
|
|
159
|
+
spinner.succeed(`Successfully learned changes (${this.stats.strategy} scan, ${this.stats.filesCount} files).`);
|
|
160
|
+
}
|
|
161
|
+
await this.loggerService.success(`Completed learning session: ${this.stats.strategy} scan.`, isSilent ? 'git-hook.log' : 'devbrain.log');
|
|
59
162
|
}
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
163
|
+
finally {
|
|
164
|
+
// 6. Release Lock
|
|
165
|
+
await this.lockManager.releaseLock();
|
|
63
166
|
}
|
|
64
167
|
}
|
|
65
168
|
async writeOutput(_context, _output, _args) {
|
|
66
|
-
//
|
|
169
|
+
// Handled in services
|
|
67
170
|
}
|
|
68
|
-
async reportResult(context,
|
|
69
|
-
|
|
171
|
+
async reportResult(context, output, args) {
|
|
172
|
+
if (args.silent || (output && output.skipped))
|
|
173
|
+
return;
|
|
174
|
+
this.logger.success(`Documentation and context regenerated at ${join(context.cwd, DEVBRAIN_DIR_NAME)}`);
|
|
70
175
|
}
|
|
71
176
|
}
|
|
72
177
|
//# sourceMappingURL=learn.command.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { CommandContext } from '@devbrain/shared';
|
|
2
|
+
import { CommandPipeline } from '../pipeline/command.pipeline.js';
|
|
3
|
+
export declare class StatusCommand extends CommandPipeline {
|
|
4
|
+
private gitService;
|
|
5
|
+
private lockManager;
|
|
6
|
+
private historyManager;
|
|
7
|
+
protected validate(context: CommandContext, _args: any): Promise<void>;
|
|
8
|
+
protected createContext(context: CommandContext, _args: any): Promise<void>;
|
|
9
|
+
protected executeService(context: CommandContext, _args: any): Promise<any>;
|
|
10
|
+
protected writeOutput(_context: CommandContext, _output: any, _args: any): Promise<void>;
|
|
11
|
+
protected reportResult(context: CommandContext, status: any, _args: any): Promise<void>;
|
|
12
|
+
}
|