deepdebug-local-agent 1.0.20 → 1.0.21
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/CLAUDE_CODE_AGENT_INFINITE.md +623 -0
- package/Dockerfile +2 -0
- package/Dockerfile.full +63 -0
- package/local-agent-source.tar.gz +0 -0
- package/package.json +4 -3
- package/src/detectors/language-detector.js +29 -12
- package/src/detectors.js +291 -8
- package/src/exec-utils.js +19 -16
- package/src/local-agent-source.tar.gz +0 -0
- package/src/runtimes/java/maven-runtime.js +4 -3
- package/src/server.js +456 -0
- package/src/utils/exec-utils.js +8 -4
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
# CLAUDE CODE — AGENT INFINITE LOOP
|
|
2
|
+
# Terminal 1: cd ~/Documents/Projects/deepdebug-local-agent
|
|
3
|
+
# claude --dangerously-skip-permissions "$(cat CLAUDE_CODE_AGENT_INFINITE.md)"
|
|
4
|
+
|
|
5
|
+
## LEI FUNDAMENTAL
|
|
6
|
+
NUNCA terminar com erro. NUNCA passar ao próximo item sem o actual estar verde.
|
|
7
|
+
O único critério de paragem: TODOS OS ITEMS VERDES.
|
|
8
|
+
|
|
9
|
+
## SETUP
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
AGENT="https://deepdebug-local-agent-qa-575662086506.us-central1.run.app"
|
|
13
|
+
GATEWAY="https://deepdebug-gateway-qa-575662086506.us-central1.run.app"
|
|
14
|
+
GCP_CLIENT="gen-lang-client-0961289539"
|
|
15
|
+
SA_EMAIL="deepdebug-agent@${GCP_CLIENT}.iam.gserviceaccount.com"
|
|
16
|
+
PG="PGPASSWORD='DeepDebugQA@2026' psql -h 35.224.155.29 -U deepdebug -d deepdebug"
|
|
17
|
+
WS_ID="b040f1ae-5f3c-4612-9ce1-0b18a5aea303"
|
|
18
|
+
|
|
19
|
+
log() { echo "[$(date '+%H:%M:%S')] $1"; }
|
|
20
|
+
ok() { echo "[OK] $1"; }
|
|
21
|
+
fail() { echo "[FAIL] $1"; }
|
|
22
|
+
|
|
23
|
+
deploy_agent() {
|
|
24
|
+
cd ~/Documents/Projects/deepdebug-local-agent
|
|
25
|
+
node --check src/server.js 2>&1 | head -5
|
|
26
|
+
if [ $? -ne 0 ]; then fail "Syntax error"; return 1; fi
|
|
27
|
+
git add -A && git commit -m "auto-heal: $1" --allow-empty -q
|
|
28
|
+
gcloud builds submit --config cloudbuild-agent-qa.yaml --project insptechai 2>&1 | tail -3
|
|
29
|
+
log "Waiting 5 min..."; sleep 300
|
|
30
|
+
ok "Agent deployed"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
read_logs() {
|
|
34
|
+
gcloud logging read "resource.type=cloud_run_revision AND resource.labels.service_name=deepdebug-local-agent-qa" \
|
|
35
|
+
--project=insptechai --freshness=5m --format="value(textPayload)" 2>/dev/null \
|
|
36
|
+
| grep -v "^$" | head -20
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
---
|
|
41
|
+
|
|
42
|
+
## ITEM A1 — /workspace/setup ENDPOINT
|
|
43
|
+
|
|
44
|
+
### Critério: endpoint existe, clone funciona, detect stack funciona, SSE retorna event:complete
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
check_setup() {
|
|
48
|
+
local R=$(curl -s -X POST "$AGENT/workspace/setup" \
|
|
49
|
+
-H "Content-Type: application/json" \
|
|
50
|
+
-d '{"workspaceId":"test-check-001","repoUrl":"https://github.com/williambella/simple-api.git","gitToken":"","provider":"deepdebug_cloud"}' \
|
|
51
|
+
--max-time 30 2>/dev/null)
|
|
52
|
+
echo "$R" | grep -q "progress\|complete\|Starting setup"
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
fix_setup() {
|
|
56
|
+
cd ~/Documents/Projects/deepdebug-local-agent
|
|
57
|
+
# Verificar se endpoint existe
|
|
58
|
+
grep -n "workspace/setup\|cloneRepository\|detectProjectStack\|generateDockerfile" src/server.js | head -10
|
|
59
|
+
|
|
60
|
+
# Se não existir, o endpoint foi adicionado em sessões anteriores mas pode ter sido sobrescrito
|
|
61
|
+
# Ver o fim do ficheiro
|
|
62
|
+
tail -100 src/server.js
|
|
63
|
+
|
|
64
|
+
# Verificar se as funções auxiliares existem
|
|
65
|
+
grep -n "async function cloneRepository\|async function detectProjectStack\|async function generateDockerfile" src/server.js
|
|
66
|
+
|
|
67
|
+
# Se não existirem, adicionar o endpoint completo com todas as funções
|
|
68
|
+
# baseado no que foi implementado nas sessões anteriores
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### Implementação completa se não existir:
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
add_setup_endpoint() {
|
|
76
|
+
cd ~/Documents/Projects/deepdebug-local-agent
|
|
77
|
+
|
|
78
|
+
# Verificar se já existe
|
|
79
|
+
if grep -q "workspace/setup" src/server.js; then
|
|
80
|
+
log "/workspace/setup already exists"
|
|
81
|
+
return 0
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
log "Adding /workspace/setup endpoint..."
|
|
85
|
+
|
|
86
|
+
# Adicionar antes do app.listen (última linha do ficheiro)
|
|
87
|
+
LISTEN_LINE=$(grep -n "app.listen\|server.listen" src/server.js | tail -1 | cut -d: -f1)
|
|
88
|
+
log "app.listen at line: $LISTEN_LINE"
|
|
89
|
+
|
|
90
|
+
# Criar ficheiro com o código a adicionar
|
|
91
|
+
cat > /tmp/setup_endpoint.js << 'JSEOF'
|
|
92
|
+
|
|
93
|
+
// ============================================================
|
|
94
|
+
// WORKSPACE BASE
|
|
95
|
+
// ============================================================
|
|
96
|
+
const WORKSPACE_BASE = process.env.WORKSPACE_BASE || process.env.WORKSPACES_MOUNT || '/mnt/workspaces';
|
|
97
|
+
|
|
98
|
+
// ============================================================
|
|
99
|
+
// POST /workspace/setup — AI Cloud Engineering Agent
|
|
100
|
+
// ============================================================
|
|
101
|
+
app.post('/workspace/setup', async (req, res) => {
|
|
102
|
+
const { workspaceId, repoUrl, gitToken, provider = 'deepdebug_cloud',
|
|
103
|
+
cloudProjectId, cloudToken, serviceAccountJson } = req.body || {};
|
|
104
|
+
|
|
105
|
+
if (!workspaceId || !repoUrl) {
|
|
106
|
+
return res.status(400).json({ error: 'workspaceId and repoUrl required' });
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
110
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
111
|
+
res.setHeader('Connection', 'keep-alive');
|
|
112
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
113
|
+
|
|
114
|
+
const emit = (event, msg) => {
|
|
115
|
+
const data = typeof msg === 'object' ? JSON.stringify(msg) : JSON.stringify({ message: msg });
|
|
116
|
+
res.write(`event:${event}\ndata:${data}\n\n`);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
const wsPath = path.join(WORKSPACE_BASE, workspaceId);
|
|
121
|
+
emit('progress', `Starting setup for workspace: ${workspaceId}`);
|
|
122
|
+
emit('progress', `Workspace path: ${wsPath}`);
|
|
123
|
+
|
|
124
|
+
// STEP 1: Clone
|
|
125
|
+
emit('progress', 'Cloning repository...');
|
|
126
|
+
const cloneResult = await cloneRepo(wsPath, repoUrl, gitToken);
|
|
127
|
+
if (!cloneResult.ok) {
|
|
128
|
+
emit('error', { error: `Clone failed: ${cloneResult.error}` });
|
|
129
|
+
return res.end();
|
|
130
|
+
}
|
|
131
|
+
emit('progress', `Repository cloned on branch: ${cloneResult.branch}`);
|
|
132
|
+
|
|
133
|
+
// STEP 2: Detect stack
|
|
134
|
+
emit('progress', 'AI analysing project structure...');
|
|
135
|
+
const stack = await detectStack(wsPath);
|
|
136
|
+
emit('progress', `Stack: ${stack.language} / ${stack.buildTool} / ${stack.framework}`);
|
|
137
|
+
|
|
138
|
+
// STEP 3: Generate Dockerfile
|
|
139
|
+
const dfResult = await ensureDockerfile(wsPath, stack);
|
|
140
|
+
emit('progress', dfResult.existed ? 'Dockerfile already exists' : `Dockerfile generated for ${stack.language}`);
|
|
141
|
+
|
|
142
|
+
// STEP 4: Register workspace
|
|
143
|
+
try {
|
|
144
|
+
await wsManager.open(workspaceId, wsPath);
|
|
145
|
+
} catch (e) { /* ignore */ }
|
|
146
|
+
|
|
147
|
+
emit('progress', `Provider: ${provider}. Workspace ready.`);
|
|
148
|
+
emit('progress', 'AI Cloud Engineering setup complete!');
|
|
149
|
+
emit('complete', {
|
|
150
|
+
success: true, workspaceId, wsPath, provider,
|
|
151
|
+
branch: cloneResult.branch,
|
|
152
|
+
stack: { language: stack.language, buildTool: stack.buildTool }
|
|
153
|
+
});
|
|
154
|
+
res.end();
|
|
155
|
+
} catch (err) {
|
|
156
|
+
console.error('[Setup] Error:', err.message);
|
|
157
|
+
emit('error', { error: err.message });
|
|
158
|
+
res.end();
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// ============================================================
|
|
163
|
+
// POST /workspace/build-push — Build + push via Cloud Build
|
|
164
|
+
// ============================================================
|
|
165
|
+
app.post('/workspace/build-push', async (req, res) => {
|
|
166
|
+
const { workspaceId, imageName, gcpProject, saKeyJson } = req.body || {};
|
|
167
|
+
if (!workspaceId || !imageName || !gcpProject || !saKeyJson) {
|
|
168
|
+
return res.status(400).json({ error: 'workspaceId, imageName, gcpProject, saKeyJson required' });
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
res.setHeader('Content-Type', 'text/event-stream');
|
|
172
|
+
res.setHeader('Cache-Control', 'no-cache');
|
|
173
|
+
res.setHeader('Connection', 'keep-alive');
|
|
174
|
+
res.setHeader('X-Accel-Buffering', 'no');
|
|
175
|
+
|
|
176
|
+
const emit = (event, msg) => {
|
|
177
|
+
const data = typeof msg === 'object' ? JSON.stringify(msg) : JSON.stringify({ message: msg });
|
|
178
|
+
res.write(`event:${event}\ndata:${data}\n\n`);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
try {
|
|
182
|
+
const wsPath = path.join(WORKSPACE_BASE, workspaceId);
|
|
183
|
+
if (!fs.existsSync(wsPath)) {
|
|
184
|
+
emit('error', { error: `Workspace not found: ${wsPath}` }); return res.end();
|
|
185
|
+
}
|
|
186
|
+
if (!fs.existsSync(path.join(wsPath, 'Dockerfile'))) {
|
|
187
|
+
emit('error', { error: 'No Dockerfile in workspace' }); return res.end();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Step 1: Get SA access token
|
|
191
|
+
emit('progress', 'Authenticating with GCP...');
|
|
192
|
+
let accessToken;
|
|
193
|
+
try {
|
|
194
|
+
const { createSign } = await import('crypto');
|
|
195
|
+
const sa = JSON.parse(saKeyJson);
|
|
196
|
+
const now = Math.floor(Date.now() / 1000);
|
|
197
|
+
const h = Buffer.from(JSON.stringify({ alg: 'RS256', typ: 'JWT' })).toString('base64url');
|
|
198
|
+
const c = Buffer.from(JSON.stringify({
|
|
199
|
+
iss: sa.client_email,
|
|
200
|
+
scope: 'https://www.googleapis.com/auth/cloud-platform',
|
|
201
|
+
aud: 'https://oauth2.googleapis.com/token',
|
|
202
|
+
iat: now, exp: now + 3600
|
|
203
|
+
})).toString('base64url');
|
|
204
|
+
const sign = createSign('SHA256');
|
|
205
|
+
sign.update(`${h}.${c}`);
|
|
206
|
+
const jwt = `${h}.${c}.${sign.sign(sa.private_key, 'base64url')}`;
|
|
207
|
+
const tokenResp = await fetch('https://oauth2.googleapis.com/token', {
|
|
208
|
+
method: 'POST',
|
|
209
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
210
|
+
body: `grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion=${jwt}`
|
|
211
|
+
});
|
|
212
|
+
const tokenData = await tokenResp.json();
|
|
213
|
+
accessToken = tokenData.access_token;
|
|
214
|
+
if (!accessToken) throw new Error('No access_token: ' + JSON.stringify(tokenData));
|
|
215
|
+
emit('progress', 'GCP authentication successful');
|
|
216
|
+
} catch (e) {
|
|
217
|
+
emit('error', { error: 'GCP auth failed: ' + e.message }); return res.end();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Step 2: Compress workspace
|
|
221
|
+
emit('progress', 'Compressing source code...');
|
|
222
|
+
const { execSync } = await import('child_process');
|
|
223
|
+
const tarPath = `/tmp/source-${workspaceId}-${Date.now()}.tar.gz`;
|
|
224
|
+
try {
|
|
225
|
+
execSync(`tar -czf "${tarPath}" --exclude=".git" --exclude="node_modules" --exclude="target" --exclude="*.class" --exclude=".gradle" -C "${wsPath}" . 2>&1`, { stdio: 'pipe', timeout: 120000 });
|
|
226
|
+
const sizeKB = Math.round(fs.statSync(tarPath).size / 1024);
|
|
227
|
+
emit('progress', `Source compressed: ${sizeKB}KB`);
|
|
228
|
+
} catch (e) {
|
|
229
|
+
emit('error', { error: 'Compression failed: ' + e.message.substring(0, 200) }); return res.end();
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Step 3: Upload to GCS
|
|
233
|
+
const bucketName = `${gcpProject}_cloudbuild`;
|
|
234
|
+
const objectName = `source/${workspaceId}-${Date.now()}.tar.gz`;
|
|
235
|
+
emit('progress', `Uploading source to GCS bucket: ${bucketName}...`);
|
|
236
|
+
try {
|
|
237
|
+
await fetch(`https://storage.googleapis.com/storage/v1/b?project=${gcpProject}`, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
|
240
|
+
body: JSON.stringify({ name: bucketName, location: 'US' })
|
|
241
|
+
});
|
|
242
|
+
const fileBuffer = fs.readFileSync(tarPath);
|
|
243
|
+
const uploadResp = await fetch(
|
|
244
|
+
`https://storage.googleapis.com/upload/storage/v1/b/${encodeURIComponent(bucketName)}/o?uploadType=media&name=${encodeURIComponent(objectName)}`,
|
|
245
|
+
{ method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/gzip' }, body: fileBuffer }
|
|
246
|
+
);
|
|
247
|
+
if (!uploadResp.ok) {
|
|
248
|
+
const errText = await uploadResp.text();
|
|
249
|
+
throw new Error(`GCS upload ${uploadResp.status}: ${errText.substring(0, 200)}`);
|
|
250
|
+
}
|
|
251
|
+
emit('progress', 'Source uploaded to GCS');
|
|
252
|
+
} catch (e) {
|
|
253
|
+
emit('error', { error: 'GCS upload failed: ' + e.message }); return res.end();
|
|
254
|
+
} finally {
|
|
255
|
+
try { fs.unlinkSync(tarPath); } catch {}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Step 4: Submit Cloud Build
|
|
259
|
+
emit('progress', 'Submitting Cloud Build...');
|
|
260
|
+
let buildId = 'unknown';
|
|
261
|
+
try {
|
|
262
|
+
const buildReq = {
|
|
263
|
+
source: { storageSource: { bucket: bucketName, object: objectName } },
|
|
264
|
+
steps: [
|
|
265
|
+
{ name: 'gcr.io/cloud-builders/docker', args: ['build', '-t', imageName, '.'], id: 'build' },
|
|
266
|
+
{ name: 'gcr.io/cloud-builders/docker', args: ['push', imageName], id: 'push', waitFor: ['build'] }
|
|
267
|
+
],
|
|
268
|
+
images: [imageName], timeout: '1200s'
|
|
269
|
+
};
|
|
270
|
+
const buildResp = await fetch(`https://cloudbuild.googleapis.com/v1/projects/${gcpProject}/builds`, {
|
|
271
|
+
method: 'POST',
|
|
272
|
+
headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' },
|
|
273
|
+
body: JSON.stringify(buildReq)
|
|
274
|
+
});
|
|
275
|
+
if (!buildResp.ok) {
|
|
276
|
+
const errText = await buildResp.text();
|
|
277
|
+
throw new Error(`Cloud Build ${buildResp.status}: ${errText.substring(0, 300)}`);
|
|
278
|
+
}
|
|
279
|
+
const buildData = await buildResp.json();
|
|
280
|
+
buildId = buildData.metadata?.build?.id || buildData.name?.split('/').pop() || 'unknown';
|
|
281
|
+
emit('progress', `Cloud Build started: ${buildId}`);
|
|
282
|
+
} catch (e) {
|
|
283
|
+
emit('error', { error: 'Cloud Build submit failed: ' + e.message }); return res.end();
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Step 5: Poll status
|
|
287
|
+
emit('progress', 'Building Docker image (3-5 minutes)...');
|
|
288
|
+
let status = 'WORKING';
|
|
289
|
+
let attempts = 0;
|
|
290
|
+
while (!['SUCCESS', 'FAILURE', 'CANCELLED', 'TIMEOUT'].includes(status) && attempts < 40) {
|
|
291
|
+
await new Promise(r => setTimeout(r, 15000));
|
|
292
|
+
attempts++;
|
|
293
|
+
try {
|
|
294
|
+
const sResp = await fetch(`https://cloudbuild.googleapis.com/v1/projects/${gcpProject}/builds/${buildId}`, { headers: { Authorization: `Bearer ${accessToken}` } });
|
|
295
|
+
const sData = await sResp.json();
|
|
296
|
+
status = sData.status || 'WORKING';
|
|
297
|
+
emit('progress', `Build status: ${status} (${attempts}/40)`);
|
|
298
|
+
} catch (e) { emit('progress', `Status check: ${e.message}`); }
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (status === 'SUCCESS') {
|
|
302
|
+
emit('progress', 'Docker image built and pushed successfully!');
|
|
303
|
+
emit('complete', { success: true, imageName, buildId });
|
|
304
|
+
} else {
|
|
305
|
+
emit('error', { error: `Cloud Build ${status}. Check GCP Console.` });
|
|
306
|
+
}
|
|
307
|
+
res.end();
|
|
308
|
+
} catch (err) {
|
|
309
|
+
console.error('[build-push] Fatal:', err.message);
|
|
310
|
+
emit('error', { error: err.message });
|
|
311
|
+
res.end();
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
// ============================================================
|
|
316
|
+
// POST /workspace/git/prepare — Clone/update repo
|
|
317
|
+
// ============================================================
|
|
318
|
+
app.post('/workspace/git/prepare', async (req, res) => {
|
|
319
|
+
const { workspaceId, repoUrl, gitToken, branch } = req.body || {};
|
|
320
|
+
if (!workspaceId || !repoUrl) return res.status(400).json({ error: 'workspaceId and repoUrl required' });
|
|
321
|
+
const wsPath = path.join(WORKSPACE_BASE, workspaceId);
|
|
322
|
+
try {
|
|
323
|
+
const result = await cloneRepo(wsPath, repoUrl, gitToken, branch);
|
|
324
|
+
res.json({ ok: true, path: wsPath, ...result });
|
|
325
|
+
} catch (e) {
|
|
326
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
// ============================================================
|
|
331
|
+
// POST /workspace/write-file — Write file to workspace
|
|
332
|
+
// ============================================================
|
|
333
|
+
app.post('/workspace/write-file', async (req, res) => {
|
|
334
|
+
const { workspaceId, path: filePath, content, overwrite = true } = req.body || {};
|
|
335
|
+
if (!filePath || content === undefined) return res.status(400).json({ error: 'path and content required' });
|
|
336
|
+
try {
|
|
337
|
+
const base = workspaceId ? path.join(WORKSPACE_BASE, workspaceId) : WORKSPACE_ROOT;
|
|
338
|
+
const abs = path.join(base, filePath);
|
|
339
|
+
if (!overwrite && fs.existsSync(abs)) return res.json({ ok: true, path: abs, skipped: true });
|
|
340
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
341
|
+
fs.writeFileSync(abs, content, 'utf8');
|
|
342
|
+
res.json({ ok: true, path: abs });
|
|
343
|
+
} catch (e) {
|
|
344
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ============================================================
|
|
349
|
+
// HELPER FUNCTIONS
|
|
350
|
+
// ============================================================
|
|
351
|
+
|
|
352
|
+
async function cloneRepo(wsPath, repoUrl, gitToken, branch = null) {
|
|
353
|
+
const { execSync } = await import('child_process');
|
|
354
|
+
let authUrl = repoUrl;
|
|
355
|
+
if (gitToken && gitToken.length > 5) {
|
|
356
|
+
try {
|
|
357
|
+
const urlObj = new URL(repoUrl);
|
|
358
|
+
urlObj.username = 'x-access-token';
|
|
359
|
+
urlObj.password = gitToken;
|
|
360
|
+
authUrl = urlObj.toString();
|
|
361
|
+
console.log(`[Clone] Using authenticated clone for ${urlObj.hostname}`);
|
|
362
|
+
} catch {}
|
|
363
|
+
}
|
|
364
|
+
const env = { ...process.env, GIT_TERMINAL_PROMPT: '0', GIT_ASKPASS: 'echo', HOME: '/tmp' };
|
|
365
|
+
const opts = { stdio: 'pipe', timeout: 300000, env, shell: true };
|
|
366
|
+
|
|
367
|
+
if (fs.existsSync(path.join(wsPath, '.git'))) {
|
|
368
|
+
console.log(`[Clone] Workspace exists, updating...`);
|
|
369
|
+
try {
|
|
370
|
+
execSync('git fetch origin 2>&1', { ...opts, cwd: wsPath });
|
|
371
|
+
const branch = execSync('git branch --show-current 2>&1', { ...opts, cwd: wsPath }).toString().trim();
|
|
372
|
+
execSync(`git reset --hard origin/${branch} 2>&1`, { ...opts, cwd: wsPath });
|
|
373
|
+
return { ok: true, branch, updated: true };
|
|
374
|
+
} catch (e) {
|
|
375
|
+
console.warn('[Clone] Update failed, re-cloning');
|
|
376
|
+
fs.rmSync(wsPath, { recursive: true, force: true });
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
fs.mkdirSync(wsPath, { recursive: true });
|
|
381
|
+
const branches = branch ? [branch] : ['main', 'master', 'develop'];
|
|
382
|
+
for (const b of branches) {
|
|
383
|
+
try {
|
|
384
|
+
console.log(`[Clone] Trying branch: ${b}...`);
|
|
385
|
+
execSync(`git clone --depth=1 --branch "${b}" "${authUrl}" "${wsPath}" 2>&1`, opts);
|
|
386
|
+
console.log(`[Clone] Cloned on branch: ${b}`);
|
|
387
|
+
return { ok: true, branch: b, cloned: true };
|
|
388
|
+
} catch (e) {
|
|
389
|
+
const err = (e.stdout || e.stderr || e.message || '').toString();
|
|
390
|
+
if (err.includes('404') || err.includes('not found') || err.includes('Repository not found')) break;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
execSync(`git clone --depth=1 "${authUrl}" "${wsPath}" 2>&1`, opts);
|
|
395
|
+
const b = execSync('git branch --show-current 2>&1', { ...opts, cwd: wsPath }).toString().trim();
|
|
396
|
+
return { ok: true, branch: b, cloned: true };
|
|
397
|
+
} catch (e) {
|
|
398
|
+
return { ok: false, error: e.message.substring(0, 200) };
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function detectStack(wsPath) {
|
|
403
|
+
const check = f => fs.existsSync(path.join(wsPath, f));
|
|
404
|
+
const read = f => { try { return fs.readFileSync(path.join(wsPath, f), 'utf8'); } catch { return ''; } };
|
|
405
|
+
let language = 'unknown', buildTool = 'unknown', framework = 'unknown', port = 8080;
|
|
406
|
+
if (check('pom.xml')) {
|
|
407
|
+
language = 'java'; buildTool = 'maven';
|
|
408
|
+
const pom = read('pom.xml');
|
|
409
|
+
if (pom.includes('spring-boot')) framework = 'spring-boot';
|
|
410
|
+
else if (pom.includes('quarkus')) framework = 'quarkus';
|
|
411
|
+
} else if (check('build.gradle') || check('build.gradle.kts')) {
|
|
412
|
+
language = 'java'; buildTool = 'gradle';
|
|
413
|
+
} else if (check('package.json')) {
|
|
414
|
+
language = 'javascript'; buildTool = 'npm';
|
|
415
|
+
const pkg = JSON.parse(read('package.json') || '{}');
|
|
416
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
417
|
+
if (deps.next) framework = 'nextjs';
|
|
418
|
+
else if (deps.express) framework = 'express';
|
|
419
|
+
else if (deps.fastify) framework = 'fastify';
|
|
420
|
+
} else if (check('requirements.txt') || check('pyproject.toml')) {
|
|
421
|
+
language = 'python'; buildTool = 'pip';
|
|
422
|
+
const reqs = read('requirements.txt');
|
|
423
|
+
if (reqs.includes('fastapi')) framework = 'fastapi';
|
|
424
|
+
else if (reqs.includes('django')) framework = 'django';
|
|
425
|
+
else if (reqs.includes('flask')) framework = 'flask';
|
|
426
|
+
} else if (check('go.mod')) { language = 'go'; buildTool = 'go'; }
|
|
427
|
+
else if (check('Cargo.toml')) { language = 'rust'; buildTool = 'cargo'; }
|
|
428
|
+
else if (check('composer.json')) { language = 'php'; buildTool = 'composer'; }
|
|
429
|
+
return { language, buildTool, framework, port, hasDockerfile: check('Dockerfile') };
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async function ensureDockerfile(wsPath, stack) {
|
|
433
|
+
const dfPath = path.join(wsPath, 'Dockerfile');
|
|
434
|
+
if (fs.existsSync(dfPath)) return { ok: true, existed: true };
|
|
435
|
+
const { language, buildTool, port = 8080 } = stack;
|
|
436
|
+
let content = '';
|
|
437
|
+
if (language === 'java' && buildTool === 'maven') {
|
|
438
|
+
content = `FROM maven:3.9-eclipse-temurin-17 AS build\nWORKDIR /app\nCOPY pom.xml .\nRUN mvn dependency:go-offline -B\nCOPY src ./src\nRUN mvn clean package -DskipTests -B\n\nFROM eclipse-temurin:17-jre-jammy\nWORKDIR /app\nCOPY --from=build /app/target/*.jar app.jar\nEXPOSE ${port}\nENTRYPOINT ["java", "-jar", "app.jar"]\n`;
|
|
439
|
+
} else if (language === 'java' && buildTool === 'gradle') {
|
|
440
|
+
content = `FROM gradle:8-jdk17 AS build\nWORKDIR /app\nCOPY . .\nRUN gradle build -x test --no-daemon\n\nFROM eclipse-temurin:17-jre-jammy\nWORKDIR /app\nCOPY --from=build /app/build/libs/*.jar app.jar\nEXPOSE ${port}\nENTRYPOINT ["java", "-jar", "app.jar"]\n`;
|
|
441
|
+
} else if (language === 'javascript' || language === 'typescript') {
|
|
442
|
+
content = `FROM node:20-alpine\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci --only=production\nCOPY . .\nEXPOSE ${port}\nENV PORT=${port}\nENV NODE_ENV=production\nCMD ["node", "src/index.js"]\n`;
|
|
443
|
+
} else if (language === 'python') {
|
|
444
|
+
content = `FROM python:3.11-slim\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY . .\nEXPOSE ${port}\nCMD ["python", "app.py"]\n`;
|
|
445
|
+
} else if (language === 'go') {
|
|
446
|
+
content = `FROM golang:1.21-alpine AS build\nWORKDIR /app\nCOPY . .\nRUN go build -o main .\n\nFROM alpine:3.18\nWORKDIR /app\nCOPY --from=build /app/main .\nEXPOSE ${port}\nCMD ["./main"]\n`;
|
|
447
|
+
} else {
|
|
448
|
+
content = `FROM ubuntu:22.04\nWORKDIR /app\nCOPY . .\nEXPOSE ${port}\nCMD ["./start.sh"]\n`;
|
|
449
|
+
}
|
|
450
|
+
fs.writeFileSync(dfPath, content, 'utf8');
|
|
451
|
+
return { ok: true, existed: false };
|
|
452
|
+
}
|
|
453
|
+
JSEOF
|
|
454
|
+
|
|
455
|
+
# Inserir antes do app.listen
|
|
456
|
+
head -n $((LISTEN_LINE - 1)) src/server.js > /tmp/server_new.js
|
|
457
|
+
cat /tmp/setup_endpoint.js >> /tmp/server_new.js
|
|
458
|
+
tail -n +$LISTEN_LINE src/server.js >> /tmp/server_new.js
|
|
459
|
+
mv /tmp/server_new.js src/server.js
|
|
460
|
+
|
|
461
|
+
log "Endpoints added to server.js"
|
|
462
|
+
}
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
---
|
|
466
|
+
|
|
467
|
+
## LOOP PRINCIPAL DO AGENT
|
|
468
|
+
|
|
469
|
+
```bash
|
|
470
|
+
log "======================================"
|
|
471
|
+
log "AGENT INFINITE LOOP — START"
|
|
472
|
+
log "======================================"
|
|
473
|
+
|
|
474
|
+
cd ~/Documents/Projects/deepdebug-local-agent
|
|
475
|
+
|
|
476
|
+
# Verificar e adicionar endpoints se necessário
|
|
477
|
+
if ! grep -q "workspace/setup" src/server.js; then
|
|
478
|
+
log "Endpoints missing — adding..."
|
|
479
|
+
add_setup_endpoint
|
|
480
|
+
deploy_agent "add setup+build-push+clone endpoints"
|
|
481
|
+
fi
|
|
482
|
+
|
|
483
|
+
AGENT_DONE=false
|
|
484
|
+
while [ "$AGENT_DONE" = "false" ]; do
|
|
485
|
+
ALL_GREEN=true
|
|
486
|
+
|
|
487
|
+
# ─── CHECK A1: /workspace/setup ───
|
|
488
|
+
log "CHECK A1: /workspace/setup"
|
|
489
|
+
SETUP_R=$(curl -s -X POST "$AGENT/workspace/setup" \
|
|
490
|
+
-H "Content-Type: application/json" \
|
|
491
|
+
-d '{"workspaceId":"agent-check-001","repoUrl":"https://github.com/williambella/simple-api.git","gitToken":"","provider":"deepdebug_cloud"}' \
|
|
492
|
+
--max-time 30 2>/dev/null)
|
|
493
|
+
if echo "$SETUP_R" | grep -q "Starting setup\|progress\|complete"; then
|
|
494
|
+
ok "A1: /workspace/setup OK"
|
|
495
|
+
else
|
|
496
|
+
fail "A1: /workspace/setup FAIL — $SETUP_R"
|
|
497
|
+
ALL_GREEN=false
|
|
498
|
+
# Fix: re-adicionar endpoints
|
|
499
|
+
add_setup_endpoint
|
|
500
|
+
deploy_agent "fix setup endpoint"
|
|
501
|
+
fi
|
|
502
|
+
|
|
503
|
+
# ─── CHECK A2: /workspace/build-push ───
|
|
504
|
+
log "CHECK A2: /workspace/build-push"
|
|
505
|
+
BP_R=$(curl -s -X POST "$AGENT/workspace/build-push" \
|
|
506
|
+
-H "Content-Type: application/json" \
|
|
507
|
+
-d '{"workspaceId":"x","imageName":"x","gcpProject":"x","saKeyJson":"{}"}' \
|
|
508
|
+
--max-time 10 2>/dev/null)
|
|
509
|
+
if echo "$BP_R" | grep -q "Authenticating\|progress\|error\|auth"; then
|
|
510
|
+
ok "A2: /workspace/build-push exists"
|
|
511
|
+
else
|
|
512
|
+
fail "A2: /workspace/build-push FAIL"
|
|
513
|
+
ALL_GREEN=false
|
|
514
|
+
add_setup_endpoint
|
|
515
|
+
deploy_agent "fix build-push endpoint"
|
|
516
|
+
fi
|
|
517
|
+
|
|
518
|
+
# ─── CHECK A3: Clone com token real ───
|
|
519
|
+
log "CHECK A3: Clone com token real"
|
|
520
|
+
GIT_TOKEN=$(PGPASSWORD='DeepDebugQA@2026' psql -h 35.224.155.29 -U deepdebug -d deepdebug -t -c \
|
|
521
|
+
"SELECT t.access_token FROM oauth_tokens t JOIN tenants tn ON tn.id::text=t.tenant_id WHERE t.provider='GITHUB' LIMIT 1;" \
|
|
522
|
+
| tr -d ' \n')
|
|
523
|
+
|
|
524
|
+
if [ -n "$GIT_TOKEN" ]; then
|
|
525
|
+
CLONE_R=$(curl -s -N -X POST "$AGENT/workspace/setup" \
|
|
526
|
+
-H "Content-Type: application/json" \
|
|
527
|
+
-d "{\"workspaceId\":\"agent-check-clone-$(date +%s)\",\"repoUrl\":\"https://github.com/williambella/simple-api.git\",\"gitToken\":\"$GIT_TOKEN\",\"provider\":\"deepdebug_cloud\"}" \
|
|
528
|
+
--max-time 60 2>/dev/null)
|
|
529
|
+
if echo "$CLONE_R" | grep -q "Stack:\|Cloned on branch\|complete"; then
|
|
530
|
+
ok "A3: Clone with token OK"
|
|
531
|
+
else
|
|
532
|
+
fail "A3: Clone FAIL — checking logs"
|
|
533
|
+
read_logs
|
|
534
|
+
ALL_GREEN=false
|
|
535
|
+
fi
|
|
536
|
+
fi
|
|
537
|
+
|
|
538
|
+
# ─── CHECK A4: /workspace/build-push com SA real ───
|
|
539
|
+
log "CHECK A4: build-push com SA key real"
|
|
540
|
+
SA_KEY_ENC=$(PGPASSWORD='DeepDebugQA@2026' psql -h 35.224.155.29 -U deepdebug -d deepdebug -t -c \
|
|
541
|
+
"SELECT service_account_key FROM oauth_tokens WHERE tenant_id='63922779-7321-4e79-b0f0-63890cd0d98e' AND provider='GCP';" \
|
|
542
|
+
| tr -d ' \n')
|
|
543
|
+
ENC_KEY=$(gcloud secrets versions access latest --secret=encryption-key-qa --project=insptechai 2>/dev/null)
|
|
544
|
+
|
|
545
|
+
# Testar JWT geração localmente primeiro
|
|
546
|
+
JWT_TEST=$(node -e "
|
|
547
|
+
const crypto = require('crypto');
|
|
548
|
+
try {
|
|
549
|
+
const enc = '$SA_KEY_ENC';
|
|
550
|
+
const key = '$ENC_KEY';
|
|
551
|
+
const kb = Buffer.alloc(32); Buffer.from(key).copy(kb);
|
|
552
|
+
const dec = crypto.createDecipheriv('aes-256-ecb', kb, null);
|
|
553
|
+
dec.setAutoPadding(true);
|
|
554
|
+
const sa = JSON.parse(Buffer.concat([dec.update(Buffer.from(enc,'base64')),dec.final()]).toString());
|
|
555
|
+
console.log('SA_OK:'+sa.client_email+':'+sa.project_id);
|
|
556
|
+
} catch(e) { console.log('SA_FAIL:'+e.message); }
|
|
557
|
+
" 2>/dev/null)
|
|
558
|
+
|
|
559
|
+
if echo "$JWT_TEST" | grep -q "SA_OK"; then
|
|
560
|
+
log "SA key decrypt OK: $JWT_TEST"
|
|
561
|
+
ok "A4: SA key decrypts correctly"
|
|
562
|
+
else
|
|
563
|
+
fail "A4: SA key decrypt FAIL: $JWT_TEST"
|
|
564
|
+
ALL_GREEN=false
|
|
565
|
+
log "Encryption key issue — checking..."
|
|
566
|
+
fi
|
|
567
|
+
|
|
568
|
+
# ─── CHECK A5: Cloud Build no projecto cliente ───
|
|
569
|
+
log "CHECK A5: Cloud Build acessível"
|
|
570
|
+
SA_TOKEN=$(node -e "
|
|
571
|
+
const crypto = require('crypto');
|
|
572
|
+
const enc = '$SA_KEY_ENC';
|
|
573
|
+
const key = '$ENC_KEY';
|
|
574
|
+
const kb = Buffer.alloc(32); Buffer.from(key).copy(kb);
|
|
575
|
+
const dec = crypto.createDecipheriv('aes-256-ecb', kb, null);
|
|
576
|
+
dec.setAutoPadding(true);
|
|
577
|
+
const sa = JSON.parse(Buffer.concat([dec.update(Buffer.from(enc,'base64')),dec.final()]).toString());
|
|
578
|
+
const now = Math.floor(Date.now()/1000);
|
|
579
|
+
const h = Buffer.from(JSON.stringify({alg:'RS256',typ:'JWT'})).toString('base64url');
|
|
580
|
+
const c = Buffer.from(JSON.stringify({iss:sa.client_email,scope:'https://www.googleapis.com/auth/cloud-platform',aud:'https://oauth2.googleapis.com/token',iat:now,exp:now+3600})).toString('base64url');
|
|
581
|
+
const s = crypto.createSign('SHA256'); s.update(h+'.'+c);
|
|
582
|
+
const jwt = h+'.'+c+'.'+s.sign(sa.private_key,'base64url');
|
|
583
|
+
fetch('https://oauth2.googleapis.com/token',{method:'POST',headers:{'Content-Type':'application/x-www-form-urlencoded'},body:'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer&assertion='+jwt})
|
|
584
|
+
.then(r=>r.json()).then(d=>{
|
|
585
|
+
if(d.access_token) { process.stdout.write(d.access_token); }
|
|
586
|
+
else { console.error('FAIL:'+JSON.stringify(d)); process.exit(1); }
|
|
587
|
+
}).catch(e=>{console.error(e.message);process.exit(1);});
|
|
588
|
+
" 2>/dev/null)
|
|
589
|
+
|
|
590
|
+
if [ -n "$SA_TOKEN" ] && [ ${#SA_TOKEN} -gt 20 ]; then
|
|
591
|
+
ok "A5: SA token obtained (${#SA_TOKEN} chars)"
|
|
592
|
+
# Testar Cloud Run API
|
|
593
|
+
CR_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
594
|
+
-H "Authorization: Bearer $SA_TOKEN" \
|
|
595
|
+
"https://run.googleapis.com/v2/projects/$GCP_CLIENT/locations/us-central1/services" 2>/dev/null)
|
|
596
|
+
log "Cloud Run API status: $CR_STATUS"
|
|
597
|
+
if [ "$CR_STATUS" = "200" ]; then
|
|
598
|
+
ok "A5: Cloud Run API accessible with SA token"
|
|
599
|
+
else
|
|
600
|
+
fail "A5: Cloud Run API returned $CR_STATUS — fixing roles"
|
|
601
|
+
ALL_GREEN=false
|
|
602
|
+
gcloud projects add-iam-policy-binding $GCP_CLIENT \
|
|
603
|
+
--member="serviceAccount:$SA_EMAIL" --role="roles/run.admin" --quiet 2>/dev/null
|
|
604
|
+
gcloud projects add-iam-policy-binding $GCP_CLIENT \
|
|
605
|
+
--member="serviceAccount:$SA_EMAIL" --role="roles/iam.serviceAccountUser" --quiet 2>/dev/null
|
|
606
|
+
sleep 10
|
|
607
|
+
fi
|
|
608
|
+
else
|
|
609
|
+
fail "A5: Could not obtain SA token"
|
|
610
|
+
ALL_GREEN=false
|
|
611
|
+
fi
|
|
612
|
+
|
|
613
|
+
if [ "$ALL_GREEN" = "true" ]; then
|
|
614
|
+
log "======================================"
|
|
615
|
+
log "AGENT — TODOS OS ITEMS VERDES!"
|
|
616
|
+
log "======================================"
|
|
617
|
+
AGENT_DONE=true
|
|
618
|
+
else
|
|
619
|
+
log "Items failing — fixing and retrying..."
|
|
620
|
+
sleep 5
|
|
621
|
+
fi
|
|
622
|
+
done
|
|
623
|
+
```
|