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.
Files changed (37) hide show
  1. package/dist/bin.js +50 -1
  2. package/dist/commands/history.command.d.ts +10 -0
  3. package/dist/commands/history.command.js +66 -0
  4. package/dist/commands/hook.command.d.ts +14 -0
  5. package/dist/commands/hook.command.js +40 -0
  6. package/dist/commands/init.command.d.ts +1 -0
  7. package/dist/commands/init.command.js +51 -15
  8. package/dist/commands/learn.command.d.ts +14 -4
  9. package/dist/commands/learn.command.js +148 -43
  10. package/dist/commands/status.command.d.ts +12 -0
  11. package/dist/commands/status.command.js +82 -0
  12. package/node_modules/@devbrain/core/dist/analysis/analyzers/source-code.analyzer.d.ts +24 -0
  13. package/node_modules/@devbrain/core/dist/analysis/analyzers/source-code.analyzer.js +126 -0
  14. package/node_modules/@devbrain/core/dist/context/context.service.d.ts +6 -7
  15. package/node_modules/@devbrain/core/dist/context/context.service.js +102 -16
  16. package/node_modules/@devbrain/core/dist/engine/dependency.resolver.d.ts +12 -0
  17. package/node_modules/@devbrain/core/dist/engine/dependency.resolver.js +101 -0
  18. package/node_modules/@devbrain/core/dist/engine/history.manager.d.ts +24 -0
  19. package/node_modules/@devbrain/core/dist/engine/history.manager.js +64 -0
  20. package/node_modules/@devbrain/core/dist/engine/lock.manager.d.ts +21 -0
  21. package/node_modules/@devbrain/core/dist/engine/lock.manager.js +89 -0
  22. package/node_modules/@devbrain/core/dist/engine/logger.service.d.ts +16 -0
  23. package/node_modules/@devbrain/core/dist/engine/logger.service.js +49 -0
  24. package/node_modules/@devbrain/core/dist/engine/memory.engine.d.ts +30 -0
  25. package/node_modules/@devbrain/core/dist/engine/memory.engine.js +200 -0
  26. package/node_modules/@devbrain/core/dist/git/git.service.d.ts +42 -0
  27. package/node_modules/@devbrain/core/dist/git/git.service.js +192 -0
  28. package/node_modules/@devbrain/core/dist/index.d.ts +7 -0
  29. package/node_modules/@devbrain/core/dist/index.js +7 -0
  30. package/node_modules/@devbrain/core/dist/memory/memory.service.d.ts +6 -3
  31. package/node_modules/@devbrain/core/dist/memory/memory.service.js +186 -6
  32. package/node_modules/@devbrain/core/package.json +2 -2
  33. package/node_modules/@devbrain/shared/dist/constants.d.ts +10 -2
  34. package/node_modules/@devbrain/shared/dist/constants.js +11 -2
  35. package/node_modules/@devbrain/shared/dist/types.d.ts +34 -0
  36. package/node_modules/@devbrain/shared/package.json +1 -1
  37. 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
- * Reads memory markdown files, validates existence, and compiles them into a single token-efficient string.
7
+ * Generates and writes context.md to the target directory.
10
8
  */
11
- buildContext(memoryDir: string): Promise<string>;
9
+ generateContext(analysis: AggregatedAnalysis, memoryDir: string): Promise<string>;
12
10
  /**
13
- * Formats and converts absolute top-level headers (#) to sub-headers (##)
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
- * Reads memory markdown files, validates existence, and compiles them into a single token-efficient string.
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; // Skip missing files gracefully or use a warning comment
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
- * Formats and converts absolute top-level headers (#) to sub-headers (##)
47
- * to maintain a single root h1 structure and optimizes whitespaces.
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
+ }