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,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;