devassist-agent 1.0.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 (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +467 -0
  3. package/bin/devassist.js +220 -0
  4. package/package.json +44 -0
  5. package/src/ai/adapter.js +464 -0
  6. package/src/ai/providers/claude.js +80 -0
  7. package/src/ai/providers/hunyuan.js +87 -0
  8. package/src/ai/providers/openai.js +74 -0
  9. package/src/ai/providers/qwen.js +81 -0
  10. package/src/cli/commands/ai.js +944 -0
  11. package/src/cli/commands/ask.js +79 -0
  12. package/src/cli/commands/backup.js +30 -0
  13. package/src/cli/commands/check.js +327 -0
  14. package/src/cli/commands/clean.js +130 -0
  15. package/src/cli/commands/comparison-report.js +326 -0
  16. package/src/cli/commands/convention.js +91 -0
  17. package/src/cli/commands/debt.js +49 -0
  18. package/src/cli/commands/deploy.js +88 -0
  19. package/src/cli/commands/diff.js +193 -0
  20. package/src/cli/commands/doctor.js +186 -0
  21. package/src/cli/commands/fix.js +195 -0
  22. package/src/cli/commands/init.js +431 -0
  23. package/src/cli/commands/inject.js +254 -0
  24. package/src/cli/commands/report.js +310 -0
  25. package/src/cli/commands/restore.js +78 -0
  26. package/src/cli/commands/schema.js +93 -0
  27. package/src/cli/commands/watch.js +212 -0
  28. package/src/cli/shared/code-context.js +51 -0
  29. package/src/cli/shared/config-loader.js +89 -0
  30. package/src/cli/shared/file-collector.js +116 -0
  31. package/src/cli/shared/inline-ignore.js +142 -0
  32. package/src/cli/shared/watch-list.js +281 -0
  33. package/src/core/event-bus.js +83 -0
  34. package/src/core/fsm.js +103 -0
  35. package/src/core/logger.js +54 -0
  36. package/src/core/rule-engine.js +117 -0
  37. package/src/index.js +64 -0
  38. package/src/modules/dev-time/arch-risk-assessor.js +250 -0
  39. package/src/modules/dev-time/code-quality-guard.js +1340 -0
  40. package/src/modules/dev-time/convention-store.js +201 -0
  41. package/src/modules/dev-time/impact-analyzer.js +292 -0
  42. package/src/modules/dev-time/pre-deploy-guard.js +284 -0
  43. package/src/modules/dev-time/schema-registry.js +284 -0
  44. package/src/modules/dev-time/tech-debt-tracker.js +225 -0
  45. package/src/modules/dev-time/version-manager.js +280 -0
  46. package/src/modules/runtime/channel-agent.js +404 -0
  47. package/src/modules/runtime/channel-middleware.js +316 -0
  48. package/src/modules/runtime/index.js +64 -0
  49. package/src/modules/runtime/infrastructure-guard.js +582 -0
  50. package/src/modules/runtime/notification-center.js +443 -0
  51. package/src/modules/runtime/schema-gatekeeper.js +664 -0
  52. package/src/modules/runtime/tool-registry.js +329 -0
  53. package/templates/ci/github-actions.yml +60 -0
  54. package/templates/ci/gitlab-ci.yml +30 -0
  55. package/templates/ci/pre-commit-hook.sh +18 -0
  56. package/templates/git-hooks/pre-commit +61 -0
  57. package/tests/run-all.js +434 -0
  58. package/tests/test-layer2.js +461 -0
  59. package/tests/test-new-rules.js +157 -0
@@ -0,0 +1,225 @@
1
+ /**
2
+ * TechDebtTracker - technical debt inventory and prioritization.
3
+ *
4
+ * Defense target: "Refactoring cost explodes" (23/88 failures).
5
+ * The v1->v5 defense system kept growing, RecoveryEngine was 347 lines,
6
+ * and by the time anyone wanted to refactor, the cost was too high.
7
+ *
8
+ * This module:
9
+ * 1. Weekly scan: complexity, coupling, duplication, file size
10
+ * 2. Module boundary guard (app.js set 800 line max -> 2720 = alert)
11
+ * 3. Debt board: auto-sorts "what to pay off next" by risk x failures / effort
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const { Logger } = require('../../core/logger');
17
+ const log = new Logger('TechDebtTracker');
18
+
19
+ class TechDebtTracker {
20
+ constructor(projectRoot) {
21
+ this.projectRoot = projectRoot;
22
+ this.configDir = path.join(projectRoot, '.devassist');
23
+ this.debtFile = path.join(this.configDir, 'tech-debt.json');
24
+ this.history = [];
25
+ this.thresholds = {
26
+ maxFileLines: 800,
27
+ maxFunctionLines: 150,
28
+ maxCyclomaticComplexity: 20,
29
+ maxDuplicateBlocks: 3,
30
+ maxCouplingScore: 10, // max imports per file
31
+ };
32
+ this._load();
33
+ }
34
+
35
+ _load() {
36
+ if (fs.existsSync(this.debtFile)) {
37
+ try {
38
+ const data = JSON.parse(fs.readFileSync(this.debtFile, 'utf-8'));
39
+ this.history = data.history || [];
40
+ if (data.thresholds) Object.assign(this.thresholds, data.thresholds);
41
+ } catch (_) {}
42
+ }
43
+ }
44
+
45
+ _save() {
46
+ if (!fs.existsSync(this.configDir)) fs.mkdirSync(this.configDir, { recursive: true });
47
+ fs.writeFileSync(this.debtFile, JSON.stringify({
48
+ history: this.history,
49
+ thresholds: this.thresholds,
50
+ }, null, 2), 'utf-8');
51
+ }
52
+
53
+ /**
54
+ * Scan all source files and compute debt metrics.
55
+ */
56
+ scan(srcDirs) {
57
+ const dirs = srcDirs || ['src', 'lib', 'static', 'public', 'app'];
58
+ const files = [];
59
+ for (const dir of dirs) {
60
+ const fullDir = path.join(this.projectRoot, dir);
61
+ if (fs.existsSync(fullDir)) this._scanDir(fullDir, files);
62
+ }
63
+ // Also scan root
64
+ this._scanDir(this.projectRoot, files, true);
65
+
66
+ const items = [];
67
+ for (const file of files) {
68
+ let content = '';
69
+ try { content = fs.readFileSync(file, 'utf-8'); } catch (_) { continue; }
70
+ const rel = path.relative(this.projectRoot, file);
71
+ const lines = content.split('\n').length;
72
+ const complexity = this._estimateComplexity(content);
73
+ const imports = (content.match(/(?:require\s*\(|import\s+.*from\s+)/g) || []).length;
74
+ const duplicates = this._findDuplicates(content);
75
+
76
+ // File too large
77
+ if (lines > this.thresholds.maxFileLines) {
78
+ items.push({
79
+ id: `debt-file-size-${rel}`,
80
+ type: 'file-size',
81
+ severity: lines > this.thresholds.maxFileLines * 2 ? 'high' : 'medium',
82
+ file: rel,
83
+ metric: { lines, threshold: this.thresholds.maxFileLines },
84
+ description: `${rel} 共 ${lines} 行(上限 ${this.thresholds.maxFileLines} 行)。风险:AI 可能误覆盖或截断大文件。`,
85
+ effort: Math.ceil((lines - this.thresholds.maxFileLines) / 100), // rough effort estimate
86
+ failureLink: '故障 #9:app.js 膨胀至 2720 行,AI 误覆盖为 106 行',
87
+ });
88
+ }
89
+
90
+ // High complexity
91
+ if (complexity > this.thresholds.maxCyclomaticComplexity) {
92
+ items.push({
93
+ id: `debt-complexity-${rel}`,
94
+ type: 'complexity',
95
+ severity: complexity > this.thresholds.maxCyclomaticComplexity * 2 ? 'high' : 'medium',
96
+ file: rel,
97
+ metric: { complexity, threshold: this.thresholds.maxCyclomaticComplexity },
98
+ description: `${rel} 圈复杂度约 ${complexity}(上限 ${this.thresholds.maxCyclomaticComplexity})。`,
99
+ effort: Math.ceil((complexity - this.thresholds.maxCyclomaticComplexity) / 5),
100
+ failureLink: '故障 #7:RecoveryEngine 347 行,高复杂度,引入 4 个新 bug',
101
+ });
102
+ }
103
+
104
+ // High coupling
105
+ if (imports > this.thresholds.maxCouplingScore) {
106
+ items.push({
107
+ id: `debt-coupling-${rel}`,
108
+ type: 'coupling',
109
+ severity: 'medium',
110
+ file: rel,
111
+ metric: { imports, threshold: this.thresholds.maxCouplingScore },
112
+ description: `${rel} 引入了 ${imports} 个模块(上限 ${this.thresholds.maxCouplingScore})。高耦合使修改变得危险。`,
113
+ effort: 3,
114
+ failureLink: '故障 #5:某模块修改导致依赖方意外崩溃',
115
+ });
116
+ }
117
+
118
+ // Code duplication
119
+ if (duplicates.length > 0) {
120
+ items.push({
121
+ id: `debt-duplication-${rel}`,
122
+ type: 'duplication',
123
+ severity: duplicates.length > this.thresholds.maxDuplicateBlocks ? 'medium' : 'low',
124
+ file: rel,
125
+ metric: { duplicateBlocks: duplicates.length, threshold: this.thresholds.maxDuplicateBlocks },
126
+ description: `${rel} 存在 ${duplicates.length} 处重复代码块。`,
127
+ effort: duplicates.length,
128
+ failureLink: '故障 #8:重复声明导致混淆',
129
+ });
130
+ }
131
+ }
132
+
133
+ // Calculate priority score: severity weight x failure linkage / effort
134
+ const severityWeight = { high: 3, medium: 2, low: 1 };
135
+ items.forEach(item => {
136
+ item.priority = Math.round((severityWeight[item.severity] || 1) * 10 / Math.max(item.effort, 1));
137
+ });
138
+ items.sort((a, b) => b.priority - a.priority);
139
+
140
+ // Save snapshot
141
+ const snapshot = {
142
+ timestamp: new Date().toISOString(),
143
+ totalFiles: files.length,
144
+ totalDebt: items.length,
145
+ highSeverity: items.filter(i => i.severity === 'high').length,
146
+ items: items.slice(0, 50), // keep top 50
147
+ };
148
+ this.history.push(snapshot);
149
+ if (this.history.length > 12) this.history.shift(); // keep 12 weeks
150
+ this._save();
151
+
152
+ log.info(`技术债扫描:发现 ${items.length} 项(${snapshot.highSeverity} 项高严重度)`);
153
+ return snapshot;
154
+ }
155
+
156
+ _scanDir(dir, results, rootOnly) {
157
+ let entries;
158
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
159
+ for (const entry of entries) {
160
+ if (['node_modules', '.git', '.devassist', 'dist', 'build'].includes(entry.name)) continue;
161
+ const full = path.join(dir, entry.name);
162
+ if (entry.isDirectory() && !rootOnly) {
163
+ this._scanDir(full, results);
164
+ } else if (entry.isFile() && /\.(js|ts|jsx|tsx|py|java|html|css)$/.test(entry.name)) {
165
+ results.push(full);
166
+ }
167
+ }
168
+ }
169
+
170
+ _estimateComplexity(content) {
171
+ return (content.match(/\bif\s*\(/g) || []).length
172
+ + (content.match(/\bfor\s*\(/g) || []).length
173
+ + (content.match(/\bwhile\s*\(/g) || []).length
174
+ + (content.match(/\bcase\s+/g) || []).length
175
+ + (content.match(/&&|\|\|/g) || []).length
176
+ + 1;
177
+ }
178
+
179
+ _findDuplicates(content) {
180
+ const lines = content.split('\n').map(l => l.trim()).filter(l => l.length > 20 && !l.startsWith('//') && !l.startsWith('/*'));
181
+ const seen = new Map();
182
+ const duplicates = [];
183
+ for (const line of lines) {
184
+ seen.set(line, (seen.get(line) || 0) + 1);
185
+ }
186
+ for (const [line, count] of seen) {
187
+ if (count > 1) duplicates.push({ line: line.substring(0, 80), count });
188
+ }
189
+ return duplicates;
190
+ }
191
+
192
+ /**
193
+ * Get the debt board (prioritized list).
194
+ */
195
+ getBoard() {
196
+ if (this.history.length === 0) return { items: [], message: '暂无扫描数据,请运行 devassist debt 进行扫描。' };
197
+ const latest = this.history[this.history.length - 1];
198
+ return {
199
+ timestamp: latest.timestamp,
200
+ totalDebt: latest.totalDebt,
201
+ highSeverity: latest.highSeverity,
202
+ topItems: latest.items.slice(0, 10),
203
+ trend: this.history.length > 1 ? this._calculateTrend() : 'baseline',
204
+ };
205
+ }
206
+
207
+ _calculateTrend() {
208
+ if (this.history.length < 2) return 'baseline';
209
+ const current = this.history[this.history.length - 1].totalDebt;
210
+ const previous = this.history[this.history.length - 2].totalDebt;
211
+ if (current > previous) return `上升(+${current - previous})`;
212
+ if (current < previous) return `下降(-${previous - current})`;
213
+ return '持平';
214
+ }
215
+
216
+ setThreshold(key, value) {
217
+ if (key in this.thresholds) {
218
+ this.thresholds[key] = value;
219
+ this._save();
220
+ log.info(`阈值已更新:${key} = ${value}`);
221
+ }
222
+ }
223
+ }
224
+
225
+ module.exports = { TechDebtTracker };
@@ -0,0 +1,280 @@
1
+ /**
2
+ * VersionManager - incremental backup and file manifest management.
3
+ *
4
+ * Defense target: "File management errors" (13% of 88 failures).
5
+ * #12 - backup restore deleted new files (cp -r overwrote entire dir)
6
+ * #13 - deployment omitted files (forgot to copy config)
7
+ * #14 - version confusion (didn't know which version was running)
8
+ * #15 - old cache served after deploy (no cache-busting)
9
+ *
10
+ * Key principle: NEVER overwrite an entire directory during restore.
11
+ * Use incremental backups with file manifests.
12
+ */
13
+
14
+ const fs = require('fs');
15
+ const path = require('path');
16
+ const crypto = require('crypto');
17
+ const { Logger } = require('../../core/logger');
18
+ const log = new Logger('VersionManager');
19
+
20
+ class VersionManager {
21
+ constructor(projectRoot) {
22
+ this.projectRoot = projectRoot;
23
+ this.configDir = path.join(projectRoot, '.devassist');
24
+ this.backupDir = path.join(this.configDir, 'backups');
25
+ this.manifestFile = path.join(this.configDir, 'version-manifest.json');
26
+ this.versions = [];
27
+ this._load();
28
+ }
29
+
30
+ _load() {
31
+ if (fs.existsSync(this.manifestFile)) {
32
+ try {
33
+ this.versions = JSON.parse(fs.readFileSync(this.manifestFile, 'utf-8'));
34
+ } catch (_) {}
35
+ }
36
+ }
37
+
38
+ _save() {
39
+ if (!fs.existsSync(this.configDir)) fs.mkdirSync(this.configDir, { recursive: true });
40
+ fs.writeFileSync(this.manifestFile, JSON.stringify(this.versions, null, 2), 'utf-8');
41
+ }
42
+
43
+ /**
44
+ * Create an incremental backup.
45
+ * Only backs up files that have changed since the last backup.
46
+ * Returns a manifest with file hashes for verification.
47
+ */
48
+ backup(label, srcDirs) {
49
+ const dirs = srcDirs || ['src', 'lib', 'static', 'public', 'app'];
50
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
51
+ const versionId = `v-${timestamp}`;
52
+ const backupPath = path.join(this.backupDir, versionId);
53
+
54
+ if (!fs.existsSync(backupPath)) fs.mkdirSync(backupPath, { recursive: true });
55
+
56
+ const files = [];
57
+ for (const dir of dirs) {
58
+ const fullDir = path.join(this.projectRoot, dir);
59
+ if (fs.existsSync(fullDir)) {
60
+ this._collectFiles(fullDir, files);
61
+ }
62
+ }
63
+ // Also collect root-level config files
64
+ this._collectRootConfigs(files);
65
+
66
+ const manifest = {
67
+ versionId,
68
+ label: label || 'manual backup',
69
+ timestamp: new Date().toISOString(),
70
+ files: [],
71
+ };
72
+
73
+ let prevManifest = this.versions.length > 0 ? this.versions[this.versions.length - 1] : null;
74
+ let prevFileMap = new Map();
75
+ if (prevManifest) {
76
+ // Load previous manifest files
77
+ const prevManifestPath = path.join(this.backupDir, prevManifest.versionId, 'manifest.json');
78
+ if (fs.existsSync(prevManifestPath)) {
79
+ try {
80
+ const prev = JSON.parse(fs.readFileSync(prevManifestPath, 'utf-8'));
81
+ for (const f of prev.files) prevFileMap.set(f.path, f.hash);
82
+ } catch (_) {}
83
+ }
84
+ }
85
+
86
+ let newCount = 0;
87
+ let changedCount = 0;
88
+ let unchangedCount = 0;
89
+
90
+ for (const file of files) {
91
+ const rel = path.relative(this.projectRoot, file);
92
+ let content;
93
+ try { content = fs.readFileSync(file); } catch (_) { continue; }
94
+ const hash = crypto.createHash('md5').update(content).digest('hex');
95
+ const prevHash = prevFileMap.get(rel);
96
+
97
+ if (prevHash === hash) {
98
+ unchangedCount++;
99
+ manifest.files.push({ path: rel, hash, status: 'unchanged' });
100
+ } else if (prevHash) {
101
+ changedCount++;
102
+ // Copy changed file to backup
103
+ const destDir = path.join(backupPath, path.dirname(rel));
104
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
105
+ fs.copyFileSync(file, path.join(backupPath, rel));
106
+ manifest.files.push({ path: rel, hash, status: 'changed', prevHash });
107
+ } else {
108
+ newCount++;
109
+ const destDir = path.join(backupPath, path.dirname(rel));
110
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
111
+ fs.copyFileSync(file, path.join(backupPath, rel));
112
+ manifest.files.push({ path: rel, hash, status: 'new' });
113
+ }
114
+ }
115
+
116
+ // Save manifest
117
+ fs.writeFileSync(path.join(backupPath, 'manifest.json'), JSON.stringify(manifest, null, 2), 'utf-8');
118
+
119
+ // Update versions list
120
+ this.versions.push({
121
+ versionId,
122
+ label: manifest.label,
123
+ timestamp: manifest.timestamp,
124
+ stats: { total: manifest.files.length, new: newCount, changed: changedCount, unchanged: unchangedCount },
125
+ });
126
+ this._save();
127
+
128
+ log.info(`备份 ${versionId}:${manifest.files.length} 个文件(${newCount} 新增, ${changedCount} 变更, ${unchangedCount} 未变)`);
129
+ return manifest;
130
+ }
131
+
132
+ /**
133
+ * Restore from a backup — INCREMENTAL, not destructive.
134
+ * Only restores files that were in the backup. Does NOT delete files that
135
+ * weren't in the backup (failure #12 defense).
136
+ *
137
+ * Returns a pre-restore report so the user can see what will change.
138
+ */
139
+ prepareRestore(versionId) {
140
+ const backupPath = path.join(this.backupDir, versionId);
141
+ const manifestPath = path.join(backupPath, 'manifest.json');
142
+
143
+ if (!fs.existsSync(manifestPath)) {
144
+ throw new Error(`备份 ${versionId} 不存在或清单缺失`);
145
+ }
146
+
147
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf-8'));
148
+ const report = {
149
+ versionId,
150
+ willRestore: [],
151
+ willOverwrite: [],
152
+ newFilesNotInBackup: [], // Files that exist now but weren't in backup
153
+ warning: null,
154
+ };
155
+
156
+ // Check which files would be restored/overwritten
157
+ for (const fileEntry of manifest.files) {
158
+ if (fileEntry.status === 'unchanged') continue;
159
+ const fullPath = path.join(this.projectRoot, fileEntry.path);
160
+ if (fs.existsSync(fullPath)) {
161
+ report.willOverwrite.push(fileEntry.path);
162
+ } else {
163
+ report.willRestore.push(fileEntry.path);
164
+ }
165
+ }
166
+
167
+ // CRITICAL: Find files that exist NOW but were NOT in the backup
168
+ // These would be LOST if we did a naive cp -r restore (failure #12)
169
+ const backupFiles = new Set(manifest.files.map(f => f.path));
170
+ const currentFiles = [];
171
+ for (const dir of ['src', 'lib', 'static', 'public', 'app']) {
172
+ const fullDir = path.join(this.projectRoot, dir);
173
+ if (fs.existsSync(fullDir)) this._collectFiles(fullDir, currentFiles);
174
+ }
175
+ this._collectRootConfigs(currentFiles);
176
+
177
+ for (const file of currentFiles) {
178
+ const rel = path.relative(this.projectRoot, file);
179
+ if (!backupFiles.has(rel)) {
180
+ report.newFilesNotInBackup.push(rel);
181
+ }
182
+ }
183
+
184
+ if (report.newFilesNotInBackup.length > 0) {
185
+ report.warning = `WARNING: ${report.newFilesNotInBackup.length} files exist now but are NOT in backup ${versionId}. A naive restore would DELETE these files. This was failure #12.`;
186
+ }
187
+
188
+ return report;
189
+ }
190
+
191
+ /**
192
+ * Actually perform the restore. Only copies back files that were in the backup.
193
+ * Does NOT delete any current files.
194
+ */
195
+ doRestore(versionId) {
196
+ const report = this.prepareRestore(versionId);
197
+ const backupPath = path.join(this.backupDir, versionId);
198
+ const manifest = JSON.parse(fs.readFileSync(path.join(backupPath, 'manifest.json'), 'utf-8'));
199
+
200
+ let restored = 0;
201
+ for (const fileEntry of manifest.files) {
202
+ if (fileEntry.status === 'unchanged') continue;
203
+ const srcFile = path.join(backupPath, fileEntry.path);
204
+ const destFile = path.join(this.projectRoot, fileEntry.path);
205
+
206
+ if (!fs.existsSync(srcFile)) {
207
+ log.warn(`备份文件缺失:${fileEntry.path}`);
208
+ continue;
209
+ }
210
+
211
+ const destDir = path.dirname(destFile);
212
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
213
+ fs.copyFileSync(srcFile, destFile);
214
+ restored++;
215
+ }
216
+
217
+ log.info(`从 ${versionId} 恢复了 ${restored} 个文件(非破坏性:保留了 ${report.newFilesNotInBackup.length} 个新文件)`);
218
+ return { restored, newFilesPreserved: report.newFilesNotInBackup.length };
219
+ }
220
+
221
+ /**
222
+ * Generate a deployment manifest — list of all files that must be deployed.
223
+ * Failure #13: files omitted during deployment.
224
+ */
225
+ generateDeployManifest(srcDirs) {
226
+ const dirs = srcDirs || ['src', 'lib', 'static', 'public', 'app'];
227
+ const files = [];
228
+ for (const dir of dirs) {
229
+ const fullDir = path.join(this.projectRoot, dir);
230
+ if (fs.existsSync(fullDir)) this._collectFiles(fullDir, files);
231
+ }
232
+ this._collectRootConfigs(files);
233
+
234
+ const manifest = {
235
+ generatedAt: new Date().toISOString(),
236
+ totalFiles: files.length,
237
+ files: files.map(f => {
238
+ const rel = path.relative(this.projectRoot, f);
239
+ let stat;
240
+ try { stat = fs.statSync(f); } catch (_) { stat = {}; }
241
+ return { path: rel, size: stat.size || 0 };
242
+ }),
243
+ };
244
+
245
+ const manifestPath = path.join(this.configDir, 'deploy-manifest.json');
246
+ if (!fs.existsSync(this.configDir)) fs.mkdirSync(this.configDir, { recursive: true });
247
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf-8');
248
+
249
+ log.info(`部署清单已生成:${manifest.totalFiles} 个文件`);
250
+ return manifest;
251
+ }
252
+
253
+ listVersions() {
254
+ return this.versions;
255
+ }
256
+
257
+ _collectFiles(dir, results) {
258
+ let entries;
259
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
260
+ for (const entry of entries) {
261
+ if (['node_modules', '.git', '.devassist', 'dist', 'build'].includes(entry.name)) continue;
262
+ const full = path.join(dir, entry.name);
263
+ if (entry.isDirectory()) {
264
+ this._collectFiles(full, results);
265
+ } else if (/\.(js|ts|jsx|tsx|py|java|html|css|json|sql|sh|conf|yaml|yml|env)$/.test(entry.name)) {
266
+ results.push(full);
267
+ }
268
+ }
269
+ }
270
+
271
+ _collectRootConfigs(results) {
272
+ const configFiles = ['package.json', 'nginx.conf', 'supervisord.conf', '.env', 'docker-compose.yml', 'Dockerfile'];
273
+ for (const name of configFiles) {
274
+ const full = path.join(this.projectRoot, name);
275
+ if (fs.existsSync(full)) results.push(full);
276
+ }
277
+ }
278
+ }
279
+
280
+ module.exports = { VersionManager };