imhcode 1.1.1 → 2.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.
- package/README.md +18 -0
- package/USER_MANUAL.md +29 -0
- package/bin/imhcode.js +798 -0
- package/dist/orchestrator/builder.d.ts.map +1 -1
- package/dist/orchestrator/builder.js +27 -4
- package/dist/orchestrator/builder.js.map +1 -1
- package/dist/orchestrator/context-scanner.d.ts +23 -0
- package/dist/orchestrator/context-scanner.d.ts.map +1 -0
- package/dist/orchestrator/context-scanner.js +248 -0
- package/dist/orchestrator/context-scanner.js.map +1 -0
- package/dist/orchestrator/import-engine.d.ts +18 -0
- package/dist/orchestrator/import-engine.d.ts.map +1 -0
- package/dist/orchestrator/import-engine.js +116 -0
- package/dist/orchestrator/import-engine.js.map +1 -0
- package/dist/orchestrator/index.d.ts +4 -0
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +13 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/modification-engine.d.ts +12 -0
- package/dist/orchestrator/modification-engine.d.ts.map +1 -0
- package/dist/orchestrator/modification-engine.js +109 -0
- package/dist/orchestrator/modification-engine.js.map +1 -0
- package/dist/orchestrator/project-scanner.d.ts +16 -0
- package/dist/orchestrator/project-scanner.d.ts.map +1 -0
- package/dist/orchestrator/project-scanner.js +164 -0
- package/dist/orchestrator/project-scanner.js.map +1 -0
- package/package.json +1 -1
- package/skills/typeui-main/test/orchestrator.test.ts +88 -41
- package/src/orchestrator/builder.ts +29 -4
- package/src/orchestrator/context-scanner.ts +221 -0
- package/src/orchestrator/import-engine.ts +106 -0
- package/src/orchestrator/index.ts +8 -0
- package/src/orchestrator/modification-engine.ts +108 -0
- package/src/orchestrator/project-scanner.ts +134 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
export interface ProjectScanResult {
|
|
5
|
+
hasFrontend: boolean;
|
|
6
|
+
hasBackend: boolean;
|
|
7
|
+
frontendFramework?: string;
|
|
8
|
+
backendFramework?: string;
|
|
9
|
+
dependencies: Record<string, string>;
|
|
10
|
+
directories: string[];
|
|
11
|
+
fileCount: number;
|
|
12
|
+
recentModifications: string[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Scan project structure to build context for modifications.
|
|
17
|
+
*/
|
|
18
|
+
export function scanProjectContext(cwd: string): ProjectScanResult {
|
|
19
|
+
const result: ProjectScanResult = {
|
|
20
|
+
hasFrontend: false,
|
|
21
|
+
hasBackend: false,
|
|
22
|
+
dependencies: {},
|
|
23
|
+
directories: [],
|
|
24
|
+
fileCount: 0,
|
|
25
|
+
recentModifications: []
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// 1. Detect directories
|
|
29
|
+
try {
|
|
30
|
+
const items = fs.readdirSync(cwd);
|
|
31
|
+
for (const item of items) {
|
|
32
|
+
if (item.startsWith(".") || item === "node_modules" || item === "dist") continue;
|
|
33
|
+
const itemPath = path.join(cwd, item);
|
|
34
|
+
try {
|
|
35
|
+
const stat = fs.statSync(itemPath);
|
|
36
|
+
if (stat.isDirectory()) {
|
|
37
|
+
result.directories.push(item);
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
// Ignore
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. Scan frontend
|
|
48
|
+
const frontendPath = path.join(cwd, "frontend");
|
|
49
|
+
if (fs.existsSync(frontendPath) && fs.statSync(frontendPath).isDirectory()) {
|
|
50
|
+
result.hasFrontend = true;
|
|
51
|
+
const pkgJsonPath = path.join(frontendPath, "package.json");
|
|
52
|
+
if (fs.existsSync(pkgJsonPath)) {
|
|
53
|
+
try {
|
|
54
|
+
const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
55
|
+
result.frontendFramework = pkg.dependencies?.next ? "Next.js" : pkg.dependencies?.vue ? "Vue" : "React";
|
|
56
|
+
Object.assign(result.dependencies, pkg.dependencies || {});
|
|
57
|
+
Object.assign(result.dependencies, pkg.devDependencies || {});
|
|
58
|
+
} catch {
|
|
59
|
+
// Ignore
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 3. Scan backend
|
|
65
|
+
const backendPath = path.join(cwd, "backend");
|
|
66
|
+
if (fs.existsSync(backendPath) && fs.statSync(backendPath).isDirectory()) {
|
|
67
|
+
result.hasBackend = true;
|
|
68
|
+
const composerJsonPath = path.join(backendPath, "composer.json");
|
|
69
|
+
if (fs.existsSync(composerJsonPath)) {
|
|
70
|
+
try {
|
|
71
|
+
const comp = JSON.parse(fs.readFileSync(composerJsonPath, "utf-8"));
|
|
72
|
+
result.backendFramework = "Laravel";
|
|
73
|
+
if (comp.require) {
|
|
74
|
+
Object.assign(result.dependencies, comp.require);
|
|
75
|
+
}
|
|
76
|
+
} catch {
|
|
77
|
+
// Ignore
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Also check for Python
|
|
81
|
+
const reqTxtPath = path.join(backendPath, "requirements.txt");
|
|
82
|
+
if (fs.existsSync(reqTxtPath)) {
|
|
83
|
+
result.backendFramework = "Python/FastAPI/Django";
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 4. Count files (shallow-ish recursive count for safety/perf)
|
|
88
|
+
let count = 0;
|
|
89
|
+
function countFiles(dir: string, depth = 0) {
|
|
90
|
+
if (depth > 4 || count > 500) return; // Cap at 500 files for scanning performance
|
|
91
|
+
try {
|
|
92
|
+
const files = fs.readdirSync(dir);
|
|
93
|
+
for (const file of files) {
|
|
94
|
+
if (file.startsWith(".") || file === "node_modules" || file === "dist" || file === "vendor") continue;
|
|
95
|
+
const fp = path.join(dir, file);
|
|
96
|
+
const stat = fs.statSync(fp);
|
|
97
|
+
if (stat.isDirectory()) {
|
|
98
|
+
countFiles(fp, depth + 1);
|
|
99
|
+
} else {
|
|
100
|
+
count++;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// Ignore
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
countFiles(cwd);
|
|
108
|
+
result.fileCount = count;
|
|
109
|
+
|
|
110
|
+
// 5. Read recent modifications from log if exists
|
|
111
|
+
const modLogPath = path.join(cwd, "docs", "modifications.md");
|
|
112
|
+
if (fs.existsSync(modLogPath)) {
|
|
113
|
+
try {
|
|
114
|
+
const content = fs.readFileSync(modLogPath, "utf-8");
|
|
115
|
+
const lines = content.split("\n").filter(l => l.trim().startsWith("|")).slice(2);
|
|
116
|
+
result.recentModifications = lines.map(l => l.trim());
|
|
117
|
+
} catch {
|
|
118
|
+
// Ignore
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Builds context prompt specifically for a modification task.
|
|
127
|
+
*/
|
|
128
|
+
export function buildModificationPrompt(context: ProjectScanResult, description: string): string {
|
|
129
|
+
const lines = [
|
|
130
|
+
`# IMH-Code Project Modification Request`,
|
|
131
|
+
`You are tasked with modifying or extending an existing project.`,
|
|
132
|
+
``,
|
|
133
|
+
`## Project Inventory`,
|
|
134
|
+
`- Frontend Active: ${context.hasFrontend ? `Yes (${context.frontendFramework})` : "No"}`,
|
|
135
|
+
`- Backend Active: ${context.hasBackend ? `Yes (${context.backendFramework})` : "No"}`,
|
|
136
|
+
`- Total Directories: ${context.directories.join(", ")}`,
|
|
137
|
+
`- File Count: ${context.fileCount}`,
|
|
138
|
+
``,
|
|
139
|
+
`## Detected Stacks & Dependencies`,
|
|
140
|
+
Object.entries(context.dependencies)
|
|
141
|
+
.slice(0, 15) // Top 15 dependencies
|
|
142
|
+
.map(([k, v]) => `- \`${k}\`: \`${v}\``)
|
|
143
|
+
.join("\n"),
|
|
144
|
+
context.recentModifications.length > 0
|
|
145
|
+
? `\n## Recent Project Modifications\n` + context.recentModifications.join("\n")
|
|
146
|
+
: "",
|
|
147
|
+
``,
|
|
148
|
+
`## Modification Instructions`,
|
|
149
|
+
`Task: ${description}`,
|
|
150
|
+
``,
|
|
151
|
+
`CRITICAL DIRECTIONS:`,
|
|
152
|
+
`1. Review the existing directories (e.g. frontend/ and backend/) to locate the relevant files.`,
|
|
153
|
+
`2. Perform the edit IN-PLACE. Do NOT delete or rewrite entire directories unless necessary.`,
|
|
154
|
+
`3. Adhere strictly to the existing coding style, frameworks, and packages.`,
|
|
155
|
+
`4. If new dependencies are needed, install them using the appropriate package manager in the correct directory.`,
|
|
156
|
+
`5. After completing, explain clearly what files were changed.`
|
|
157
|
+
];
|
|
158
|
+
|
|
159
|
+
return lines.join("\n");
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Helper to auto-select best agent ID based on task description keywords.
|
|
164
|
+
*/
|
|
165
|
+
export function detectBestAgent(description: string, context: ProjectScanResult): string {
|
|
166
|
+
const d = description.toLowerCase();
|
|
167
|
+
|
|
168
|
+
// 1. Planning/Architecture
|
|
169
|
+
if (d.includes("plan") || d.includes("architect") || d.includes("road map") || d.includes("sprint")) {
|
|
170
|
+
return "planner";
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// 2. DevOps/Infrastructure
|
|
174
|
+
if (d.includes("docker") || d.includes("kubernetes") || d.includes("deploy") || d.includes("ci/cd") || d.includes("github action") || d.includes("nginx")) {
|
|
175
|
+
return "devops-executor";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 3. Testing
|
|
179
|
+
if (d.includes("test") || d.includes("spec") || d.includes("audit") || d.includes("security")) {
|
|
180
|
+
if (d.includes("security") || d.includes("vulnerability") || d.includes("owasp")) {
|
|
181
|
+
return "security-auditor";
|
|
182
|
+
}
|
|
183
|
+
return "tester";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 4. SEO
|
|
187
|
+
if (d.includes("seo") || d.includes("meta tag") || d.includes("sitemap") || d.includes("robots.txt")) {
|
|
188
|
+
return "seo-optimizer";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 5. Frontend Keywords Check
|
|
192
|
+
const hasFrontendKeywords = d.includes("ui") || d.includes("page") || d.includes("component") || d.includes("navbar") || d.includes("style") || d.includes("css") || d.includes("tailwind") || d.includes("button") || d.includes("layout");
|
|
193
|
+
if (hasFrontendKeywords) {
|
|
194
|
+
if (context.frontendFramework === "Next.js") return "nextjs-executor";
|
|
195
|
+
if (context.frontendFramework === "Vue") return "vue-executor";
|
|
196
|
+
return "react-executor";
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 6. Backend Keywords Check
|
|
200
|
+
const hasBackendKeywords = d.includes("api") || d.includes("route") || d.includes("database") || d.includes("controller") || d.includes("migration") || d.includes("model") || d.includes("query") || d.includes("auth");
|
|
201
|
+
if (hasBackendKeywords) {
|
|
202
|
+
if (context.backendFramework === "Laravel") return "laravel-executor";
|
|
203
|
+
if (context.backendFramework === "Python/FastAPI/Django") return "python-executor";
|
|
204
|
+
return "laravel-executor";
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 7. Directory fallbacks if no keywords matched
|
|
208
|
+
if (context.hasFrontend) {
|
|
209
|
+
if (context.frontendFramework === "Next.js") return "nextjs-executor";
|
|
210
|
+
if (context.frontendFramework === "Vue") return "vue-executor";
|
|
211
|
+
return "react-executor";
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (context.hasBackend) {
|
|
215
|
+
if (context.backendFramework === "Laravel") return "laravel-executor";
|
|
216
|
+
if (context.backendFramework === "Python/FastAPI/Django") return "python-executor";
|
|
217
|
+
return "laravel-executor";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return "laravel-executor"; // Ultimate default
|
|
221
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { scanProject } from "./project-scanner";
|
|
4
|
+
|
|
5
|
+
export interface ImportResult {
|
|
6
|
+
success: boolean;
|
|
7
|
+
scanResult: any;
|
|
8
|
+
importMap: {
|
|
9
|
+
frontend: string;
|
|
10
|
+
backend: string;
|
|
11
|
+
database: string;
|
|
12
|
+
detectedStack: {
|
|
13
|
+
frontend: string | null;
|
|
14
|
+
backend: string | null;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Import an existing codebase into the IMH-Code orchestration framework.
|
|
21
|
+
*/
|
|
22
|
+
export function importProject(cwd: string): ImportResult {
|
|
23
|
+
const scanResult = scanProject(cwd);
|
|
24
|
+
|
|
25
|
+
const imhDir = path.join(cwd, ".imhcode");
|
|
26
|
+
if (!fs.existsSync(imhDir)) {
|
|
27
|
+
fs.mkdirSync(imhDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const cmdDir = path.join(imhDir, "commands");
|
|
31
|
+
if (!fs.existsSync(cmdDir)) {
|
|
32
|
+
fs.mkdirSync(cmdDir, { recursive: true });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const sessionsDir = path.join(imhDir, "sessions");
|
|
36
|
+
if (!fs.existsSync(sessionsDir)) {
|
|
37
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Map paths. If it scanned to ".", default to empty or standard string
|
|
41
|
+
const importMap = {
|
|
42
|
+
frontend: scanResult.frontendPath || "frontend",
|
|
43
|
+
backend: scanResult.backendPath || "backend",
|
|
44
|
+
database: scanResult.database || "PostgreSQL",
|
|
45
|
+
detectedStack: {
|
|
46
|
+
frontend: scanResult.detectedFrontend,
|
|
47
|
+
backend: scanResult.detectedBackend
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
fs.writeFileSync(
|
|
52
|
+
path.join(imhDir, "import-map.json"),
|
|
53
|
+
JSON.stringify(importMap, null, 2),
|
|
54
|
+
"utf-8"
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// PROJECT_BRIEF.md
|
|
58
|
+
const briefPath = path.join(cwd, "PROJECT_BRIEF.md");
|
|
59
|
+
const briefContent = `# PROJECT_BRIEF.md (Imported Project)
|
|
60
|
+
|
|
61
|
+
> **IMH-Code — Imam Hussain Coding Harness Platform**
|
|
62
|
+
> Centralized memory for this imported project.
|
|
63
|
+
|
|
64
|
+
## Project Summary
|
|
65
|
+
|
|
66
|
+
*(Imported project. Add a description of your project here to guide the agents.)*
|
|
67
|
+
|
|
68
|
+
## Status
|
|
69
|
+
|
|
70
|
+
- **Imported**: ${new Date().toLocaleDateString()}
|
|
71
|
+
- **Current Sprint**: Sprint 1 (Modifications Mode)
|
|
72
|
+
|
|
73
|
+
## Design Specification
|
|
74
|
+
|
|
75
|
+
- **Design Style**: Modern SaaS
|
|
76
|
+
- **Color Palette**: Dark Premium
|
|
77
|
+
|
|
78
|
+
## Scope
|
|
79
|
+
|
|
80
|
+
- **Frontend Path**: \`${importMap.frontend}\` (${scanResult.detectedFrontend || "None"})
|
|
81
|
+
- **Backend Path**: \`${importMap.backend}\` (${scanResult.detectedBackend || "None"})
|
|
82
|
+
- **Database**: ${importMap.database}
|
|
83
|
+
`;
|
|
84
|
+
fs.writeFileSync(briefPath, briefContent, "utf-8");
|
|
85
|
+
|
|
86
|
+
// context.md
|
|
87
|
+
const contextPath = path.join(imhDir, "context.md");
|
|
88
|
+
const contextContent = `# IMH-Code Project Context (Imported)
|
|
89
|
+
|
|
90
|
+
Generated: ${new Date().toLocaleDateString()}
|
|
91
|
+
Project Path: ${cwd}
|
|
92
|
+
Frontend Framework: ${scanResult.detectedFrontend || "None"} (Path: ${importMap.frontend})
|
|
93
|
+
Backend Framework: ${scanResult.detectedBackend || "None"} (Path: ${importMap.backend})
|
|
94
|
+
|
|
95
|
+
## Directory Structure Mappings
|
|
96
|
+
- Frontend: \`${importMap.frontend}\`
|
|
97
|
+
- Backend: \`${importMap.backend}\`
|
|
98
|
+
`;
|
|
99
|
+
fs.writeFileSync(contextPath, contextContent, "utf-8");
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
scanResult,
|
|
104
|
+
importMap
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -48,6 +48,14 @@ export {
|
|
|
48
48
|
// Session
|
|
49
49
|
export { saveSession, resolveSessionDir } from "./session";
|
|
50
50
|
|
|
51
|
+
// Modification & Scanning
|
|
52
|
+
export { scanProjectContext, buildModificationPrompt, detectBestAgent } from "./context-scanner";
|
|
53
|
+
export { runModification } from "./modification-engine";
|
|
54
|
+
|
|
55
|
+
// Project Import & Extension
|
|
56
|
+
export { scanProject } from "./project-scanner";
|
|
57
|
+
export { importProject } from "./import-engine";
|
|
58
|
+
|
|
51
59
|
// ─── High-level run function ──────────────────────────────────────────────────
|
|
52
60
|
|
|
53
61
|
import * as fs from "fs";
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
import { scanProjectContext, buildModificationPrompt, detectBestAgent } from "./context-scanner";
|
|
4
|
+
import { loadRegistry, getAgent } from "./registry";
|
|
5
|
+
import { runAgent } from "./index";
|
|
6
|
+
|
|
7
|
+
export interface ModificationOptions {
|
|
8
|
+
agent?: string;
|
|
9
|
+
engine?: string;
|
|
10
|
+
model?: string;
|
|
11
|
+
dryRun?: boolean;
|
|
12
|
+
scope?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Execute a targeted project modification.
|
|
17
|
+
*/
|
|
18
|
+
export async function runModification(
|
|
19
|
+
cwd: string,
|
|
20
|
+
description: string,
|
|
21
|
+
options: ModificationOptions = {}
|
|
22
|
+
): Promise<any> {
|
|
23
|
+
// 1. Scan context
|
|
24
|
+
const context = scanProjectContext(cwd);
|
|
25
|
+
|
|
26
|
+
// 2. Select agent
|
|
27
|
+
const selectedAgentId = options.agent || detectBestAgent(description, context);
|
|
28
|
+
|
|
29
|
+
console.log(`\n🛠️ [IMH-Code] Initializing Modification Engine`);
|
|
30
|
+
console.log(` Task: "${description}"`);
|
|
31
|
+
console.log(` Agent: ${selectedAgentId}`);
|
|
32
|
+
|
|
33
|
+
// Resolve agent manifests directory
|
|
34
|
+
const globalAgents = path.join(process.env.HOME || "", ".imhcode", "agents");
|
|
35
|
+
const localAgents = path.join(cwd, "agents");
|
|
36
|
+
const pkgAgents = path.join(__dirname, "..", "..", "agents");
|
|
37
|
+
const agentsDir = fs.existsSync(globalAgents) ? globalAgents :
|
|
38
|
+
fs.existsSync(localAgents) ? localAgents :
|
|
39
|
+
pkgAgents;
|
|
40
|
+
|
|
41
|
+
// 3. Load Registry
|
|
42
|
+
const { agents, errors } = await loadRegistry(agentsDir, cwd);
|
|
43
|
+
if (errors.some(e => e.agentId === selectedAgentId)) {
|
|
44
|
+
throw new Error(`Failed to load agent "${selectedAgentId}": ${errors.find(e => e.agentId === selectedAgentId)?.error}`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const agent = getAgent(agents, selectedAgentId);
|
|
48
|
+
|
|
49
|
+
// 4. Build custom modification task
|
|
50
|
+
const modificationTask = buildModificationPrompt(context, description);
|
|
51
|
+
|
|
52
|
+
// 5. Run the agent
|
|
53
|
+
const result = await runAgent(
|
|
54
|
+
agent,
|
|
55
|
+
modificationTask,
|
|
56
|
+
{
|
|
57
|
+
dryRun: options.dryRun !== false,
|
|
58
|
+
engine: options.engine,
|
|
59
|
+
model: options.model,
|
|
60
|
+
outputDir: path.join(cwd, ".imhcode", "sessions"),
|
|
61
|
+
cwd
|
|
62
|
+
}
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// 6. Log changes if live and successful
|
|
66
|
+
if (!result.dryRun && result.errors.length === 0) {
|
|
67
|
+
logModification(cwd, {
|
|
68
|
+
task: description,
|
|
69
|
+
agentId: selectedAgentId,
|
|
70
|
+
model: result.model,
|
|
71
|
+
durationMs: result.durationMs
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
interface LogEntry {
|
|
79
|
+
task: string;
|
|
80
|
+
agentId: string;
|
|
81
|
+
model: string;
|
|
82
|
+
durationMs: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function logModification(cwd: string, entry: LogEntry): void {
|
|
86
|
+
const docsDir = path.join(cwd, "docs");
|
|
87
|
+
if (!fs.existsSync(docsDir)) {
|
|
88
|
+
fs.mkdirSync(docsDir, { recursive: true });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const logPath = path.join(docsDir, "modifications.md");
|
|
92
|
+
const timestamp = new Date().toISOString().replace("T", " ").substring(0, 19);
|
|
93
|
+
|
|
94
|
+
let header = `# 🛠️ IMH-Code Project Modifications\n\n| Date | Task | Agent | Model | Duration |\n|------|------|-------|-------|----------|\n`;
|
|
95
|
+
if (fs.existsSync(logPath)) {
|
|
96
|
+
try {
|
|
97
|
+
const existing = fs.readFileSync(logPath, "utf-8");
|
|
98
|
+
if (existing.trim().length > 10) {
|
|
99
|
+
header = existing;
|
|
100
|
+
}
|
|
101
|
+
} catch {
|
|
102
|
+
// Ignore
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const row = `| ${timestamp} | ${entry.task} | \`${entry.agentId}\` | ${entry.model} | ${(entry.durationMs / 1000).toFixed(1)}s |\n`;
|
|
107
|
+
fs.writeFileSync(logPath, header.endsWith("\n") ? header + row : header + "\n" + row, "utf-8");
|
|
108
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import * as fs from "fs";
|
|
2
|
+
import * as path from "path";
|
|
3
|
+
|
|
4
|
+
export interface ScanResult {
|
|
5
|
+
detectedFrontend: string | null;
|
|
6
|
+
detectedBackend: string | null;
|
|
7
|
+
frontendPath: string | null;
|
|
8
|
+
backendPath: string | null;
|
|
9
|
+
database: string | null;
|
|
10
|
+
dockerized: boolean;
|
|
11
|
+
hasCICD: boolean;
|
|
12
|
+
packageJson?: any;
|
|
13
|
+
composerJson?: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Deep scan an existing codebase to detect stack and structure.
|
|
18
|
+
*/
|
|
19
|
+
export function scanProject(cwd: string): ScanResult {
|
|
20
|
+
const result: ScanResult = {
|
|
21
|
+
detectedFrontend: null,
|
|
22
|
+
detectedBackend: null,
|
|
23
|
+
frontendPath: null,
|
|
24
|
+
backendPath: null,
|
|
25
|
+
database: null,
|
|
26
|
+
dockerized: false,
|
|
27
|
+
hasCICD: false,
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// 1. Helper to search for files recursively with limits
|
|
31
|
+
function findFile(dir: string, name: string, depth = 0): string | null {
|
|
32
|
+
if (depth > 3) return null;
|
|
33
|
+
try {
|
|
34
|
+
const files = fs.readdirSync(dir);
|
|
35
|
+
if (files.includes(name)) {
|
|
36
|
+
return path.join(dir, name);
|
|
37
|
+
}
|
|
38
|
+
for (const file of files) {
|
|
39
|
+
if (file.startsWith(".") || file === "node_modules" || file === "dist" || file === "vendor") continue;
|
|
40
|
+
const fp = path.join(dir, file);
|
|
41
|
+
if (fs.statSync(fp).isDirectory()) {
|
|
42
|
+
const found = findFile(fp, name, depth + 1);
|
|
43
|
+
if (found) return found;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
} catch {
|
|
47
|
+
// Ignore
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// 2. Scan for package.json (Frontend detection)
|
|
53
|
+
const pkgJsonPath = findFile(cwd, "package.json");
|
|
54
|
+
if (pkgJsonPath) {
|
|
55
|
+
try {
|
|
56
|
+
const content = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
|
|
57
|
+
result.packageJson = content;
|
|
58
|
+
const deps = { ...(content.dependencies || {}), ...(content.devDependencies || {}) };
|
|
59
|
+
|
|
60
|
+
if (deps.next) result.detectedFrontend = "Next.js";
|
|
61
|
+
else if (deps.nuxt || deps.nuxt3 || deps.nuxt4) result.detectedFrontend = "Nuxt/Vue";
|
|
62
|
+
else if (deps.react) result.detectedFrontend = "React SPA";
|
|
63
|
+
else if (deps.vue) result.detectedFrontend = "Vue SPA";
|
|
64
|
+
else if (deps.svelte) result.detectedFrontend = "Svelte";
|
|
65
|
+
else result.detectedFrontend = "Node/JS";
|
|
66
|
+
|
|
67
|
+
// If package.json is in a subdirectory, use that subdirectory
|
|
68
|
+
const relative = path.relative(cwd, path.dirname(pkgJsonPath));
|
|
69
|
+
result.frontendPath = relative === "" ? "." : relative;
|
|
70
|
+
} catch {
|
|
71
|
+
// Ignore
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 3. Scan for composer.json, requirements.txt, or pom.xml (Backend detection)
|
|
76
|
+
const composerPath = findFile(cwd, "composer.json");
|
|
77
|
+
if (composerPath) {
|
|
78
|
+
try {
|
|
79
|
+
const content = JSON.parse(fs.readFileSync(composerPath, "utf-8"));
|
|
80
|
+
result.composerJson = content;
|
|
81
|
+
if (content.require && (content.require["laravel/framework"] || content.require["laravel/lumen-framework"])) {
|
|
82
|
+
result.detectedBackend = "Laravel";
|
|
83
|
+
} else {
|
|
84
|
+
result.detectedBackend = "PHP";
|
|
85
|
+
}
|
|
86
|
+
const relative = path.relative(cwd, path.dirname(composerPath));
|
|
87
|
+
result.backendPath = relative === "" ? "." : relative;
|
|
88
|
+
} catch {
|
|
89
|
+
// Ignore
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!result.detectedBackend) {
|
|
94
|
+
const reqsTxtPath = findFile(cwd, "requirements.txt");
|
|
95
|
+
const pyprojectPath = findFile(cwd, "pyproject.toml");
|
|
96
|
+
if (reqsTxtPath || pyprojectPath) {
|
|
97
|
+
result.detectedBackend = "Python (Django/FastAPI)";
|
|
98
|
+
const matchedPath = reqsTxtPath || pyprojectPath || "";
|
|
99
|
+
const relative = path.relative(cwd, path.dirname(matchedPath));
|
|
100
|
+
result.backendPath = relative === "" ? "." : relative;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// 4. Database Detection from env or configs
|
|
105
|
+
const envPath = findFile(cwd, ".env") || findFile(cwd, ".env.example");
|
|
106
|
+
if (envPath) {
|
|
107
|
+
try {
|
|
108
|
+
const content = fs.readFileSync(envPath, "utf-8");
|
|
109
|
+
if (content.includes("DB_CONNECTION=pgsql") || content.includes("postgresql://")) {
|
|
110
|
+
result.database = "PostgreSQL";
|
|
111
|
+
} else if (content.includes("DB_CONNECTION=mysql") || content.includes("mysql://")) {
|
|
112
|
+
result.database = "MySQL";
|
|
113
|
+
} else if (content.includes("DB_CONNECTION=sqlite") || content.includes("sqlite3")) {
|
|
114
|
+
result.database = "SQLite";
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// Ignore
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 5. Infrastructure detection
|
|
122
|
+
const dockerCompose = findFile(cwd, "docker-compose.yml");
|
|
123
|
+
const dockerfile = findFile(cwd, "Dockerfile");
|
|
124
|
+
if (dockerCompose || dockerfile) {
|
|
125
|
+
result.dockerized = true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const githubWorkflows = findFile(cwd, ".github/workflows");
|
|
129
|
+
if (githubWorkflows) {
|
|
130
|
+
result.hasCICD = true;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return result;
|
|
134
|
+
}
|