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
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { unlink, stat } from 'node:fs/promises';
|
|
3
|
+
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
4
|
+
import { DEVBRAIN_DIR_NAME, LOCK_FILE_NAME } from '@devbrain/shared';
|
|
5
|
+
export class LockManager {
|
|
6
|
+
cwd;
|
|
7
|
+
fsService;
|
|
8
|
+
lockPath;
|
|
9
|
+
staleTimeoutMs = 10 * 60 * 1000; // 10 minutes
|
|
10
|
+
constructor(cwd, fsService = new FilesystemService()) {
|
|
11
|
+
this.cwd = cwd;
|
|
12
|
+
this.fsService = fsService;
|
|
13
|
+
this.lockPath = join(cwd, DEVBRAIN_DIR_NAME, LOCK_FILE_NAME);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Attempts to acquire the lock. Returns true if acquired, false otherwise.
|
|
17
|
+
*/
|
|
18
|
+
async acquireLock() {
|
|
19
|
+
try {
|
|
20
|
+
if (await this.fsService.exists(this.lockPath)) {
|
|
21
|
+
// Check if the lock is stale
|
|
22
|
+
const isStale = await this.checkIfStale();
|
|
23
|
+
if (isStale) {
|
|
24
|
+
// Release the stale lock
|
|
25
|
+
await this.releaseLock();
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Write current timestamp
|
|
32
|
+
const timestamp = Date.now().toString();
|
|
33
|
+
await this.fsService.writeAtomic(this.lockPath, timestamp);
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Releases the lock by deleting the lock file.
|
|
42
|
+
*/
|
|
43
|
+
async releaseLock() {
|
|
44
|
+
try {
|
|
45
|
+
if (await this.fsService.exists(this.lockPath)) {
|
|
46
|
+
await unlink(this.lockPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
// Ignore errors during release
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Checks if a lock currently exists and is active (not stale).
|
|
55
|
+
*/
|
|
56
|
+
async isLocked() {
|
|
57
|
+
try {
|
|
58
|
+
if (await this.fsService.exists(this.lockPath)) {
|
|
59
|
+
const isStale = await this.checkIfStale();
|
|
60
|
+
return !isStale;
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async checkIfStale() {
|
|
69
|
+
try {
|
|
70
|
+
const stats = await stat(this.lockPath);
|
|
71
|
+
const fileAgeMs = Date.now() - stats.mtimeMs;
|
|
72
|
+
if (fileAgeMs > this.staleTimeoutMs) {
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
// Or read timestamp from file content
|
|
76
|
+
const content = await this.fsService.read(this.lockPath);
|
|
77
|
+
const timestamp = parseInt(content.trim(), 10);
|
|
78
|
+
if (!isNaN(timestamp)) {
|
|
79
|
+
return Date.now() - timestamp > this.staleTimeoutMs;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
// If we cannot check, assume it is stale to prevent locking out permanently
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=lock.manager.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
2
|
+
export declare class LoggerService {
|
|
3
|
+
private readonly cwd;
|
|
4
|
+
private readonly fsService;
|
|
5
|
+
private readonly logDir;
|
|
6
|
+
constructor(cwd: string, fsService?: FilesystemService);
|
|
7
|
+
/**
|
|
8
|
+
* Log a message with a timestamp and log level to a specific file.
|
|
9
|
+
*/
|
|
10
|
+
log(message: string, level?: string, fileName?: string): Promise<void>;
|
|
11
|
+
info(message: string, fileName?: string): Promise<void>;
|
|
12
|
+
success(message: string, fileName?: string): Promise<void>;
|
|
13
|
+
warn(message: string, fileName?: string): Promise<void>;
|
|
14
|
+
error(message: string, fileName?: string): Promise<void>;
|
|
15
|
+
debug(message: string, fileName?: string): Promise<void>;
|
|
16
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { DEVBRAIN_DIR_NAME, LOGS_DIR_NAME } from '@devbrain/shared';
|
|
3
|
+
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
4
|
+
export class LoggerService {
|
|
5
|
+
cwd;
|
|
6
|
+
fsService;
|
|
7
|
+
logDir;
|
|
8
|
+
constructor(cwd, fsService = new FilesystemService()) {
|
|
9
|
+
this.cwd = cwd;
|
|
10
|
+
this.fsService = fsService;
|
|
11
|
+
this.logDir = join(cwd, DEVBRAIN_DIR_NAME, LOGS_DIR_NAME);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Log a message with a timestamp and log level to a specific file.
|
|
15
|
+
*/
|
|
16
|
+
async log(message, level = 'INFO', fileName = 'devbrain.log') {
|
|
17
|
+
try {
|
|
18
|
+
const timestamp = new Date().toISOString();
|
|
19
|
+
const logLine = `[${timestamp}] [${level}] ${message}\n`;
|
|
20
|
+
const targetPath = join(this.logDir, fileName);
|
|
21
|
+
// Create logs directory if it does not exist
|
|
22
|
+
if (!(await this.fsService.exists(this.logDir))) {
|
|
23
|
+
const { mkdir } = await import('node:fs/promises');
|
|
24
|
+
await mkdir(this.logDir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
const { appendFile } = await import('node:fs/promises');
|
|
27
|
+
await appendFile(targetPath, logLine, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Fallback silently if logging fails to prevent interrupting operations
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async info(message, fileName = 'devbrain.log') {
|
|
34
|
+
await this.log(message, 'INFO', fileName);
|
|
35
|
+
}
|
|
36
|
+
async success(message, fileName = 'devbrain.log') {
|
|
37
|
+
await this.log(message, 'SUCCESS', fileName);
|
|
38
|
+
}
|
|
39
|
+
async warn(message, fileName = 'devbrain.log') {
|
|
40
|
+
await this.log(message, 'WARN', fileName);
|
|
41
|
+
}
|
|
42
|
+
async error(message, fileName = 'devbrain.log') {
|
|
43
|
+
await this.log(message, 'ERROR', fileName);
|
|
44
|
+
}
|
|
45
|
+
async debug(message, fileName = 'devbrain.log') {
|
|
46
|
+
await this.log(message, 'DEBUG', fileName);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=logger.service.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { AggregatedAnalysis, DevBrainConfig } from '@devbrain/shared';
|
|
2
|
+
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
3
|
+
export declare class MemoryEngine {
|
|
4
|
+
private readonly cwd;
|
|
5
|
+
private readonly config;
|
|
6
|
+
private readonly fsService;
|
|
7
|
+
private readonly memoryPath;
|
|
8
|
+
private readonly registry;
|
|
9
|
+
constructor(cwd: string, config: DevBrainConfig, fsService?: FilesystemService);
|
|
10
|
+
/**
|
|
11
|
+
* Loads memory.json or runs a full scan to create it.
|
|
12
|
+
*/
|
|
13
|
+
loadMemory(): Promise<AggregatedAnalysis>;
|
|
14
|
+
/**
|
|
15
|
+
* Persists memory.json.
|
|
16
|
+
*/
|
|
17
|
+
saveMemory(memory: AggregatedAnalysis): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Run a full project scan.
|
|
20
|
+
*/
|
|
21
|
+
runFullScan(): Promise<AggregatedAnalysis>;
|
|
22
|
+
/**
|
|
23
|
+
* Performs an incremental learning phase on modified/deleted/renamed files.
|
|
24
|
+
*/
|
|
25
|
+
incrementalLearn(modifiedFiles: string[], deletedFiles: string[], renamedFiles: {
|
|
26
|
+
from: string;
|
|
27
|
+
to: string;
|
|
28
|
+
}[]): Promise<AggregatedAnalysis>;
|
|
29
|
+
private mergeIncrementalResults;
|
|
30
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { DEVBRAIN_DIR_NAME, MEMORY_FILE_NAME } from '@devbrain/shared';
|
|
3
|
+
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
4
|
+
import { AnalyzerRegistry } from '../analysis/analyzer.registry.js';
|
|
5
|
+
import { ReadmeAnalyzer } from '../analysis/analyzers/readme.analyzer.js';
|
|
6
|
+
import { PackageJsonAnalyzer } from '../analysis/analyzers/package-json.analyzer.js';
|
|
7
|
+
import { TsconfigAnalyzer } from '../analysis/analyzers/tsconfig.analyzer.js';
|
|
8
|
+
import { GitAnalyzer } from '../analysis/analyzers/git.analyzer.js';
|
|
9
|
+
import { DockerfileAnalyzer } from '../analysis/analyzers/docker.analyzer.js';
|
|
10
|
+
import { RequirementsTxtAnalyzer } from '../analysis/analyzers/requirements-txt.analyzer.js';
|
|
11
|
+
import { PomXmlAnalyzer } from '../analysis/analyzers/pom-xml.analyzer.js';
|
|
12
|
+
import { BuildGradleAnalyzer } from '../analysis/analyzers/build-gradle.analyzer.js';
|
|
13
|
+
import { SourceCodeAnalyzer } from '../analysis/analyzers/source-code.analyzer.js';
|
|
14
|
+
import { RepositoryScanner } from '../analysis/scanner.js';
|
|
15
|
+
export class MemoryEngine {
|
|
16
|
+
cwd;
|
|
17
|
+
config;
|
|
18
|
+
fsService;
|
|
19
|
+
memoryPath;
|
|
20
|
+
registry;
|
|
21
|
+
constructor(cwd, config, fsService = new FilesystemService()) {
|
|
22
|
+
this.cwd = cwd;
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.fsService = fsService;
|
|
25
|
+
this.memoryPath = join(cwd, DEVBRAIN_DIR_NAME, MEMORY_FILE_NAME);
|
|
26
|
+
// Setup analyzer registry
|
|
27
|
+
this.registry = new AnalyzerRegistry();
|
|
28
|
+
this.registry.register(new ReadmeAnalyzer(this.fsService));
|
|
29
|
+
this.registry.register(new PackageJsonAnalyzer(this.fsService));
|
|
30
|
+
this.registry.register(new TsconfigAnalyzer(this.fsService));
|
|
31
|
+
this.registry.register(new GitAnalyzer(this.fsService));
|
|
32
|
+
this.registry.register(new DockerfileAnalyzer(this.fsService));
|
|
33
|
+
this.registry.register(new RequirementsTxtAnalyzer(this.fsService));
|
|
34
|
+
this.registry.register(new PomXmlAnalyzer(this.fsService));
|
|
35
|
+
this.registry.register(new BuildGradleAnalyzer(this.fsService));
|
|
36
|
+
this.registry.register(new SourceCodeAnalyzer(this.fsService));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Loads memory.json or runs a full scan to create it.
|
|
40
|
+
*/
|
|
41
|
+
async loadMemory() {
|
|
42
|
+
try {
|
|
43
|
+
if (await this.fsService.exists(this.memoryPath)) {
|
|
44
|
+
return await this.fsService.readJson(this.memoryPath);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// Fall through to full scan if error
|
|
49
|
+
}
|
|
50
|
+
// Run full scan if no memory file
|
|
51
|
+
return await this.runFullScan();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Persists memory.json.
|
|
55
|
+
*/
|
|
56
|
+
async saveMemory(memory) {
|
|
57
|
+
await this.fsService.writeJson(this.memoryPath, memory);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Run a full project scan.
|
|
61
|
+
*/
|
|
62
|
+
async runFullScan() {
|
|
63
|
+
const scanner = new RepositoryScanner(this.fsService);
|
|
64
|
+
const files = await scanner.scan(this.cwd, {
|
|
65
|
+
ignore: this.config.scanner.ignore,
|
|
66
|
+
followSymlinks: this.config.scanner.followSymlinks,
|
|
67
|
+
respectGitignore: this.config.scanner.respectGitignore,
|
|
68
|
+
});
|
|
69
|
+
const projectContext = {
|
|
70
|
+
cwd: this.cwd,
|
|
71
|
+
files,
|
|
72
|
+
config: this.config,
|
|
73
|
+
};
|
|
74
|
+
const analysis = await this.registry.runAll(projectContext);
|
|
75
|
+
await this.saveMemory(analysis);
|
|
76
|
+
return analysis;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Performs an incremental learning phase on modified/deleted/renamed files.
|
|
80
|
+
*/
|
|
81
|
+
async incrementalLearn(modifiedFiles, deletedFiles, renamedFiles) {
|
|
82
|
+
const memory = await this.loadMemory();
|
|
83
|
+
// 1. Process deletions
|
|
84
|
+
const deletedSet = new Set(deletedFiles);
|
|
85
|
+
for (const rename of renamedFiles) {
|
|
86
|
+
deletedSet.add(rename.from);
|
|
87
|
+
}
|
|
88
|
+
// Filter memory files list
|
|
89
|
+
memory.files = memory.files.filter((f) => !deletedSet.has(f));
|
|
90
|
+
// Handle source code file removal from modules cache
|
|
91
|
+
if (memory.analyzers['source-code']) {
|
|
92
|
+
const scData = memory.analyzers['source-code'];
|
|
93
|
+
if (scData.modules) {
|
|
94
|
+
scData.modules = scData.modules.filter((m) => !deletedSet.has(m.filePath));
|
|
95
|
+
}
|
|
96
|
+
if (scData.apis) {
|
|
97
|
+
scData.apis = scData.apis.filter((a) => !deletedSet.has(a.file));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// 2. Process renames (add the 'to' file)
|
|
101
|
+
for (const rename of renamedFiles) {
|
|
102
|
+
if (!memory.files.includes(rename.to)) {
|
|
103
|
+
memory.files.push(rename.to);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
// 3. Process modifications and additions
|
|
107
|
+
const filesToAnalyze = [...modifiedFiles, ...renamedFiles.map((r) => r.to)];
|
|
108
|
+
// Add unique entries to files list
|
|
109
|
+
for (const file of filesToAnalyze) {
|
|
110
|
+
if (!memory.files.includes(file)) {
|
|
111
|
+
memory.files.push(file);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (filesToAnalyze.length > 0) {
|
|
115
|
+
const projectContext = {
|
|
116
|
+
cwd: this.cwd,
|
|
117
|
+
files: filesToAnalyze,
|
|
118
|
+
config: this.config,
|
|
119
|
+
};
|
|
120
|
+
const incrementalAnalysis = await this.registry.runAll(projectContext);
|
|
121
|
+
// Merge incremental analysis results back to main memory
|
|
122
|
+
this.mergeIncrementalResults(memory, incrementalAnalysis, filesToAnalyze);
|
|
123
|
+
}
|
|
124
|
+
// Re-aggregate globals from updated source code modules
|
|
125
|
+
if (memory.analyzers['source-code']) {
|
|
126
|
+
const scData = memory.analyzers['source-code'];
|
|
127
|
+
const dbEntities = new Set();
|
|
128
|
+
const authMethods = new Set();
|
|
129
|
+
const extServices = new Set();
|
|
130
|
+
if (scData.modules) {
|
|
131
|
+
for (const mod of scData.modules) {
|
|
132
|
+
if (mod.type === 'model' && mod.classes) {
|
|
133
|
+
for (const c of mod.classes)
|
|
134
|
+
dbEntities.add(c);
|
|
135
|
+
}
|
|
136
|
+
// Scan for Auth in name
|
|
137
|
+
if (/auth|jwt|passport|login|session/i.test(mod.name)) {
|
|
138
|
+
authMethods.add('Token/Credentials');
|
|
139
|
+
}
|
|
140
|
+
// imports
|
|
141
|
+
if (mod.imports) {
|
|
142
|
+
for (const imp of mod.imports) {
|
|
143
|
+
if (['axios', 'stripe', 'twilio', 'aws-sdk', 'redis', '@google-cloud/'].some(s => imp.includes(s))) {
|
|
144
|
+
extServices.add(imp);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
scData.databaseEntities = Array.from(dbEntities);
|
|
151
|
+
scData.authMethods = Array.from(authMethods);
|
|
152
|
+
scData.externalServices = Array.from(extServices);
|
|
153
|
+
}
|
|
154
|
+
await this.saveMemory(memory);
|
|
155
|
+
return memory;
|
|
156
|
+
}
|
|
157
|
+
mergeIncrementalResults(main, inc, analyzedFiles) {
|
|
158
|
+
// Merge core lists
|
|
159
|
+
main.technologies = Array.from(new Set([...main.technologies, ...inc.technologies]));
|
|
160
|
+
main.frameworks = Array.from(new Set([...main.frameworks, ...inc.frameworks]));
|
|
161
|
+
Object.assign(main.metadata, inc.metadata);
|
|
162
|
+
const analyzedSet = new Set(analyzedFiles);
|
|
163
|
+
// Merge analyzer blocks
|
|
164
|
+
for (const [id, incData] of Object.entries(inc.analyzers)) {
|
|
165
|
+
if (incData.error)
|
|
166
|
+
continue;
|
|
167
|
+
if (id === 'source-code') {
|
|
168
|
+
if (!main.analyzers['source-code']) {
|
|
169
|
+
main.analyzers['source-code'] = { modules: [], apis: [] };
|
|
170
|
+
}
|
|
171
|
+
const mainSc = main.analyzers['source-code'];
|
|
172
|
+
// Remove existing items for analyzed files
|
|
173
|
+
if (mainSc.modules) {
|
|
174
|
+
mainSc.modules = mainSc.modules.filter((m) => !analyzedSet.has(m.filePath));
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
mainSc.modules = [];
|
|
178
|
+
}
|
|
179
|
+
if (mainSc.apis) {
|
|
180
|
+
mainSc.apis = mainSc.apis.filter((a) => !analyzedSet.has(a.file));
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
mainSc.apis = [];
|
|
184
|
+
}
|
|
185
|
+
// Add new items
|
|
186
|
+
if (incData.modules) {
|
|
187
|
+
mainSc.modules.push(...incData.modules);
|
|
188
|
+
}
|
|
189
|
+
if (incData.apis) {
|
|
190
|
+
mainSc.apis.push(...incData.apis);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
// Configuration files analyzers return repo-wide details, overwrite them
|
|
195
|
+
main.analyzers[id] = incData;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=memory.engine.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
2
|
+
export declare class GitService {
|
|
3
|
+
private readonly cwd;
|
|
4
|
+
private readonly fsService;
|
|
5
|
+
private readonly git;
|
|
6
|
+
constructor(cwd: string, fsService?: FilesystemService);
|
|
7
|
+
/**
|
|
8
|
+
* Check if the directory is a valid git repository.
|
|
9
|
+
*/
|
|
10
|
+
isGitRepository(): Promise<boolean>;
|
|
11
|
+
/**
|
|
12
|
+
* Retrieve the current HEAD commit hash.
|
|
13
|
+
*/
|
|
14
|
+
getCurrentCommitHash(): Promise<string | null>;
|
|
15
|
+
/**
|
|
16
|
+
* Get the list of modified/added/deleted files between two commits.
|
|
17
|
+
*/
|
|
18
|
+
getChangedFiles(sinceHash: string, toHash?: string): Promise<{
|
|
19
|
+
modified: string[];
|
|
20
|
+
deleted: string[];
|
|
21
|
+
renamed: {
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
}[];
|
|
25
|
+
}>;
|
|
26
|
+
/**
|
|
27
|
+
* Get the commit message of the specified commit hash.
|
|
28
|
+
*/
|
|
29
|
+
getCommitMessage(hash: string): Promise<string>;
|
|
30
|
+
/**
|
|
31
|
+
* Install post-commit Git hook.
|
|
32
|
+
*/
|
|
33
|
+
installHook(hookName?: string): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Remove post-commit Git hook.
|
|
36
|
+
*/
|
|
37
|
+
removeHook(hookName?: string): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Check if hook is installed and managed by DevBrain.
|
|
40
|
+
*/
|
|
41
|
+
isHookInstalled(hookName?: string): Promise<boolean>;
|
|
42
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { chmod } from 'node:fs/promises';
|
|
3
|
+
import { simpleGit } from 'simple-git';
|
|
4
|
+
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
5
|
+
export class GitService {
|
|
6
|
+
cwd;
|
|
7
|
+
fsService;
|
|
8
|
+
git;
|
|
9
|
+
constructor(cwd, fsService = new FilesystemService()) {
|
|
10
|
+
this.cwd = cwd;
|
|
11
|
+
this.fsService = fsService;
|
|
12
|
+
this.git = simpleGit({ baseDir: cwd });
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Check if the directory is a valid git repository.
|
|
16
|
+
*/
|
|
17
|
+
async isGitRepository() {
|
|
18
|
+
try {
|
|
19
|
+
return await this.git.checkIsRepo();
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Retrieve the current HEAD commit hash.
|
|
27
|
+
*/
|
|
28
|
+
async getCurrentCommitHash() {
|
|
29
|
+
try {
|
|
30
|
+
const hash = await this.git.revparse(['HEAD']);
|
|
31
|
+
return hash.trim();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Get the list of modified/added/deleted files between two commits.
|
|
39
|
+
*/
|
|
40
|
+
async getChangedFiles(sinceHash, toHash = 'HEAD') {
|
|
41
|
+
try {
|
|
42
|
+
// Get raw diff summary
|
|
43
|
+
const diffSummary = await this.git.diffSummary([sinceHash, toHash]);
|
|
44
|
+
const modified = [];
|
|
45
|
+
const deleted = [];
|
|
46
|
+
const renamed = [];
|
|
47
|
+
for (const file of diffSummary.files) {
|
|
48
|
+
// simple-git's diffSummary indicates file operations
|
|
49
|
+
if (file.file.includes(' => ')) {
|
|
50
|
+
// File rename format: path/to/{old => new}/file or old => new
|
|
51
|
+
const renameMatch = file.file.match(/^(.*?)\{(.*?) => (.*?)\}(.*)$/) ||
|
|
52
|
+
file.file.match(/^(.*?) => (.*)$/);
|
|
53
|
+
if (renameMatch) {
|
|
54
|
+
let from = '';
|
|
55
|
+
let to = '';
|
|
56
|
+
if (renameMatch.length === 5) {
|
|
57
|
+
const prefix = renameMatch[1];
|
|
58
|
+
const oldPart = renameMatch[2];
|
|
59
|
+
const newPart = renameMatch[3];
|
|
60
|
+
const suffix = renameMatch[4];
|
|
61
|
+
from = `${prefix}${oldPart}${suffix}`.replace('//', '/');
|
|
62
|
+
to = `${prefix}${newPart}${suffix}`.replace('//', '/');
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
from = renameMatch[1].trim();
|
|
66
|
+
to = renameMatch[2].trim();
|
|
67
|
+
}
|
|
68
|
+
renamed.push({ from, to });
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
// Fallback if regex mismatch
|
|
72
|
+
modified.push(file.file);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
// Check if deleted
|
|
77
|
+
// In git diff-tree or summary, we check if changes are deletions
|
|
78
|
+
// diffSummary includes changes. We can verify if the file exists locally to confirm status.
|
|
79
|
+
const exists = await this.fsService.exists(join(this.cwd, file.file));
|
|
80
|
+
if (!exists) {
|
|
81
|
+
deleted.push(file.file);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
modified.push(file.file);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { modified, deleted, renamed };
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return { modified: [], deleted: [], renamed: [] };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Get the commit message of the specified commit hash.
|
|
96
|
+
*/
|
|
97
|
+
async getCommitMessage(hash) {
|
|
98
|
+
try {
|
|
99
|
+
const message = await this.git.show(['-s', '--format=%B', hash]);
|
|
100
|
+
return message.trim();
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return 'No commit message';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Install post-commit Git hook.
|
|
108
|
+
*/
|
|
109
|
+
async installHook(hookName = 'post-commit') {
|
|
110
|
+
const hooksDir = join(this.cwd, '.git', 'hooks');
|
|
111
|
+
if (!(await this.fsService.exists(hooksDir))) {
|
|
112
|
+
throw new Error('Not a Git repository or hooks directory is missing.');
|
|
113
|
+
}
|
|
114
|
+
const hookPath = join(hooksDir, hookName);
|
|
115
|
+
// Construct robust POSIX shell script template that executes async
|
|
116
|
+
const hookContent = [
|
|
117
|
+
'#!/bin/sh',
|
|
118
|
+
'# DevBrain Autonomous Git Memory Engine Hook',
|
|
119
|
+
'',
|
|
120
|
+
'# Set base path',
|
|
121
|
+
'PROJECT_DIR="$(pwd)"',
|
|
122
|
+
'LOG_FILE="$PROJECT_DIR/.devbrain/logs/git-hook.log"',
|
|
123
|
+
'LOCK_FILE="$PROJECT_DIR/.devbrain/.lock"',
|
|
124
|
+
'',
|
|
125
|
+
'mkdir -p "$PROJECT_DIR/.devbrain/logs"',
|
|
126
|
+
'',
|
|
127
|
+
'# Run in background to not block commit',
|
|
128
|
+
'(',
|
|
129
|
+
' # Check lock file',
|
|
130
|
+
' if [ -f "$LOCK_FILE" ]; then',
|
|
131
|
+
' echo "$(date): Skip execution: Another DevBrain process is running." >> "$LOG_FILE"',
|
|
132
|
+
' exit 0',
|
|
133
|
+
' fi',
|
|
134
|
+
'',
|
|
135
|
+
' echo "$(date): Triggering incremental learn..." >> "$LOG_FILE"',
|
|
136
|
+
'',
|
|
137
|
+
' # Resolve and run devbrain',
|
|
138
|
+
' if [ -f "$PROJECT_DIR/node_modules/.bin/devbrain" ]; then',
|
|
139
|
+
' "$PROJECT_DIR/node_modules/.bin/devbrain" learn --silent >> "$LOG_FILE" 2>&1',
|
|
140
|
+
' elif command -v devbrain >/dev/null 2>&1; then',
|
|
141
|
+
' devbrain learn --silent >> "$LOG_FILE" 2>&1',
|
|
142
|
+
' else',
|
|
143
|
+
' npx --no-install devbrain learn --silent >> "$LOG_FILE" 2>&1',
|
|
144
|
+
' fi',
|
|
145
|
+
' ',
|
|
146
|
+
' # Log result status code',
|
|
147
|
+
' STATUS=$?',
|
|
148
|
+
' if [ $STATUS -ne 0 ]; then',
|
|
149
|
+
' echo "$(date): Error: DevBrain execution failed with code $STATUS" >> "$LOG_FILE"',
|
|
150
|
+
' else',
|
|
151
|
+
' echo "$(date): DevBrain learning updated successfully." >> "$LOG_FILE"',
|
|
152
|
+
' fi',
|
|
153
|
+
') >/dev/null 2>&1 &',
|
|
154
|
+
'',
|
|
155
|
+
'exit 0',
|
|
156
|
+
].join('\n') + '\n';
|
|
157
|
+
await this.fsService.write(hookPath, hookContent);
|
|
158
|
+
// Make script executable
|
|
159
|
+
try {
|
|
160
|
+
await chmod(hookPath, 0o755);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
// Mode modification might fail on Windows, fallback silently
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Remove post-commit Git hook.
|
|
168
|
+
*/
|
|
169
|
+
async removeHook(hookName = 'post-commit') {
|
|
170
|
+
const hookPath = join(this.cwd, '.git', 'hooks', hookName);
|
|
171
|
+
if (await this.fsService.exists(hookPath)) {
|
|
172
|
+
const content = await this.fsService.read(hookPath);
|
|
173
|
+
// Only delete if it's managed by DevBrain
|
|
174
|
+
if (content.includes('DevBrain Autonomous')) {
|
|
175
|
+
const { unlink } = await import('node:fs/promises');
|
|
176
|
+
await unlink(hookPath);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Check if hook is installed and managed by DevBrain.
|
|
182
|
+
*/
|
|
183
|
+
async isHookInstalled(hookName = 'post-commit') {
|
|
184
|
+
const hookPath = join(this.cwd, '.git', 'hooks', hookName);
|
|
185
|
+
if (await this.fsService.exists(hookPath)) {
|
|
186
|
+
const content = await this.fsService.read(hookPath);
|
|
187
|
+
return content.includes('DevBrain Autonomous');
|
|
188
|
+
}
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
//# sourceMappingURL=git.service.js.map
|
|
@@ -4,6 +4,12 @@ export { Analyzer } from './analysis/analyzer.interface.js';
|
|
|
4
4
|
export { AnalyzerRegistry } from './analysis/analyzer.registry.js';
|
|
5
5
|
export { MemoryService } from './memory/memory.service.js';
|
|
6
6
|
export { ContextService } from './context/context.service.js';
|
|
7
|
+
export { GitService } from './git/git.service.js';
|
|
8
|
+
export { LockManager } from './engine/lock.manager.js';
|
|
9
|
+
export { LoggerService } from './engine/logger.service.js';
|
|
10
|
+
export { HistoryManager } from './engine/history.manager.js';
|
|
11
|
+
export { DependencyResolver } from './engine/dependency.resolver.js';
|
|
12
|
+
export { MemoryEngine } from './engine/memory.engine.js';
|
|
7
13
|
export { ReadmeAnalyzer } from './analysis/analyzers/readme.analyzer.js';
|
|
8
14
|
export { PackageJsonAnalyzer } from './analysis/analyzers/package-json.analyzer.js';
|
|
9
15
|
export { TsconfigAnalyzer } from './analysis/analyzers/tsconfig.analyzer.js';
|
|
@@ -12,3 +18,4 @@ export { DockerfileAnalyzer } from './analysis/analyzers/docker.analyzer.js';
|
|
|
12
18
|
export { RequirementsTxtAnalyzer } from './analysis/analyzers/requirements-txt.analyzer.js';
|
|
13
19
|
export { PomXmlAnalyzer } from './analysis/analyzers/pom-xml.analyzer.js';
|
|
14
20
|
export { BuildGradleAnalyzer } from './analysis/analyzers/build-gradle.analyzer.js';
|
|
21
|
+
export { SourceCodeAnalyzer } from './analysis/analyzers/source-code.analyzer.js';
|
|
@@ -4,6 +4,12 @@ export { RepositoryScanner } from './analysis/scanner.js';
|
|
|
4
4
|
export { AnalyzerRegistry } from './analysis/analyzer.registry.js';
|
|
5
5
|
export { MemoryService } from './memory/memory.service.js';
|
|
6
6
|
export { ContextService } from './context/context.service.js';
|
|
7
|
+
export { GitService } from './git/git.service.js';
|
|
8
|
+
export { LockManager } from './engine/lock.manager.js';
|
|
9
|
+
export { LoggerService } from './engine/logger.service.js';
|
|
10
|
+
export { HistoryManager } from './engine/history.manager.js';
|
|
11
|
+
export { DependencyResolver } from './engine/dependency.resolver.js';
|
|
12
|
+
export { MemoryEngine } from './engine/memory.engine.js';
|
|
7
13
|
// Concrete Analyzer exports
|
|
8
14
|
export { ReadmeAnalyzer } from './analysis/analyzers/readme.analyzer.js';
|
|
9
15
|
export { PackageJsonAnalyzer } from './analysis/analyzers/package-json.analyzer.js';
|
|
@@ -13,4 +19,5 @@ export { DockerfileAnalyzer } from './analysis/analyzers/docker.analyzer.js';
|
|
|
13
19
|
export { RequirementsTxtAnalyzer } from './analysis/analyzers/requirements-txt.analyzer.js';
|
|
14
20
|
export { PomXmlAnalyzer } from './analysis/analyzers/pom-xml.analyzer.js';
|
|
15
21
|
export { BuildGradleAnalyzer } from './analysis/analyzers/build-gradle.analyzer.js';
|
|
22
|
+
export { SourceCodeAnalyzer } from './analysis/analyzers/source-code.analyzer.js';
|
|
16
23
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { AggregatedAnalysis } from '@devbrain/shared';
|
|
2
2
|
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
3
|
-
/**
|
|
4
|
-
* Service responsible for generating deterministic, human-readable markdown project documentation.
|
|
5
|
-
*/
|
|
6
3
|
export declare class MemoryService {
|
|
7
4
|
private readonly fsService;
|
|
8
5
|
constructor(fsService?: FilesystemService);
|
|
@@ -10,6 +7,12 @@ export declare class MemoryService {
|
|
|
10
7
|
* Generates memory markdown files and writes them to the specified directory.
|
|
11
8
|
*/
|
|
12
9
|
generateMemory(analysis: AggregatedAnalysis, memoryDir: string): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Section-based markdown writer to prevent redundant Git diffs.
|
|
12
|
+
*/
|
|
13
|
+
private writeSectionBasedMarkdown;
|
|
14
|
+
private parseSections;
|
|
15
|
+
private buildProjectMemory;
|
|
13
16
|
private buildSummary;
|
|
14
17
|
private buildStack;
|
|
15
18
|
private buildArchitecture;
|