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,82 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { DEVBRAIN_DIR_NAME, CONFIG_FILE_NAME, VERSION_FILE_NAME } from '@devbrain/shared';
|
|
3
|
+
import { GitService, LockManager, HistoryManager } from '@devbrain/core';
|
|
4
|
+
import { CommandPipeline } from '../pipeline/command.pipeline.js';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
export class StatusCommand extends CommandPipeline {
|
|
7
|
+
gitService;
|
|
8
|
+
lockManager;
|
|
9
|
+
historyManager;
|
|
10
|
+
async validate(context, _args) {
|
|
11
|
+
const configPath = join(context.cwd, DEVBRAIN_DIR_NAME, CONFIG_FILE_NAME);
|
|
12
|
+
if (!(await this.fsService.exists(configPath))) {
|
|
13
|
+
throw new Error('DevBrain is not initialized. Run "devbrain init" first.');
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async createContext(context, _args) {
|
|
17
|
+
this.gitService = new GitService(context.cwd, this.fsService);
|
|
18
|
+
this.lockManager = new LockManager(context.cwd, this.fsService);
|
|
19
|
+
this.historyManager = new HistoryManager(context.cwd, this.fsService);
|
|
20
|
+
}
|
|
21
|
+
async executeService(context, _args) {
|
|
22
|
+
const isGitRepo = await this.gitService.isGitRepository();
|
|
23
|
+
const isHookInstalled = isGitRepo ? await this.gitService.isHookInstalled('post-commit') : false;
|
|
24
|
+
const isLocked = await this.lockManager.isLocked();
|
|
25
|
+
const versionPath = join(context.cwd, DEVBRAIN_DIR_NAME, VERSION_FILE_NAME);
|
|
26
|
+
let versionInfo = null;
|
|
27
|
+
if (await this.fsService.exists(versionPath)) {
|
|
28
|
+
try {
|
|
29
|
+
versionInfo = await this.fsService.readJson(versionPath);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
// Fallback
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const history = await this.historyManager.loadHistory();
|
|
36
|
+
const lastCommit = history.lastProcessedCommit || 'none';
|
|
37
|
+
// Count tracked files in memory.json
|
|
38
|
+
const memoryPath = join(context.cwd, DEVBRAIN_DIR_NAME, 'memory.json');
|
|
39
|
+
let filesCount = 0;
|
|
40
|
+
if (await this.fsService.exists(memoryPath)) {
|
|
41
|
+
try {
|
|
42
|
+
const memoryData = await this.fsService.readJson(memoryPath);
|
|
43
|
+
filesCount = memoryData.files?.length || 0;
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// Ignore
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
isGitRepo,
|
|
51
|
+
isHookInstalled,
|
|
52
|
+
isLocked,
|
|
53
|
+
versionInfo,
|
|
54
|
+
lastCommit,
|
|
55
|
+
filesCount,
|
|
56
|
+
config: context.config,
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
async writeOutput(_context, _output, _args) {
|
|
60
|
+
// No-op
|
|
61
|
+
}
|
|
62
|
+
async reportResult(context, status, _args) {
|
|
63
|
+
this.logger.header('DevBrain Status Board');
|
|
64
|
+
console.log(`- Version: ${chalk.green(status.versionInfo?.version || '0.1.0')}`);
|
|
65
|
+
console.log(`- Memory Schema: ${chalk.cyan(status.versionInfo?.memorySchema || '1')}`);
|
|
66
|
+
console.log(`- Last Processed Commit: ${chalk.yellow(status.lastCommit)}`);
|
|
67
|
+
console.log(`- Files Tracked in Memory: ${chalk.blue(status.filesCount)}`);
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log(chalk.bold('Integrations & Processes:'));
|
|
70
|
+
console.log(`- Git Repository: ${status.isGitRepo ? chalk.green('Yes') : chalk.red('No')}`);
|
|
71
|
+
console.log(`- Post-commit Git Hook Installed: ${status.isHookInstalled ? chalk.green('Yes') : chalk.yellow('No')}`);
|
|
72
|
+
console.log(`- Lock Active: ${status.isLocked ? chalk.red('Yes') : chalk.green('No (Idle)')}`);
|
|
73
|
+
console.log('');
|
|
74
|
+
console.log(chalk.bold('Configuration Parameters:'));
|
|
75
|
+
console.log(`- Auto Update: ${status.config.auto_update !== false ? chalk.green('Enabled') : chalk.yellow('Disabled')}`);
|
|
76
|
+
console.log(`- Documentation Gen: ${status.config.documentation_generation !== false ? chalk.green('Enabled') : chalk.yellow('Disabled')}`);
|
|
77
|
+
console.log(`- AI Context Gen: ${status.config.context_generation !== false ? chalk.green('Enabled') : chalk.yellow('Disabled')}`);
|
|
78
|
+
console.log(`- Safe Mode: ${status.config.safe_mode !== false ? chalk.green('Enabled') : chalk.yellow('Disabled')}`);
|
|
79
|
+
console.log('');
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=status.command.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ProjectContext, AnalysisResult } from '@devbrain/shared';
|
|
2
|
+
import { Analyzer } from '../analyzer.interface.js';
|
|
3
|
+
import { FilesystemService } from '../../filesystem/filesystem.service.js';
|
|
4
|
+
export interface SourceModule {
|
|
5
|
+
filePath: string;
|
|
6
|
+
name: string;
|
|
7
|
+
type: 'service' | 'controller' | 'route' | 'model' | 'other';
|
|
8
|
+
classes: string[];
|
|
9
|
+
functions: string[];
|
|
10
|
+
imports: string[];
|
|
11
|
+
}
|
|
12
|
+
export interface SourceApi {
|
|
13
|
+
method: string;
|
|
14
|
+
path: string;
|
|
15
|
+
file: string;
|
|
16
|
+
}
|
|
17
|
+
export declare class SourceCodeAnalyzer implements Analyzer {
|
|
18
|
+
private readonly fsService;
|
|
19
|
+
readonly id = "source-code";
|
|
20
|
+
readonly name = "Source Code Structure Analyzer";
|
|
21
|
+
constructor(fsService?: FilesystemService);
|
|
22
|
+
supports(project: ProjectContext): Promise<boolean>;
|
|
23
|
+
analyze(project: ProjectContext): Promise<AnalysisResult>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { join, extname, basename } from 'node:path';
|
|
2
|
+
import { FilesystemService } from '../../filesystem/filesystem.service.js';
|
|
3
|
+
export class SourceCodeAnalyzer {
|
|
4
|
+
fsService;
|
|
5
|
+
id = 'source-code';
|
|
6
|
+
name = 'Source Code Structure Analyzer';
|
|
7
|
+
constructor(fsService = new FilesystemService()) {
|
|
8
|
+
this.fsService = fsService;
|
|
9
|
+
}
|
|
10
|
+
async supports(project) {
|
|
11
|
+
// Supports if there are any TS/JS source files in the project list
|
|
12
|
+
return project.files.some((f) => /\.(ts|js|tsx|jsx)$/.test(f) && !f.includes('node_modules') && !f.includes('dist'));
|
|
13
|
+
}
|
|
14
|
+
async analyze(project) {
|
|
15
|
+
const modules = [];
|
|
16
|
+
const apis = [];
|
|
17
|
+
const databaseEntities = new Set();
|
|
18
|
+
const authMethods = new Set();
|
|
19
|
+
const externalServices = new Set();
|
|
20
|
+
// Process only TS/JS source files up to a reasonable limit (e.g. 500 files for speed)
|
|
21
|
+
const sourceFiles = project.files
|
|
22
|
+
.filter((f) => /\.(ts|js|tsx|jsx)$/.test(f) && !f.includes('node_modules') && !f.includes('dist') && !f.endsWith('.test.ts') && !f.endsWith('.spec.ts'))
|
|
23
|
+
.slice(0, 500);
|
|
24
|
+
for (const file of sourceFiles) {
|
|
25
|
+
try {
|
|
26
|
+
const fullPath = join(project.cwd, file);
|
|
27
|
+
const content = await this.fsService.read(fullPath);
|
|
28
|
+
const baseName = basename(file);
|
|
29
|
+
const ext = extname(file);
|
|
30
|
+
const nameWithoutExt = baseName.slice(0, -ext.length);
|
|
31
|
+
// 1. Determine Module Type
|
|
32
|
+
let type = 'other';
|
|
33
|
+
if (/controller/i.test(nameWithoutExt))
|
|
34
|
+
type = 'controller';
|
|
35
|
+
else if (/service/i.test(nameWithoutExt))
|
|
36
|
+
type = 'service';
|
|
37
|
+
else if (/route/i.test(nameWithoutExt))
|
|
38
|
+
type = 'route';
|
|
39
|
+
else if (/model/i.test(nameWithoutExt) || /entity/i.test(nameWithoutExt))
|
|
40
|
+
type = 'model';
|
|
41
|
+
// 2. Classes
|
|
42
|
+
const classes = [];
|
|
43
|
+
const classRegex = /class\s+([A-Za-z0-9_]+)/g;
|
|
44
|
+
let match;
|
|
45
|
+
while ((match = classRegex.exec(content)) !== null) {
|
|
46
|
+
classes.push(match[1]);
|
|
47
|
+
if (type === 'model') {
|
|
48
|
+
databaseEntities.add(match[1]);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// 3. Functions
|
|
52
|
+
const functions = [];
|
|
53
|
+
const funcRegex = /(?:export\s+)?(?:async\s+)?function\s+([A-Za-z0-9_]+)/g;
|
|
54
|
+
while ((match = funcRegex.exec(content)) !== null) {
|
|
55
|
+
functions.push(match[1]);
|
|
56
|
+
}
|
|
57
|
+
const arrowFuncRegex = /(?:const|let|var)\s+([A-Za-z0-9_]+)\s*=\s*(?:async\s*)?\((?:[^)]*)\)\s*=>/g;
|
|
58
|
+
while ((match = arrowFuncRegex.exec(content)) !== null) {
|
|
59
|
+
functions.push(match[1]);
|
|
60
|
+
}
|
|
61
|
+
// 4. Imports / External Services
|
|
62
|
+
const imports = [];
|
|
63
|
+
const importRegex = /(?:import|require)\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
|
|
64
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
65
|
+
const importSource = match[1];
|
|
66
|
+
imports.push(importSource);
|
|
67
|
+
// Detect external service integrations
|
|
68
|
+
if (['axios', 'stripe', 'twilio', 'aws-sdk', 'redis', '@google-cloud/'].some(s => importSource.includes(s))) {
|
|
69
|
+
externalServices.add(importSource);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// 5. API Endpoints
|
|
73
|
+
// Match express-like router: router.get('/path', ...) or app.post('/path', ...)
|
|
74
|
+
const expressApiRegex = /(?:app|router|route|express)\.(get|post|put|delete|patch)\s*\(\s*['"]([^'"]+)['"]/gi;
|
|
75
|
+
while ((match = expressApiRegex.exec(content)) !== null) {
|
|
76
|
+
apis.push({
|
|
77
|
+
method: match[1].toUpperCase(),
|
|
78
|
+
path: match[2],
|
|
79
|
+
file,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Match NestJS decorators: @Get('/path') or @Post()
|
|
83
|
+
const nestApiDecoratorRegex = /@(Get|Post|Put|Delete|Patch)\s*\(\s*(?:['"]([^'"]+)['"])?/g;
|
|
84
|
+
while ((match = nestApiDecoratorRegex.exec(content)) !== null) {
|
|
85
|
+
apis.push({
|
|
86
|
+
method: match[1].toUpperCase(),
|
|
87
|
+
path: match[2] || '/',
|
|
88
|
+
file,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// 6. Authentication detection
|
|
92
|
+
if (/jwt|passport|bcrypt|oauth|session|authHeader|bearer/i.test(content)) {
|
|
93
|
+
if (content.includes('jwt') || content.includes('JWT'))
|
|
94
|
+
authMethods.add('JWT');
|
|
95
|
+
if (content.includes('oauth') || content.includes('OAuth'))
|
|
96
|
+
authMethods.add('OAuth');
|
|
97
|
+
if (content.includes('session'))
|
|
98
|
+
authMethods.add('Session-based');
|
|
99
|
+
}
|
|
100
|
+
modules.push({
|
|
101
|
+
filePath: file,
|
|
102
|
+
name: nameWithoutExt,
|
|
103
|
+
type,
|
|
104
|
+
classes,
|
|
105
|
+
functions: functions.slice(0, 15), // cap function list
|
|
106
|
+
imports,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// Skip unparseable files
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
analyzerId: this.id,
|
|
115
|
+
analyzerName: this.name,
|
|
116
|
+
data: {
|
|
117
|
+
modules,
|
|
118
|
+
apis: apis.slice(0, 100), // cap API endpoints
|
|
119
|
+
databaseEntities: Array.from(databaseEntities),
|
|
120
|
+
authMethods: Array.from(authMethods),
|
|
121
|
+
externalServices: Array.from(externalServices),
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=source-code.analyzer.js.map
|
|
@@ -1,17 +1,16 @@
|
|
|
1
|
+
import { AggregatedAnalysis } from '@devbrain/shared';
|
|
1
2
|
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
2
|
-
/**
|
|
3
|
-
* Service responsible for reading generated project memory and consolidating it into an optimized AI prompt context.
|
|
4
|
-
*/
|
|
5
3
|
export declare class ContextService {
|
|
6
4
|
private readonly fsService;
|
|
7
5
|
constructor(fsService?: FilesystemService);
|
|
8
6
|
/**
|
|
9
|
-
*
|
|
7
|
+
* Generates and writes context.md to the target directory.
|
|
10
8
|
*/
|
|
11
|
-
|
|
9
|
+
generateContext(analysis: AggregatedAnalysis, memoryDir: string): Promise<string>;
|
|
12
10
|
/**
|
|
13
|
-
*
|
|
14
|
-
* to maintain a single root h1 structure and optimizes whitespaces.
|
|
11
|
+
* Reads project memory and compiles it into a single token-efficient string.
|
|
15
12
|
*/
|
|
13
|
+
buildContext(memoryDir: string): Promise<string>;
|
|
14
|
+
private buildContextString;
|
|
16
15
|
private normalizeHeadings;
|
|
17
16
|
}
|
|
@@ -1,18 +1,48 @@
|
|
|
1
|
-
import { join } from 'node:path';
|
|
1
|
+
import { join, dirname, basename } from 'node:path';
|
|
2
|
+
import { CONTEXT_FILE_NAME, MEMORY_FILE_NAME, ValidationError, } from '@devbrain/shared';
|
|
2
3
|
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
3
|
-
import { ValidationError } from '@devbrain/shared';
|
|
4
|
-
/**
|
|
5
|
-
* Service responsible for reading generated project memory and consolidating it into an optimized AI prompt context.
|
|
6
|
-
*/
|
|
7
4
|
export class ContextService {
|
|
8
5
|
fsService;
|
|
9
6
|
constructor(fsService = new FilesystemService()) {
|
|
10
7
|
this.fsService = fsService;
|
|
11
8
|
}
|
|
12
9
|
/**
|
|
13
|
-
*
|
|
10
|
+
* Generates and writes context.md to the target directory.
|
|
11
|
+
*/
|
|
12
|
+
async generateContext(analysis, memoryDir) {
|
|
13
|
+
const targetDir = basename(memoryDir) === 'memory' ? dirname(memoryDir) : memoryDir;
|
|
14
|
+
const contextPath = join(targetDir, CONTEXT_FILE_NAME);
|
|
15
|
+
const contextContent = this.buildContextString(analysis);
|
|
16
|
+
const fileContent = [
|
|
17
|
+
'<!-- ⚠️ THIS FILE IS AUTO-GENERATED BY DEVBRAIN. -->',
|
|
18
|
+
'<!-- DO NOT EDIT. YOUR CHANGES WILL BE OVERWRITTEN. -->',
|
|
19
|
+
'',
|
|
20
|
+
contextContent,
|
|
21
|
+
].join('\n');
|
|
22
|
+
await this.fsService.writeAtomic(contextPath, fileContent);
|
|
23
|
+
return contextContent;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Reads project memory and compiles it into a single token-efficient string.
|
|
14
27
|
*/
|
|
15
28
|
async buildContext(memoryDir) {
|
|
29
|
+
const targetDir = basename(memoryDir) === 'memory' ? dirname(memoryDir) : memoryDir;
|
|
30
|
+
const jsonPath = join(targetDir, MEMORY_FILE_NAME);
|
|
31
|
+
// If memory.json exists, build context from it
|
|
32
|
+
if (await this.fsService.exists(jsonPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const analysis = await this.fsService.readJson(jsonPath);
|
|
35
|
+
return this.buildContextString(analysis);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// Fall back to folder scanning if parsing fails
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Ensure memory folder exists if memory.json is not present (backward compatibility check)
|
|
42
|
+
if (!(await this.fsService.exists(memoryDir))) {
|
|
43
|
+
throw new ValidationError('Project memory directory not found.', `The directory ${memoryDir} does not exist.`, 'Run "devbrain learn" to scan the repository and populate the memory files first.');
|
|
44
|
+
}
|
|
45
|
+
// Fallback: Read individual .md files (v0.1 structure)
|
|
16
46
|
const files = [
|
|
17
47
|
'summary.md',
|
|
18
48
|
'stack.md',
|
|
@@ -22,17 +52,13 @@ export class ContextService {
|
|
|
22
52
|
'timeline.md',
|
|
23
53
|
];
|
|
24
54
|
const parts = [];
|
|
25
|
-
// Ensure memory folder exists
|
|
26
|
-
if (!(await this.fsService.exists(memoryDir))) {
|
|
27
|
-
throw new ValidationError('Project memory directory not found.', `The directory ${memoryDir} does not exist.`, 'Run "devbrain learn" to scan the repository and populate the memory files first.');
|
|
28
|
-
}
|
|
29
55
|
parts.push('<!-- START OF DEVBRAIN AI ASSISTANT CONTEXT SCHEMA -->');
|
|
30
56
|
parts.push('# DEVBRAIN PROJECT CONTEXT INDEX');
|
|
31
57
|
parts.push('');
|
|
32
58
|
for (const file of files) {
|
|
33
59
|
const filePath = join(memoryDir, file);
|
|
34
60
|
if (!(await this.fsService.exists(filePath))) {
|
|
35
|
-
continue;
|
|
61
|
+
continue;
|
|
36
62
|
}
|
|
37
63
|
const content = await this.fsService.read(filePath);
|
|
38
64
|
const formatted = this.normalizeHeadings(file, content);
|
|
@@ -42,17 +68,77 @@ export class ContextService {
|
|
|
42
68
|
parts.push('<!-- END OF DEVBRAIN AI ASSISTANT CONTEXT SCHEMA -->');
|
|
43
69
|
return parts.join('\n');
|
|
44
70
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
71
|
+
buildContextString(analysis) {
|
|
72
|
+
const readmeData = analysis.analyzers.readme || {};
|
|
73
|
+
const packageJsonData = analysis.analyzers['package-json'] || {};
|
|
74
|
+
const tsconfigData = analysis.analyzers.tsconfig || {};
|
|
75
|
+
const gitData = analysis.analyzers.git || {};
|
|
76
|
+
const sourceCodeData = analysis.analyzers['source-code'] || {};
|
|
77
|
+
const projectName = analysis.projectName || packageJsonData.name || 'unnamed-project';
|
|
78
|
+
const overview = readmeData.description || 'No description available.';
|
|
79
|
+
const filesCount = analysis.files.length;
|
|
80
|
+
const keyFiles = analysis.files.filter((f) => f.includes('package.json') ||
|
|
81
|
+
f.includes('tsconfig.json') ||
|
|
82
|
+
f.includes('routes') ||
|
|
83
|
+
f.includes('controller') ||
|
|
84
|
+
f.includes('service') ||
|
|
85
|
+
f.includes('model') ||
|
|
86
|
+
f.includes('entity')).slice(0, 30);
|
|
87
|
+
const modules = (sourceCodeData.modules || [])
|
|
88
|
+
.map((m) => `- Module: \`${m.name}\` (Type: ${m.type}, File: \`${m.filePath}\`)\n Classes: [${m.classes.join(', ')}]\n Functions: [${m.functions.join(', ')}]`)
|
|
89
|
+
.join('\n');
|
|
90
|
+
const apis = (sourceCodeData.apis || [])
|
|
91
|
+
.map((a) => `- ${a.method} ${a.path}`)
|
|
92
|
+
.join('\n');
|
|
93
|
+
const dbs = (sourceCodeData.databaseEntities || []).map((d) => `- Model: ${d}`).join('\n');
|
|
94
|
+
const auths = (sourceCodeData.authMethods || []).map((a) => `- Auth: ${a}`).join('\n');
|
|
95
|
+
const externals = (sourceCodeData.externalServices || []).map((e) => `- Service: ${e}`).join('\n');
|
|
96
|
+
const commits = (gitData.recentCommits || [])
|
|
97
|
+
.slice(0, 3)
|
|
98
|
+
.map((c) => `- [${c.hash}] ${c.message}`)
|
|
99
|
+
.join('\n');
|
|
100
|
+
return [
|
|
101
|
+
'# DEVBRAIN AI CONTEXT',
|
|
102
|
+
'',
|
|
103
|
+
`## Project: ${projectName}`,
|
|
104
|
+
overview,
|
|
105
|
+
'',
|
|
106
|
+
`## Tech Stack & Language`,
|
|
107
|
+
`- Core Languages: ${analysis.technologies.join(', ') || 'None detected'}`,
|
|
108
|
+
`- Frameworks: ${analysis.frameworks.join(', ') || 'None detected'}`,
|
|
109
|
+
`- TS Compiler Target: ${tsconfigData.target || 'N/A'}`,
|
|
110
|
+
`- Strict Mode: ${tsconfigData.strict ? 'Enabled' : 'Disabled'}`,
|
|
111
|
+
'',
|
|
112
|
+
'## Project Architecture & Modules',
|
|
113
|
+
modules || '- No specific source code modules identified.',
|
|
114
|
+
'',
|
|
115
|
+
'## API Endpoints',
|
|
116
|
+
apis || '- No routes detected.',
|
|
117
|
+
'',
|
|
118
|
+
'## Database & Schemas',
|
|
119
|
+
dbs || '- No database models identified.',
|
|
120
|
+
'',
|
|
121
|
+
'## Authentication Systems',
|
|
122
|
+
auths || '- Default token/credentials handling.',
|
|
123
|
+
'',
|
|
124
|
+
'## Third-Party Integrations',
|
|
125
|
+
externals || '- No external API integrations.',
|
|
126
|
+
'',
|
|
127
|
+
`## Repository Structure (${filesCount} total files)`,
|
|
128
|
+
'Important files:',
|
|
129
|
+
keyFiles.map((kf) => `- \`${kf}\``).join('\n') || '- No critical files highlighted.',
|
|
130
|
+
'',
|
|
131
|
+
'## Recent Changes',
|
|
132
|
+
commits || '- No git commit log available.',
|
|
133
|
+
'',
|
|
134
|
+
].join('\n');
|
|
135
|
+
}
|
|
49
136
|
normalizeHeadings(fileName, content) {
|
|
50
137
|
const sectionName = fileName.replace('.md', '').toUpperCase();
|
|
51
138
|
const lines = content.split('\n');
|
|
52
139
|
const processedLines = [`## SECTION: ${sectionName}`, ''];
|
|
53
140
|
for (let line of lines) {
|
|
54
141
|
line = line.trimEnd();
|
|
55
|
-
// Translate root header # to second-level ##
|
|
56
142
|
if (line.startsWith('# ')) {
|
|
57
143
|
processedLines.push(`### ${line.substring(2)}`);
|
|
58
144
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class DependencyResolver {
|
|
2
|
+
private readonly coreConfigs;
|
|
3
|
+
/**
|
|
4
|
+
* Resolves the full list of files to analyze based on blast-radius rules.
|
|
5
|
+
*/
|
|
6
|
+
resolveBlastRadius(changedFiles: string[], projectFiles: string[]): {
|
|
7
|
+
resolvedFiles: string[];
|
|
8
|
+
isFullScan: boolean;
|
|
9
|
+
};
|
|
10
|
+
private expandControllerBlastRadius;
|
|
11
|
+
private expandEntityBlastRadius;
|
|
12
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { basename, extname } from 'node:path';
|
|
2
|
+
export class DependencyResolver {
|
|
3
|
+
coreConfigs = new Set([
|
|
4
|
+
'package.json',
|
|
5
|
+
'tsconfig.json',
|
|
6
|
+
'docker-compose.yml',
|
|
7
|
+
'.env.example',
|
|
8
|
+
'schema.prisma',
|
|
9
|
+
'pom.xml',
|
|
10
|
+
'build.gradle',
|
|
11
|
+
'.gitignore',
|
|
12
|
+
'.eslintrc.json',
|
|
13
|
+
'.prettierrc',
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* Resolves the full list of files to analyze based on blast-radius rules.
|
|
17
|
+
*/
|
|
18
|
+
resolveBlastRadius(changedFiles, projectFiles) {
|
|
19
|
+
// 1. Check for core configuration changes
|
|
20
|
+
for (const file of changedFiles) {
|
|
21
|
+
const name = basename(file);
|
|
22
|
+
if (this.coreConfigs.has(name) || name.startsWith('.env')) {
|
|
23
|
+
return { resolvedFiles: projectFiles, isFullScan: true };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const resolved = new Set();
|
|
27
|
+
for (const file of changedFiles) {
|
|
28
|
+
// Add the file itself
|
|
29
|
+
resolved.add(file);
|
|
30
|
+
const base = basename(file);
|
|
31
|
+
const ext = extname(file);
|
|
32
|
+
const nameWithoutExt = base.slice(0, -ext.length);
|
|
33
|
+
// Check if it's a source code file (JS, TS, Python, Go, Java, C#, PHP, Ruby, etc.)
|
|
34
|
+
const isSourceFile = /\.(ts|js|tsx|jsx|py|go|java|cs|php|rb)$/.test(ext);
|
|
35
|
+
if (!isSourceFile) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
// Check category: Controller
|
|
39
|
+
if (/controller/i.test(nameWithoutExt)) {
|
|
40
|
+
const prefix = nameWithoutExt.replace(/controller/i, '').toLowerCase();
|
|
41
|
+
if (prefix) {
|
|
42
|
+
this.expandControllerBlastRadius(prefix, projectFiles, resolved);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
// Check category: Entity / Model
|
|
46
|
+
else if (/entity/i.test(nameWithoutExt) ||
|
|
47
|
+
/model/i.test(nameWithoutExt) ||
|
|
48
|
+
file.includes('/entities/') ||
|
|
49
|
+
file.includes('/models/') ||
|
|
50
|
+
// A simple filename in a src/models or entities dir, or standard short names
|
|
51
|
+
/^[A-Z][a-zA-Z0-9]*$/.test(nameWithoutExt) // e.g. User.ts, Product.ts
|
|
52
|
+
) {
|
|
53
|
+
// Extract prefix (e.g. UserEntity -> User, user.model -> user)
|
|
54
|
+
let prefix = nameWithoutExt
|
|
55
|
+
.replace(/entity/i, '')
|
|
56
|
+
.replace(/model/i, '')
|
|
57
|
+
.toLowerCase();
|
|
58
|
+
// If it's a single word capitalized like User.ts, prefix is user
|
|
59
|
+
if (!prefix) {
|
|
60
|
+
prefix = nameWithoutExt.toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
this.expandEntityBlastRadius(prefix, projectFiles, resolved);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Filter resolved files to ensure they exist in the current project files list
|
|
66
|
+
const projectFilesSet = new Set(projectFiles);
|
|
67
|
+
const finalFiles = Array.from(resolved).filter((f) => projectFilesSet.has(f));
|
|
68
|
+
return { resolvedFiles: finalFiles, isFullScan: false };
|
|
69
|
+
}
|
|
70
|
+
expandControllerBlastRadius(prefix, projectFiles, resolved) {
|
|
71
|
+
// Find related services and routes
|
|
72
|
+
for (const file of projectFiles) {
|
|
73
|
+
const base = basename(file).toLowerCase();
|
|
74
|
+
if (base.includes(prefix)) {
|
|
75
|
+
if (base.includes('service') || base.includes('route') || base.includes('controller')) {
|
|
76
|
+
resolved.add(file);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
expandEntityBlastRadius(prefix, projectFiles, resolved) {
|
|
82
|
+
// Find related repositories, DTOs, services, schemas
|
|
83
|
+
for (const file of projectFiles) {
|
|
84
|
+
const base = basename(file).toLowerCase();
|
|
85
|
+
if (base.includes(prefix)) {
|
|
86
|
+
if (base.includes('repository') ||
|
|
87
|
+
base.includes('dto') ||
|
|
88
|
+
base.includes('service') ||
|
|
89
|
+
base.includes('entity') ||
|
|
90
|
+
base.includes('model') ||
|
|
91
|
+
base.includes('schema')) {
|
|
92
|
+
resolved.add(file);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
else if (base.includes('schema.prisma') || base.includes('schema.sql')) {
|
|
96
|
+
resolved.add(file);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=dependency.resolver.js.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { HistorySchema, CommitHistoryEntry } from '@devbrain/shared';
|
|
2
|
+
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
3
|
+
export declare class HistoryManager {
|
|
4
|
+
private readonly cwd;
|
|
5
|
+
private readonly fsService;
|
|
6
|
+
private readonly historyPath;
|
|
7
|
+
constructor(cwd: string, fsService?: FilesystemService);
|
|
8
|
+
/**
|
|
9
|
+
* Loads the current history or returns an empty default schema.
|
|
10
|
+
*/
|
|
11
|
+
loadHistory(): Promise<HistorySchema>;
|
|
12
|
+
/**
|
|
13
|
+
* Persists the history schema.
|
|
14
|
+
*/
|
|
15
|
+
saveHistory(history: HistorySchema): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Adds an entry to commit history and updates lastProcessedCommit.
|
|
18
|
+
*/
|
|
19
|
+
addCommitEntry(entry: CommitHistoryEntry): Promise<void>;
|
|
20
|
+
/**
|
|
21
|
+
* Gets the last processed commit hash.
|
|
22
|
+
*/
|
|
23
|
+
getLastProcessedCommit(): Promise<string | null>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { DEVBRAIN_DIR_NAME, HISTORY_FILE_NAME } from '@devbrain/shared';
|
|
3
|
+
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
4
|
+
export class HistoryManager {
|
|
5
|
+
cwd;
|
|
6
|
+
fsService;
|
|
7
|
+
historyPath;
|
|
8
|
+
constructor(cwd, fsService = new FilesystemService()) {
|
|
9
|
+
this.cwd = cwd;
|
|
10
|
+
this.fsService = fsService;
|
|
11
|
+
this.historyPath = join(cwd, DEVBRAIN_DIR_NAME, HISTORY_FILE_NAME);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Loads the current history or returns an empty default schema.
|
|
15
|
+
*/
|
|
16
|
+
async loadHistory() {
|
|
17
|
+
try {
|
|
18
|
+
if (await this.fsService.exists(this.historyPath)) {
|
|
19
|
+
return await this.fsService.readJson(this.historyPath);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
// Fall through to default if reading/parsing fails
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
lastProcessedCommit: '',
|
|
27
|
+
commits: [],
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Persists the history schema.
|
|
32
|
+
*/
|
|
33
|
+
async saveHistory(history) {
|
|
34
|
+
await this.fsService.writeJson(this.historyPath, history);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Adds an entry to commit history and updates lastProcessedCommit.
|
|
38
|
+
*/
|
|
39
|
+
async addCommitEntry(entry) {
|
|
40
|
+
const history = await this.loadHistory();
|
|
41
|
+
// Check if commit already processed to avoid duplicates
|
|
42
|
+
const index = history.commits.findIndex((c) => c.commitHash === entry.commitHash);
|
|
43
|
+
if (index !== -1) {
|
|
44
|
+
history.commits[index] = entry;
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
history.commits.unshift(entry); // Prepend so new commits are first
|
|
48
|
+
}
|
|
49
|
+
history.lastProcessedCommit = entry.commitHash;
|
|
50
|
+
// Limit log entries to keep history.json reasonably sized (e.g. max 500 entries)
|
|
51
|
+
if (history.commits.length > 500) {
|
|
52
|
+
history.commits = history.commits.slice(0, 500);
|
|
53
|
+
}
|
|
54
|
+
await this.saveHistory(history);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Gets the last processed commit hash.
|
|
58
|
+
*/
|
|
59
|
+
async getLastProcessedCommit() {
|
|
60
|
+
const history = await this.loadHistory();
|
|
61
|
+
return history.lastProcessedCommit || null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=history.manager.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { FilesystemService } from '../filesystem/filesystem.service.js';
|
|
2
|
+
export declare class LockManager {
|
|
3
|
+
private readonly cwd;
|
|
4
|
+
private readonly fsService;
|
|
5
|
+
private readonly lockPath;
|
|
6
|
+
private readonly staleTimeoutMs;
|
|
7
|
+
constructor(cwd: string, fsService?: FilesystemService);
|
|
8
|
+
/**
|
|
9
|
+
* Attempts to acquire the lock. Returns true if acquired, false otherwise.
|
|
10
|
+
*/
|
|
11
|
+
acquireLock(): Promise<boolean>;
|
|
12
|
+
/**
|
|
13
|
+
* Releases the lock by deleting the lock file.
|
|
14
|
+
*/
|
|
15
|
+
releaseLock(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Checks if a lock currently exists and is active (not stale).
|
|
18
|
+
*/
|
|
19
|
+
isLocked(): Promise<boolean>;
|
|
20
|
+
private checkIfStale;
|
|
21
|
+
}
|