deepdebug-local-agent 1.0.10 β 1.0.11
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/package.json +1 -1
- package/src/routes/workspace.route.js +434 -199
- package/src/server.js +87 -3
package/package.json
CHANGED
|
@@ -6,10 +6,87 @@ import os from "os";
|
|
|
6
6
|
const router = express.Router();
|
|
7
7
|
|
|
8
8
|
// πΉ Caminho global do workspace (inicializado com fallback padrΓ£o)
|
|
9
|
+
// β
FIX: Read WORKSPACE_ROOT (set by install.js Docker) OR WORKSPACE_BASE (legacy)
|
|
9
10
|
let workspacePath =
|
|
11
|
+
process.env.WORKSPACE_ROOT ||
|
|
10
12
|
process.env.WORKSPACE_BASE ||
|
|
11
13
|
path.join(os.homedir(), "Documents", "Projects");
|
|
12
14
|
|
|
15
|
+
// π HostβDocker path translation
|
|
16
|
+
// When Gateway sends host paths (C:\Users\willi\Projects\my-app),
|
|
17
|
+
// we need to translate them to Docker paths (/workspace/my-app)
|
|
18
|
+
const WORKSPACE_MOUNT = process.env.WORKSPACE_ROOT || "/workspace";
|
|
19
|
+
const HOST_PROJECTS_PATH = process.env.HOST_PROJECTS_PATH || null;
|
|
20
|
+
|
|
21
|
+
console.log(`π [workspace_route] Initialized with workspacePath: ${workspacePath}`);
|
|
22
|
+
console.log(`π [workspace_route] WORKSPACE_MOUNT: ${WORKSPACE_MOUNT}`);
|
|
23
|
+
console.log(`π [workspace_route] HOST_PROJECTS_PATH: ${HOST_PROJECTS_PATH || 'not set'}`);
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Translates a host-side path to the Docker container path.
|
|
27
|
+
*
|
|
28
|
+
* Examples:
|
|
29
|
+
* C:\Users\willi\Documents\Projects\pure-core-ms β /workspace/pure-core-ms
|
|
30
|
+
* /Users/macintosh/IdeaProjects/pure-core-ms β /workspace/pure-core-ms
|
|
31
|
+
* /workspace/pure-core-ms β /workspace/pure-core-ms (unchanged)
|
|
32
|
+
* src/main/java/... β src/main/java/... (relative, unchanged)
|
|
33
|
+
*/
|
|
34
|
+
function translateToDockerPath(hostPath) {
|
|
35
|
+
if (!hostPath) return workspacePath;
|
|
36
|
+
|
|
37
|
+
// Normalize path separators (Windows β Unix)
|
|
38
|
+
let normalized = hostPath.replace(/\\/g, '/');
|
|
39
|
+
|
|
40
|
+
// Already a Docker path
|
|
41
|
+
if (normalized.startsWith('/workspace')) {
|
|
42
|
+
return normalized;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Relative path (like src/main/java/...) - not a workspace root
|
|
46
|
+
if (!path.isAbsolute(normalized) && !normalized.match(/^[A-Za-z]:/)) {
|
|
47
|
+
return normalized;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// If we know the host projects root, strip it
|
|
51
|
+
if (HOST_PROJECTS_PATH) {
|
|
52
|
+
const normalizedHost = HOST_PROJECTS_PATH.replace(/\\/g, '/').replace(/\/$/, '');
|
|
53
|
+
if (normalized.toLowerCase().startsWith(normalizedHost.toLowerCase())) {
|
|
54
|
+
const relative = normalized.substring(normalizedHost.length).replace(/^\//, '');
|
|
55
|
+
const dockerPath = path.posix.join(WORKSPACE_MOUNT, relative);
|
|
56
|
+
console.log(`π [translate] ${hostPath} β ${dockerPath} (via HOST_PROJECTS_PATH)`);
|
|
57
|
+
return dockerPath;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Smart detection: look for common project root patterns
|
|
62
|
+
const projectRootPatterns = [
|
|
63
|
+
/.*[/\\](Documents[/\\]Projects)[/\\]/i,
|
|
64
|
+
/.*[/\\](IdeaProjects)[/\\]/i,
|
|
65
|
+
/.*[/\\](source[/\\]repos)[/\\]/i,
|
|
66
|
+
/.*[/\\](Projects)[/\\]/i,
|
|
67
|
+
/.*[/\\](repos)[/\\]/i,
|
|
68
|
+
/.*[/\\](dev)[/\\]/i,
|
|
69
|
+
/.*[/\\](code)[/\\]/i,
|
|
70
|
+
];
|
|
71
|
+
|
|
72
|
+
for (const pattern of projectRootPatterns) {
|
|
73
|
+
const match = normalized.match(pattern);
|
|
74
|
+
if (match) {
|
|
75
|
+
const afterRoot = normalized.substring(match.index + match[0].length);
|
|
76
|
+
const dockerPath = path.posix.join(WORKSPACE_MOUNT, afterRoot);
|
|
77
|
+
console.log(`π [translate] ${hostPath} β ${dockerPath} (via pattern: ${match[1]})`);
|
|
78
|
+
return dockerPath;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Last resort: extract the last folder name
|
|
83
|
+
const parts = normalized.replace(/\/$/, '').split('/');
|
|
84
|
+
const projectName = parts[parts.length - 1];
|
|
85
|
+
const dockerPath = path.posix.join(WORKSPACE_MOUNT, projectName);
|
|
86
|
+
console.log(`π [translate] ${hostPath} β ${dockerPath} (via last folder name)`);
|
|
87
|
+
return dockerPath;
|
|
88
|
+
}
|
|
89
|
+
|
|
13
90
|
// --------------------------------------------------
|
|
14
91
|
// π§© 1οΈβ£ GET /workspace/info
|
|
15
92
|
// Retorna informaΓ§Γ΅es sobre o workspace atual
|
|
@@ -22,6 +99,11 @@ router.get("/info", async (req, res) => {
|
|
|
22
99
|
isDirectory: fs.existsSync(workspacePath)
|
|
23
100
|
? fs.lstatSync(workspacePath).isDirectory()
|
|
24
101
|
: false,
|
|
102
|
+
env: {
|
|
103
|
+
WORKSPACE_ROOT: process.env.WORKSPACE_ROOT || null,
|
|
104
|
+
WORKSPACE_BASE: process.env.WORKSPACE_BASE || null,
|
|
105
|
+
HOST_PROJECTS_PATH: process.env.HOST_PROJECTS_PATH || null,
|
|
106
|
+
}
|
|
25
107
|
});
|
|
26
108
|
} catch (err) {
|
|
27
109
|
console.error("β Error reading workspace info:", err);
|
|
@@ -29,20 +111,246 @@ router.get("/info", async (req, res) => {
|
|
|
29
111
|
}
|
|
30
112
|
});
|
|
31
113
|
|
|
114
|
+
// --------------------------------------------------
|
|
115
|
+
// π POST /workspace/open
|
|
116
|
+
// Gateway Java (LocalAgentClient.ensureActiveWorkspaceOpen) calls this
|
|
117
|
+
//
|
|
118
|
+
// TWO MODES:
|
|
119
|
+
// 1. LOCAL MODE: { "root": "/host/path/to/project" }
|
|
120
|
+
// β Translates host paths to Docker container paths (for Docker local)
|
|
121
|
+
//
|
|
122
|
+
// 2. CLOUD MODE: { "repoUrl": "https://github.com/org/repo", "token": "ghp_xxx", "branch": "main" }
|
|
123
|
+
// β Clones the repo via git (for Cloud Run β no local filesystem)
|
|
124
|
+
// β Caches in /tmp/workspaces/{repoName} to avoid re-cloning
|
|
125
|
+
// --------------------------------------------------
|
|
126
|
+
|
|
127
|
+
// Git clone cache directory (survives between requests in same Cloud Run instance)
|
|
128
|
+
const WORKSPACE_CACHE = process.env.WORKSPACE_CACHE || '/tmp/workspaces';
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Execute a shell command and return { stdout, stderr, exitCode }
|
|
132
|
+
*/
|
|
133
|
+
function execAsync(cmd, options = {}) {
|
|
134
|
+
const { execSync } = require('child_process');
|
|
135
|
+
try {
|
|
136
|
+
const stdout = execSync(cmd, {
|
|
137
|
+
encoding: 'utf8',
|
|
138
|
+
timeout: options.timeout || 120000,
|
|
139
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
140
|
+
...options
|
|
141
|
+
});
|
|
142
|
+
return { stdout: stdout.trim(), stderr: '', exitCode: 0 };
|
|
143
|
+
} catch (err) {
|
|
144
|
+
return {
|
|
145
|
+
stdout: err.stdout ? err.stdout.toString().trim() : '',
|
|
146
|
+
stderr: err.stderr ? err.stderr.toString().trim() : err.message,
|
|
147
|
+
exitCode: err.status || 1
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clone or update a git repository.
|
|
154
|
+
* Returns the local path where the repo is checked out.
|
|
155
|
+
*/
|
|
156
|
+
async function ensureRepoCloned(repoUrl, token, branch = 'main') {
|
|
157
|
+
// Extract repo name from URL
|
|
158
|
+
let repoName = repoUrl.split('/').pop().replace('.git', '');
|
|
159
|
+
// Include org for uniqueness: org/repo β org_repo
|
|
160
|
+
const urlParts = repoUrl.replace(/\.git$/, '').split('/');
|
|
161
|
+
if (urlParts.length >= 2) {
|
|
162
|
+
const org = urlParts[urlParts.length - 2];
|
|
163
|
+
repoName = `${org}_${repoName}`;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const clonePath = path.join(WORKSPACE_CACHE, repoName);
|
|
167
|
+
|
|
168
|
+
// Ensure cache directory exists
|
|
169
|
+
if (!fs.existsSync(WORKSPACE_CACHE)) {
|
|
170
|
+
fs.mkdirSync(WORKSPACE_CACHE, { recursive: true });
|
|
171
|
+
console.log(`π Created workspace cache: ${WORKSPACE_CACHE}`);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Build authenticated URL if token provided
|
|
175
|
+
const authUrl = token
|
|
176
|
+
? repoUrl.replace('https://', `https://x-access-token:${token}@`)
|
|
177
|
+
: repoUrl;
|
|
178
|
+
|
|
179
|
+
if (fs.existsSync(path.join(clonePath, '.git'))) {
|
|
180
|
+
// Already cloned β git fetch + checkout + pull
|
|
181
|
+
console.log(`π [git] Repo already cloned: ${clonePath}, updating...`);
|
|
182
|
+
|
|
183
|
+
// Fetch latest
|
|
184
|
+
let result = execAsync(`git fetch origin`, { cwd: clonePath });
|
|
185
|
+
if (result.exitCode !== 0) {
|
|
186
|
+
// Try with token in remote URL
|
|
187
|
+
execAsync(`git remote set-url origin "${authUrl}"`, { cwd: clonePath });
|
|
188
|
+
result = execAsync(`git fetch origin`, { cwd: clonePath });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Checkout correct branch
|
|
192
|
+
execAsync(`git checkout ${branch} 2>/dev/null || git checkout -b ${branch} origin/${branch}`, { cwd: clonePath });
|
|
193
|
+
|
|
194
|
+
// Reset to remote (discard any local patches from previous fixes)
|
|
195
|
+
execAsync(`git reset --hard origin/${branch}`, { cwd: clonePath });
|
|
196
|
+
|
|
197
|
+
console.log(`β
[git] Updated to latest ${branch}: ${clonePath}`);
|
|
198
|
+
} else {
|
|
199
|
+
// Fresh clone
|
|
200
|
+
console.log(`π [git] Cloning ${repoUrl} (branch: ${branch})...`);
|
|
201
|
+
|
|
202
|
+
const result = execAsync(
|
|
203
|
+
`git clone --branch ${branch} --single-branch --depth 50 "${authUrl}" "${clonePath}"`,
|
|
204
|
+
{ timeout: 180000 } // 3 minutes for large repos
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
if (result.exitCode !== 0) {
|
|
208
|
+
throw new Error(`Git clone failed: ${result.stderr}`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Configure git user for future commits
|
|
212
|
+
execAsync(`git config user.email "deepdebug-ai@deepdebug.ai"`, { cwd: clonePath });
|
|
213
|
+
execAsync(`git config user.name "DeepDebug AI"`, { cwd: clonePath });
|
|
214
|
+
|
|
215
|
+
// Store the authenticated remote URL for future push
|
|
216
|
+
if (token) {
|
|
217
|
+
execAsync(`git remote set-url origin "${authUrl}"`, { cwd: clonePath });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
console.log(`β
[git] Cloned successfully: ${clonePath}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
return clonePath;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
router.post("/open", async (req, res) => {
|
|
227
|
+
try {
|
|
228
|
+
const { root, workspacePath: wpPath, repoUrl, token, branch = 'main' } = req.body;
|
|
229
|
+
|
|
230
|
+
// ==========================================
|
|
231
|
+
// CLOUD MODE: Git clone
|
|
232
|
+
// ==========================================
|
|
233
|
+
if (repoUrl) {
|
|
234
|
+
console.log(`π [/workspace/open] CLOUD MODE β cloning ${repoUrl} (branch: ${branch})`);
|
|
235
|
+
|
|
236
|
+
try {
|
|
237
|
+
const clonePath = await ensureRepoCloned(repoUrl, token, branch);
|
|
238
|
+
|
|
239
|
+
// Detect project type
|
|
240
|
+
const files = fs.readdirSync(clonePath);
|
|
241
|
+
let language = "unknown", buildTool = "unknown", marker = null;
|
|
242
|
+
if (files.includes("pom.xml")) { language = "java"; buildTool = "maven"; marker = "pom.xml"; }
|
|
243
|
+
else if (files.includes("build.gradle") || files.includes("build.gradle.kts")) { language = "java"; buildTool = "gradle"; marker = "build.gradle"; }
|
|
244
|
+
else if (files.includes("package.json")) { language = "node"; buildTool = "npm"; marker = "package.json"; }
|
|
245
|
+
else if (files.includes("requirements.txt") || files.includes("setup.py")) { language = "python"; buildTool = "pip"; marker = "requirements.txt"; }
|
|
246
|
+
else if (files.includes("go.mod")) { language = "go"; buildTool = "go"; marker = "go.mod"; }
|
|
247
|
+
else if (files.some(f => f.endsWith(".csproj"))) { language = "csharp"; buildTool = "dotnet"; marker = ".csproj"; }
|
|
248
|
+
|
|
249
|
+
workspacePath = clonePath;
|
|
250
|
+
console.log(`β
[/workspace/open] Cloud workspace set to: ${workspacePath} (${language}/${buildTool})`);
|
|
251
|
+
|
|
252
|
+
return res.json({
|
|
253
|
+
ok: true,
|
|
254
|
+
root: workspacePath,
|
|
255
|
+
mode: "cloud-clone",
|
|
256
|
+
meta: { language, buildTool, marker },
|
|
257
|
+
port: null
|
|
258
|
+
});
|
|
259
|
+
} catch (gitErr) {
|
|
260
|
+
console.error(`β [/workspace/open] Git clone failed:`, gitErr.message);
|
|
261
|
+
return res.status(500).json({
|
|
262
|
+
ok: false,
|
|
263
|
+
error: `Git clone failed: ${gitErr.message}`,
|
|
264
|
+
hint: "Check that the repository URL and token are correct."
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ==========================================
|
|
270
|
+
// LOCAL MODE: Path-based (Docker local)
|
|
271
|
+
// ==========================================
|
|
272
|
+
const requestedPath = root || wpPath;
|
|
273
|
+
|
|
274
|
+
if (!requestedPath) {
|
|
275
|
+
return res.status(400).json({ error: "Missing 'root', 'workspacePath', or 'repoUrl' in body" });
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Translate host path to docker path if needed
|
|
279
|
+
let resolvedPath = translateToDockerPath(requestedPath);
|
|
280
|
+
resolvedPath = path.resolve(resolvedPath);
|
|
281
|
+
|
|
282
|
+
console.log(`π [/workspace/open] LOCAL MODE β Requested: ${requestedPath}`);
|
|
283
|
+
console.log(`π [/workspace/open] Resolved: ${resolvedPath}`);
|
|
284
|
+
|
|
285
|
+
if (!fs.existsSync(resolvedPath) || !fs.lstatSync(resolvedPath).isDirectory()) {
|
|
286
|
+
// If translated path doesn't exist, try the raw path as fallback
|
|
287
|
+
const rawResolved = path.resolve(requestedPath.replace(/\\/g, '/'));
|
|
288
|
+
if (fs.existsSync(rawResolved) && fs.lstatSync(rawResolved).isDirectory()) {
|
|
289
|
+
resolvedPath = rawResolved;
|
|
290
|
+
console.log(`π [/workspace/open] Fallback to raw path: ${resolvedPath}`);
|
|
291
|
+
} else {
|
|
292
|
+
// List what IS in /workspace to help debug
|
|
293
|
+
let availableDirs = [];
|
|
294
|
+
try {
|
|
295
|
+
if (fs.existsSync(WORKSPACE_MOUNT)) {
|
|
296
|
+
availableDirs = fs.readdirSync(WORKSPACE_MOUNT, { withFileTypes: true })
|
|
297
|
+
.filter(d => d.isDirectory())
|
|
298
|
+
.map(d => d.name);
|
|
299
|
+
}
|
|
300
|
+
} catch (e) {}
|
|
301
|
+
|
|
302
|
+
console.error(`β [/workspace/open] Directory not found: ${resolvedPath}`);
|
|
303
|
+
return res.status(400).json({
|
|
304
|
+
error: `Directory not found: ${resolvedPath}`,
|
|
305
|
+
requestedPath, resolvedPath,
|
|
306
|
+
availableProjects: availableDirs,
|
|
307
|
+
hint: "The workspace path might not be mounted in Docker. Check docker -v mount."
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Detect project type
|
|
313
|
+
const files = fs.readdirSync(resolvedPath);
|
|
314
|
+
let language = "unknown", buildTool = "unknown", marker = null;
|
|
315
|
+
if (files.includes("pom.xml")) { language = "java"; buildTool = "maven"; marker = "pom.xml"; }
|
|
316
|
+
else if (files.includes("build.gradle") || files.includes("build.gradle.kts")) { language = "java"; buildTool = "gradle"; marker = "build.gradle"; }
|
|
317
|
+
else if (files.includes("package.json")) { language = "node"; buildTool = "npm"; marker = "package.json"; }
|
|
318
|
+
else if (files.includes("requirements.txt") || files.includes("setup.py")) { language = "python"; buildTool = "pip"; marker = "requirements.txt"; }
|
|
319
|
+
else if (files.includes("go.mod")) { language = "go"; buildTool = "go"; marker = "go.mod"; }
|
|
320
|
+
|
|
321
|
+
workspacePath = resolvedPath;
|
|
322
|
+
console.log(`β
[/workspace/open] Local workspace set to: ${workspacePath} (${language}/${buildTool})`);
|
|
323
|
+
|
|
324
|
+
return res.json({
|
|
325
|
+
ok: true,
|
|
326
|
+
root: workspacePath,
|
|
327
|
+
mode: "local",
|
|
328
|
+
meta: { language, buildTool, marker },
|
|
329
|
+
port: null
|
|
330
|
+
});
|
|
331
|
+
} catch (err) {
|
|
332
|
+
console.error("β [/workspace/open] Error:", err);
|
|
333
|
+
return res.status(500).json({ error: err.message });
|
|
334
|
+
}
|
|
335
|
+
});
|
|
336
|
+
|
|
32
337
|
// --------------------------------------------------
|
|
33
338
|
// π§© 2οΈβ£ POST /workspace
|
|
34
339
|
// Atualiza o diretΓ³rio de workspace selecionado pelo usuΓ‘rio
|
|
340
|
+
// (legacy endpoint - kept for backward compatibility)
|
|
35
341
|
// --------------------------------------------------
|
|
36
342
|
router.post("/", async (req, res) => {
|
|
37
343
|
try {
|
|
38
344
|
console.log(workspacePath)
|
|
39
|
-
const { workspacePath: newPath } = req.body;
|
|
40
|
-
|
|
41
|
-
|
|
345
|
+
const { workspacePath: newPath, root } = req.body;
|
|
346
|
+
const requestedPath = newPath || root;
|
|
347
|
+
if (!requestedPath) {
|
|
348
|
+
return res.status(400).json({ error: "Missing workspacePath or root" });
|
|
42
349
|
}
|
|
43
350
|
|
|
44
|
-
//
|
|
45
|
-
|
|
351
|
+
// Translate host path if in Docker
|
|
352
|
+
let resolvedPath = translateToDockerPath(requestedPath);
|
|
353
|
+
resolvedPath = path.resolve(resolvedPath);
|
|
46
354
|
|
|
47
355
|
if (!fs.existsSync(resolvedPath) || !fs.lstatSync(resolvedPath).isDirectory()) {
|
|
48
356
|
return res
|
|
@@ -78,9 +386,27 @@ router.get("/files", async (req, res) => {
|
|
|
78
386
|
for (const entry of entries) {
|
|
79
387
|
const fullPath = path.join(dir, entry.name);
|
|
80
388
|
if (entry.isDirectory()) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
389
|
+
if (
|
|
390
|
+
!["node_modules", ".git", "target", "build", ".idea"].includes(
|
|
391
|
+
entry.name
|
|
392
|
+
)
|
|
393
|
+
) {
|
|
394
|
+
walk(fullPath, depth + 1);
|
|
395
|
+
}
|
|
396
|
+
} else if (
|
|
397
|
+
entry.name.endsWith(".java") ||
|
|
398
|
+
entry.name.endsWith(".js") ||
|
|
399
|
+
entry.name.endsWith(".ts") ||
|
|
400
|
+
entry.name.endsWith(".py") ||
|
|
401
|
+
entry.name.endsWith(".go") ||
|
|
402
|
+
entry.name.endsWith(".cs") ||
|
|
403
|
+
entry.name.endsWith(".xml") ||
|
|
404
|
+
entry.name.endsWith(".yml") ||
|
|
405
|
+
entry.name.endsWith(".yaml") ||
|
|
406
|
+
entry.name.endsWith(".json") ||
|
|
407
|
+
entry.name.endsWith(".properties")
|
|
408
|
+
) {
|
|
409
|
+
files.push(path.relative(workspacePath, fullPath));
|
|
84
410
|
}
|
|
85
411
|
}
|
|
86
412
|
}
|
|
@@ -88,7 +414,7 @@ router.get("/files", async (req, res) => {
|
|
|
88
414
|
walk(workspacePath);
|
|
89
415
|
return res.json({ workspacePath, files });
|
|
90
416
|
} catch (err) {
|
|
91
|
-
console.error("β Error listing files:", err);
|
|
417
|
+
console.error("β Error listing workspace files:", err);
|
|
92
418
|
return res.status(500).json({ error: err.message });
|
|
93
419
|
}
|
|
94
420
|
});
|
|
@@ -144,8 +470,7 @@ router.get("/file", async (req, res) => {
|
|
|
144
470
|
|
|
145
471
|
// --------------------------------------------------
|
|
146
472
|
// π§© 5οΈβ£ POST /workspace/resolve
|
|
147
|
-
// Resolve caminho
|
|
148
|
-
// (usado quando o dev escolhe o workspace no Setup Wizard)
|
|
473
|
+
// Resolve caminho relativo a absoluto (para exibiΓ§Γ£o no frontend)
|
|
149
474
|
// --------------------------------------------------
|
|
150
475
|
router.post("/resolve", async (req, res) => {
|
|
151
476
|
try {
|
|
@@ -200,94 +525,62 @@ router.get("/detect", async (req, res) => {
|
|
|
200
525
|
// ==========================================
|
|
201
526
|
|
|
202
527
|
// --------------------------------------------------
|
|
203
|
-
// π§© 7οΈβ£
|
|
204
|
-
// Busca
|
|
205
|
-
// Usado pelo InvestigationModeService para encontrar arquivos por hint
|
|
528
|
+
// π§© 7οΈβ£ GET /workspace/search-file
|
|
529
|
+
// Busca um arquivo pelo nome dentro do workspace
|
|
206
530
|
// --------------------------------------------------
|
|
207
|
-
router.
|
|
531
|
+
router.get("/search-file", async (req, res) => {
|
|
208
532
|
try {
|
|
209
|
-
const { fileName } = req.
|
|
210
|
-
|
|
533
|
+
const { fileName } = req.query;
|
|
211
534
|
if (!fileName) {
|
|
212
535
|
return res.status(400).json({ error: "Missing fileName" });
|
|
213
536
|
}
|
|
214
537
|
|
|
215
538
|
if (!workspacePath || !fs.existsSync(workspacePath)) {
|
|
216
|
-
return res.status(400).json({ error: "Workspace not configured
|
|
539
|
+
return res.status(400).json({ error: "Workspace not configured" });
|
|
217
540
|
}
|
|
218
541
|
|
|
219
542
|
console.log(`π [search-file] Searching for: ${fileName} in ${workspacePath}`);
|
|
220
543
|
|
|
221
544
|
let foundPath = null;
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
// FunΓ§Γ£o de busca recursiva
|
|
225
|
-
function searchRecursive(dir, depth = 0) {
|
|
226
|
-
if (depth > 10 || foundPath) return; // Limitar profundidade
|
|
545
|
+
const alternatives = [];
|
|
227
546
|
|
|
547
|
+
function searchFile(dir, depth = 0) {
|
|
548
|
+
if (depth > 10 || foundPath) return;
|
|
228
549
|
try {
|
|
229
550
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
230
|
-
|
|
231
551
|
for (const entry of entries) {
|
|
232
|
-
|
|
233
|
-
if (entry.name.startsWith('.') ||
|
|
234
|
-
entry.name === 'node_modules' ||
|
|
235
|
-
entry.name === 'target' ||
|
|
236
|
-
entry.name === 'build' ||
|
|
237
|
-
entry.name === '.git') {
|
|
238
|
-
continue;
|
|
239
|
-
}
|
|
240
|
-
|
|
552
|
+
if (foundPath) return;
|
|
241
553
|
const fullPath = path.join(dir, entry.name);
|
|
242
|
-
|
|
243
554
|
if (entry.isDirectory()) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
555
|
+
if (!["node_modules", ".git", "target", "build", ".idea", ".gradle"].includes(entry.name)) {
|
|
556
|
+
searchFile(fullPath, depth + 1);
|
|
557
|
+
}
|
|
558
|
+
} else {
|
|
559
|
+
// Exact match
|
|
560
|
+
if (entry.name === fileName || entry.name === path.basename(fileName)) {
|
|
249
561
|
foundPath = path.relative(workspacePath, fullPath);
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
} else if (entry.name.includes(fileName.replace('.java', ''))) {
|
|
255
|
-
matches.push({
|
|
562
|
+
}
|
|
563
|
+
// Partial match for alternatives
|
|
564
|
+
else if (entry.name.toLowerCase().includes(path.basename(fileName).toLowerCase().replace('.java', '').replace('.js', ''))) {
|
|
565
|
+
alternatives.push({
|
|
256
566
|
path: path.relative(workspacePath, fullPath),
|
|
257
|
-
|
|
567
|
+
name: entry.name
|
|
258
568
|
});
|
|
259
569
|
}
|
|
260
570
|
}
|
|
261
571
|
}
|
|
262
|
-
} catch (
|
|
263
|
-
// Ignorar erros de permissΓ£o
|
|
264
|
-
console.warn(`β οΈ Cannot read directory: ${dir}`);
|
|
265
|
-
}
|
|
572
|
+
} catch (e) { /* ignore permission errors */ }
|
|
266
573
|
}
|
|
267
574
|
|
|
268
|
-
|
|
575
|
+
searchFile(workspacePath);
|
|
269
576
|
|
|
270
577
|
if (foundPath) {
|
|
271
578
|
console.log(`β
[search-file] Found: ${foundPath}`);
|
|
272
|
-
return res.json({
|
|
273
|
-
found: true,
|
|
274
|
-
path: foundPath,
|
|
275
|
-
matches: matches.slice(0, 10) // Limitar matches
|
|
276
|
-
});
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Se nΓ£o encontrou match exato, retornar matches parciais
|
|
280
|
-
if (matches.length > 0) {
|
|
281
|
-
console.log(`πΆ [search-file] Partial matches found: ${matches.length}`);
|
|
282
|
-
return res.json({
|
|
283
|
-
found: true,
|
|
284
|
-
path: matches[0].path,
|
|
285
|
-
matches: matches.slice(0, 10)
|
|
286
|
-
});
|
|
579
|
+
return res.json({ found: true, path: foundPath, alternatives: alternatives.slice(0, 5) });
|
|
287
580
|
}
|
|
288
581
|
|
|
289
|
-
console.log(
|
|
290
|
-
return res.json({ found: false,
|
|
582
|
+
console.log(`β οΈ [search-file] Not found: ${fileName} (${alternatives.length} alternatives)`);
|
|
583
|
+
return res.json({ found: false, path: null, alternatives: alternatives.slice(0, 10) });
|
|
291
584
|
|
|
292
585
|
} catch (err) {
|
|
293
586
|
console.error("β Error searching file:", err);
|
|
@@ -296,189 +589,129 @@ router.post("/search-file", async (req, res) => {
|
|
|
296
589
|
});
|
|
297
590
|
|
|
298
591
|
// --------------------------------------------------
|
|
299
|
-
// π§© 8οΈβ£
|
|
300
|
-
//
|
|
301
|
-
// Usado pelo InvestigationModeService como ΓΊltima estratΓ©gia
|
|
592
|
+
// π§© 8οΈβ£ POST /workspace/search
|
|
593
|
+
// Busca por conteΓΊdo dentro dos arquivos do workspace
|
|
302
594
|
// --------------------------------------------------
|
|
303
|
-
router.
|
|
595
|
+
router.post("/search", async (req, res) => {
|
|
304
596
|
try {
|
|
305
|
-
const
|
|
597
|
+
const { query, filePattern, maxResults = 20 } = req.body;
|
|
598
|
+
if (!query) {
|
|
599
|
+
return res.status(400).json({ error: "Missing 'query' in body" });
|
|
600
|
+
}
|
|
306
601
|
|
|
307
602
|
if (!workspacePath || !fs.existsSync(workspacePath)) {
|
|
308
|
-
return res.status(400).json({ error: "Workspace not configured
|
|
603
|
+
return res.status(400).json({ error: "Workspace not configured" });
|
|
309
604
|
}
|
|
310
605
|
|
|
311
|
-
console.log(
|
|
312
|
-
|
|
313
|
-
const allFiles = [];
|
|
606
|
+
console.log(`π [search] Searching for: "${query}" in ${workspacePath}`);
|
|
314
607
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
if (depth > 5) return; // Limitar profundidade
|
|
608
|
+
const results = [];
|
|
609
|
+
const extensions = ['.java', '.js', '.ts', '.py', '.go', '.cs', '.xml', '.yml', '.yaml', '.json', '.properties', '.html', '.css'];
|
|
318
610
|
|
|
611
|
+
function searchContent(dir, depth = 0) {
|
|
612
|
+
if (depth > 8 || results.length >= maxResults) return;
|
|
319
613
|
try {
|
|
320
614
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
321
|
-
|
|
322
615
|
for (const entry of entries) {
|
|
323
|
-
|
|
324
|
-
if (entry.name.startsWith('.') ||
|
|
325
|
-
entry.name === 'node_modules' ||
|
|
326
|
-
entry.name === 'target' ||
|
|
327
|
-
entry.name === 'build' ||
|
|
328
|
-
entry.name === '.git') {
|
|
329
|
-
continue;
|
|
330
|
-
}
|
|
331
|
-
|
|
616
|
+
if (results.length >= maxResults) return;
|
|
332
617
|
const fullPath = path.join(dir, entry.name);
|
|
333
|
-
|
|
334
618
|
if (entry.isDirectory()) {
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
(entry.name.endsWith('.java') ||
|
|
338
|
-
entry.name.endsWith('.js') ||
|
|
339
|
-
entry.name.endsWith('.ts'))) {
|
|
340
|
-
try {
|
|
341
|
-
const stats = fs.statSync(fullPath);
|
|
342
|
-
allFiles.push({
|
|
343
|
-
path: path.relative(workspacePath, fullPath),
|
|
344
|
-
name: entry.name,
|
|
345
|
-
modified: stats.mtime,
|
|
346
|
-
size: stats.size
|
|
347
|
-
});
|
|
348
|
-
} catch (e) {
|
|
349
|
-
// Ignorar arquivos que nΓ£o podem ser lidos
|
|
619
|
+
if (!["node_modules", ".git", "target", "build", ".idea", ".gradle", "__pycache__"].includes(entry.name)) {
|
|
620
|
+
searchContent(fullPath, depth + 1);
|
|
350
621
|
}
|
|
622
|
+
} else if (extensions.some(ext => entry.name.endsWith(ext))) {
|
|
623
|
+
if (filePattern && !entry.name.match(new RegExp(filePattern, 'i'))) continue;
|
|
624
|
+
try {
|
|
625
|
+
const content = fs.readFileSync(fullPath, 'utf8');
|
|
626
|
+
const lines = content.split('\n');
|
|
627
|
+
for (let i = 0; i < lines.length; i++) {
|
|
628
|
+
if (lines[i].includes(query)) {
|
|
629
|
+
results.push({
|
|
630
|
+
file: path.relative(workspacePath, fullPath),
|
|
631
|
+
line: i + 1,
|
|
632
|
+
content: lines[i].trim().substring(0, 200),
|
|
633
|
+
context: lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 3)).join('\n')
|
|
634
|
+
});
|
|
635
|
+
if (results.length >= maxResults) return;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} catch (e) { /* ignore read errors */ }
|
|
351
639
|
}
|
|
352
640
|
}
|
|
353
|
-
} catch (
|
|
354
|
-
// Ignorar erros de permissΓ£o
|
|
355
|
-
}
|
|
641
|
+
} catch (e) { /* ignore permission errors */ }
|
|
356
642
|
}
|
|
357
643
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// Ordenar por data de modificaΓ§Γ£o (mais recente primeiro)
|
|
361
|
-
allFiles.sort((a, b) => b.modified - a.modified);
|
|
362
|
-
|
|
363
|
-
// Retornar apenas os N mais recentes
|
|
364
|
-
const recentFiles = allFiles.slice(0, limit);
|
|
644
|
+
searchContent(workspacePath);
|
|
365
645
|
|
|
366
|
-
console.log(`β
[
|
|
367
|
-
|
|
368
|
-
return res.json({
|
|
369
|
-
workspacePath,
|
|
370
|
-
total: allFiles.length,
|
|
371
|
-
files: recentFiles
|
|
372
|
-
});
|
|
646
|
+
console.log(`β
[search] Found ${results.length} matches`);
|
|
647
|
+
return res.json({ query, results, total: results.length });
|
|
373
648
|
|
|
374
649
|
} catch (err) {
|
|
375
|
-
console.error("β Error
|
|
650
|
+
console.error("β Error searching:", err);
|
|
376
651
|
return res.status(500).json({ error: err.message });
|
|
377
652
|
}
|
|
378
653
|
});
|
|
379
654
|
|
|
380
655
|
// --------------------------------------------------
|
|
381
656
|
// π§© 9οΈβ£ GET /workspace/scan
|
|
382
|
-
//
|
|
383
|
-
// Usado pelo SmartFileDetectionService para anΓ‘lise com AI
|
|
657
|
+
// Lista TODOS os arquivos do workspace (para list_files tool)
|
|
384
658
|
// --------------------------------------------------
|
|
385
659
|
router.get("/scan", async (req, res) => {
|
|
386
660
|
try {
|
|
387
661
|
if (!workspacePath || !fs.existsSync(workspacePath)) {
|
|
388
|
-
return res.status(400).json({ error: "Workspace not configured
|
|
662
|
+
return res.status(400).json({ error: "Workspace not configured" });
|
|
389
663
|
}
|
|
390
664
|
|
|
391
|
-
|
|
665
|
+
const { directory, pattern, maxDepth = 5 } = req.query;
|
|
666
|
+
const scanRoot = directory ? path.join(workspacePath, directory) : workspacePath;
|
|
392
667
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
const extensionMap = {
|
|
399
|
-
'.java': 'Java',
|
|
400
|
-
'.js': 'JavaScript',
|
|
401
|
-
'.ts': 'TypeScript',
|
|
402
|
-
'.py': 'Python',
|
|
403
|
-
'.go': 'Go',
|
|
404
|
-
'.rs': 'Rust',
|
|
405
|
-
'.kt': 'Kotlin',
|
|
406
|
-
'.scala': 'Scala',
|
|
407
|
-
'.rb': 'Ruby',
|
|
408
|
-
'.php': 'PHP',
|
|
409
|
-
'.cs': 'C#',
|
|
410
|
-
'.cpp': 'C++',
|
|
411
|
-
'.c': 'C'
|
|
412
|
-
};
|
|
668
|
+
if (!fs.existsSync(scanRoot)) {
|
|
669
|
+
return res.status(404).json({ error: `Directory not found: ${directory || '/'}` });
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
console.log(`π [scan] Scanning: ${scanRoot} (maxDepth: ${maxDepth})`);
|
|
413
673
|
|
|
414
|
-
|
|
415
|
-
|
|
674
|
+
const files = [];
|
|
675
|
+
const dirs = [];
|
|
416
676
|
|
|
677
|
+
function scan(dir, depth = 0) {
|
|
678
|
+
if (depth > parseInt(maxDepth)) return;
|
|
417
679
|
try {
|
|
418
680
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
419
|
-
|
|
420
681
|
for (const entry of entries) {
|
|
421
|
-
// Ignorar diretΓ³rios comuns
|
|
422
|
-
if (entry.name.startsWith('.') ||
|
|
423
|
-
entry.name === 'node_modules' ||
|
|
424
|
-
entry.name === 'target' ||
|
|
425
|
-
entry.name === 'build' ||
|
|
426
|
-
entry.name === '.git' ||
|
|
427
|
-
entry.name === '__pycache__') {
|
|
428
|
-
continue;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
682
|
const fullPath = path.join(dir, entry.name);
|
|
432
|
-
|
|
683
|
+
const relativePath = path.relative(workspacePath, fullPath);
|
|
433
684
|
if (entry.isDirectory()) {
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
if (
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
// Limitar arquivos retornados para nΓ£o estourar memΓ³ria
|
|
444
|
-
if (files.length < 200) {
|
|
445
|
-
try {
|
|
446
|
-
const stats = fs.statSync(fullPath);
|
|
447
|
-
files.push({
|
|
448
|
-
path: path.relative(workspacePath, fullPath),
|
|
449
|
-
name: entry.name,
|
|
450
|
-
language: extensionMap[ext],
|
|
451
|
-
size: stats.size,
|
|
452
|
-
modified: stats.mtime
|
|
453
|
-
});
|
|
454
|
-
} catch (e) {
|
|
455
|
-
// Ignorar
|
|
456
|
-
}
|
|
685
|
+
if (!["node_modules", ".git", "target", "build", ".idea", ".gradle", "__pycache__", ".next"].includes(entry.name)) {
|
|
686
|
+
dirs.push(relativePath);
|
|
687
|
+
scan(fullPath, depth + 1);
|
|
688
|
+
}
|
|
689
|
+
} else {
|
|
690
|
+
if (pattern) {
|
|
691
|
+
if (entry.name.match(new RegExp(pattern, 'i'))) {
|
|
692
|
+
files.push(relativePath);
|
|
457
693
|
}
|
|
694
|
+
} else {
|
|
695
|
+
files.push(relativePath);
|
|
458
696
|
}
|
|
459
697
|
}
|
|
460
698
|
}
|
|
461
|
-
} catch (
|
|
462
|
-
// Ignorar erros de permissΓ£o
|
|
463
|
-
}
|
|
699
|
+
} catch (e) { /* ignore */ }
|
|
464
700
|
}
|
|
465
701
|
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
console.log(`β
[scan] Found ${totalFiles} files in ${languages.size} languages`);
|
|
702
|
+
scan(scanRoot);
|
|
469
703
|
|
|
470
704
|
return res.json({
|
|
705
|
+
root: directory || '/',
|
|
471
706
|
workspacePath,
|
|
472
|
-
files,
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
scannedAt: new Date().toISOString()
|
|
477
|
-
}
|
|
707
|
+
files: files.slice(0, 500),
|
|
708
|
+
directories: dirs,
|
|
709
|
+
totalFiles: files.length,
|
|
710
|
+
truncated: files.length > 500
|
|
478
711
|
});
|
|
479
712
|
|
|
480
713
|
} catch (err) {
|
|
481
|
-
console.error("β Error scanning
|
|
714
|
+
console.error("β Error scanning:", err);
|
|
482
715
|
return res.status(500).json({ error: err.message });
|
|
483
716
|
}
|
|
484
717
|
});
|
|
@@ -508,7 +741,9 @@ router.get("/file-content", async (req, res) => {
|
|
|
508
741
|
if (!fs.existsSync(fullPath)) {
|
|
509
742
|
return res.status(404).json({
|
|
510
743
|
error: "File not found",
|
|
511
|
-
path: requestedPath
|
|
744
|
+
path: requestedPath,
|
|
745
|
+
resolvedPath: fullPath,
|
|
746
|
+
workspacePath,
|
|
512
747
|
});
|
|
513
748
|
}
|
|
514
749
|
|
package/src/server.js
CHANGED
|
@@ -717,8 +717,92 @@ app.get("/health", (_req, res) => {
|
|
|
717
717
|
|
|
718
718
|
/** Define/abre o workspace local */
|
|
719
719
|
app.post("/workspace/open", async (req, res) => {
|
|
720
|
-
const { root, workspaceId } = req.body || {};
|
|
721
|
-
|
|
720
|
+
const { root, workspaceId, repoUrl, token, branch = "main" } = req.body || {};
|
|
721
|
+
|
|
722
|
+
// ==========================================
|
|
723
|
+
// CLOUD MODE: Git clone (when Gateway sends repoUrl)
|
|
724
|
+
// ==========================================
|
|
725
|
+
if (repoUrl) {
|
|
726
|
+
console.log(`π [/workspace/open] CLOUD MODE β cloning ${repoUrl} (branch: ${branch})`);
|
|
727
|
+
|
|
728
|
+
try {
|
|
729
|
+
// Extract repo name: https://github.com/org/repo β org_repo
|
|
730
|
+
let repoName = repoUrl.split('/').pop().replace('.git', '');
|
|
731
|
+
const urlParts = repoUrl.replace(/\.git$/, '').split('/');
|
|
732
|
+
if (urlParts.length >= 2) {
|
|
733
|
+
const org = urlParts[urlParts.length - 2];
|
|
734
|
+
repoName = `${org}_${repoName}`;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
const cacheDir = process.env.WORKSPACE_CACHE || '/tmp/workspaces';
|
|
738
|
+
const clonePath = path.join(cacheDir, repoName);
|
|
739
|
+
|
|
740
|
+
// Ensure cache directory exists
|
|
741
|
+
await fsPromises.mkdir(cacheDir, { recursive: true });
|
|
742
|
+
|
|
743
|
+
// Build authenticated URL
|
|
744
|
+
const authUrl = token
|
|
745
|
+
? repoUrl.replace('https://', `https://x-access-token:${token}@`)
|
|
746
|
+
: repoUrl;
|
|
747
|
+
|
|
748
|
+
const gitDir = path.join(clonePath, '.git');
|
|
749
|
+
const alreadyCloned = await exists(gitDir);
|
|
750
|
+
|
|
751
|
+
if (alreadyCloned) {
|
|
752
|
+
console.log(`π [cloud] Repo exists: ${clonePath}, updating...`);
|
|
753
|
+
// Update remote URL with fresh token
|
|
754
|
+
await execAsync(`git remote set-url origin "${authUrl}"`, { cwd: clonePath }).catch(() => {});
|
|
755
|
+
await execAsync(`git fetch origin`, { cwd: clonePath, timeout: 120000 });
|
|
756
|
+
// Checkout correct branch and reset to remote (discard previous patches)
|
|
757
|
+
await execAsync(`git checkout ${branch} 2>/dev/null || git checkout -b ${branch} origin/${branch}`, { cwd: clonePath }).catch(() => {});
|
|
758
|
+
await execAsync(`git reset --hard origin/${branch}`, { cwd: clonePath });
|
|
759
|
+
console.log(`β
[cloud] Updated to latest ${branch}`);
|
|
760
|
+
} else {
|
|
761
|
+
console.log(`π½ [cloud] Cloning ${repoUrl} (branch: ${branch})...`);
|
|
762
|
+
await execAsync(
|
|
763
|
+
`git clone --branch ${branch} --single-branch --depth 50 "${authUrl}" "${clonePath}"`,
|
|
764
|
+
{ timeout: 300000 }
|
|
765
|
+
);
|
|
766
|
+
// Configure git user for future commits
|
|
767
|
+
await execAsync(`git config user.email "deepdebug-ai@deepdebug.ai"`, { cwd: clonePath });
|
|
768
|
+
await execAsync(`git config user.name "DeepDebug AI"`, { cwd: clonePath });
|
|
769
|
+
console.log(`β
[cloud] Cloned successfully: ${clonePath}`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Set as active workspace
|
|
773
|
+
WORKSPACE_ROOT = clonePath;
|
|
774
|
+
const wsId = workspaceId || "default";
|
|
775
|
+
try { await wsManager.open(wsId, clonePath); } catch (err) {
|
|
776
|
+
console.warn(`β οΈ WorkspaceManager.open failed (non-fatal): ${err.message}`);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
const meta = await detectProject(clonePath);
|
|
780
|
+
const port = await detectPort(clonePath);
|
|
781
|
+
|
|
782
|
+
return res.json({
|
|
783
|
+
ok: true,
|
|
784
|
+
root: WORKSPACE_ROOT,
|
|
785
|
+
workspaceId: wsId,
|
|
786
|
+
mode: "cloud-clone",
|
|
787
|
+
meta,
|
|
788
|
+
port
|
|
789
|
+
});
|
|
790
|
+
|
|
791
|
+
} catch (gitErr) {
|
|
792
|
+
console.error(`β [cloud] Git clone failed:`, gitErr.message);
|
|
793
|
+
const hint = gitErr.message.includes('Authentication') || gitErr.message.includes('could not read')
|
|
794
|
+
? "Authentication failed. Check token and repo URL."
|
|
795
|
+
: gitErr.message.includes('not found') || gitErr.message.includes('does not exist')
|
|
796
|
+
? "Repository not found. Check the URL."
|
|
797
|
+
: "Clone failed. Ensure the URL is accessible.";
|
|
798
|
+
return res.status(500).json({ ok: false, error: gitErr.message, hint });
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// ==========================================
|
|
803
|
+
// LOCAL MODE: Path-based (existing behavior)
|
|
804
|
+
// ==========================================
|
|
805
|
+
if (!root) return res.status(400).json({ error: "root or repoUrl is required" });
|
|
722
806
|
const abs = path.resolve(root);
|
|
723
807
|
if (!(await exists(abs))) return res.status(404).json({ error: "path not found" });
|
|
724
808
|
|
|
@@ -734,7 +818,7 @@ app.post("/workspace/open", async (req, res) => {
|
|
|
734
818
|
|
|
735
819
|
const meta = await detectProject(WORKSPACE_ROOT);
|
|
736
820
|
const port = await detectPort(WORKSPACE_ROOT);
|
|
737
|
-
res.json({ ok: true, root: WORKSPACE_ROOT, workspaceId: wsId, meta, port });
|
|
821
|
+
res.json({ ok: true, root: WORKSPACE_ROOT, workspaceId: wsId, mode: "local", meta, port });
|
|
738
822
|
});
|
|
739
823
|
|
|
740
824
|
/**
|