deepdebug-local-agent 1.0.22 → 1.1.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/.claude/context.md +76 -0
- package/.claude/skills/README.md +25 -0
- package/.claude/skills/tdd-debugging/SKILL.md +53 -0
- package/.claude/skills/tdd-debugging/references/anti-patterns.md +43 -0
- package/.claude/skills/tdd-debugging/references/command-reference.md +46 -0
- package/.claude/standards/README.md +51 -0
- package/.claude/standards/clean-code.md +75 -0
- package/.claude/standards/code-review-checklist.md +61 -0
- package/.claude/standards/layered-architecture.md +163 -0
- package/.claude/standards/node.md +45 -0
- package/.claude/standards/security-owasp.md +114 -0
- package/.claude/standards/simplicity.md +42 -0
- package/.claude/standards/solid.md +64 -0
- package/.claude/standards/testing.md +59 -0
- package/.cursor/rules/standards.mdc +17 -0
- package/.github/workflows/build-byoc-image.yml +315 -0
- package/.github/workflows/deploy-gcp.yml +102 -0
- package/.github/workflows/npm-publish.yml +69 -8
- package/AGENTS.md +21 -0
- package/CHANGELOG.md +69 -0
- package/CLAUDE.md +13 -0
- package/CLAUDE_CODE_AGENT_INFINITE.md +5 -3
- package/Dockerfile +4 -0
- package/Dockerfile.full +55 -3
- package/README.md +81 -0
- package/bin/install.js +16 -5
- package/cloudbuild-full.yaml +57 -0
- package/docker-compose.yml +5 -3
- package/docs/OBSERVABILITY.md +164 -0
- package/docs/byoc-agent-image.md +70 -0
- package/docs/graphify-packaging-plan.md +133 -0
- package/docs/graphify-validation-run-2026-10-28.md +243 -0
- package/docs/legacy-connectors.md +268 -0
- package/docs/releasing.md +43 -0
- package/docs/signed-commands.md +163 -0
- package/env.example +30 -0
- package/helm/Chart.yaml +2 -2
- package/helm/templates/deployment.yaml +13 -3
- package/helm/templates/networkpolicy.yaml +22 -0
- package/helm/templates/secret.yaml +1 -0
- package/helm/values.yaml +13 -14
- package/mcp-server.js +4 -1
- package/package.json +12 -4
- package/scripts/sync-fixtures.mjs +246 -0
- package/scripts/upgrade-observability-push.sh +227 -0
- package/src/adapters/as400/as400-parser.js +12 -0
- package/src/adapters/as400/index.js +134 -0
- package/src/adapters/cobol/cobol-parser.js +37 -0
- package/src/adapters/cobol/index.js +151 -0
- package/src/adapters/ibmmq/ibmmq-parser.js +23 -0
- package/src/adapters/ibmmq/index.js +137 -0
- package/src/adapters/index.js +79 -0
- package/src/adapters/oracle-forms/index.js +145 -0
- package/src/adapters/oracle-forms/oracle-forms-parser.js +11 -0
- package/src/adapters/sap/index.js +146 -0
- package/src/adapters/sap/sap-parser.js +41 -0
- package/src/ai-engine.js +22 -22
- package/src/config/trusted-signing-keys.json +3 -0
- package/src/detectors/build-tool-detector.js +50 -0
- package/src/detectors/multi-project-detector.js +22 -22
- package/src/detectors/service-detector.js +24 -24
- package/src/exec-utils.js +26 -4
- package/src/git/base-git-provider.js +117 -117
- package/src/git/bitbucket-provider.js +2 -2
- package/src/git/git-provider-registry.js +18 -18
- package/src/git/github-provider.js +13 -13
- package/src/git/multi-repo-pr.js +161 -0
- package/src/mcp-http-server.js +32 -16
- package/src/observability/bundle-files.js +418 -0
- package/src/observability/detect.js +151 -0
- package/src/observability/provision.js +212 -0
- package/src/observability/render-cli.js +59 -0
- package/src/observability/routes.js +88 -0
- package/src/patch/patch-engine.js +28 -28
- package/src/patch-manager.js +85 -85
- package/src/patch_bkp.js +14 -14
- package/src/ports.js +4 -4
- package/src/routes/workspace.route.js +47 -16
- package/src/runtimes/base-runtime.js +75 -75
- package/src/runtimes/java/gradle-runtime.js +17 -17
- package/src/runtimes/java/java-integrations.js +25 -25
- package/src/runtimes/java/maven-runtime.js +34 -34
- package/src/runtimes/node/node-integrations.js +1 -1
- package/src/runtimes/node/npm-runtime.js +14 -14
- package/src/runtimes/node/yarn-runtime.js +4 -4
- package/src/runtimes/runtime-registry.js +25 -25
- package/src/server-local.js +4 -4
- package/src/server.js +1412 -335
- package/src/siblings/burst-suppressor.js +89 -0
- package/src/siblings/capabilities.js +273 -0
- package/src/siblings/env-compat.cjs +58 -0
- package/src/siblings/incident-forwarder.js +320 -0
- package/src/siblings/logger.js +15 -0
- package/src/siblings/metrics.js +94 -0
- package/src/utils/agent-auth.js +184 -0
- package/src/utils/approval-token.js +56 -0
- package/src/utils/bind-host.js +29 -0
- package/src/utils/branch-refspec.js +98 -0
- package/src/utils/child-env.js +34 -0
- package/src/utils/environment-diagnostics.js +55 -55
- package/src/utils/exec-policy.js +962 -0
- package/src/utils/exec-utils.js +15 -15
- package/src/utils/file-count.js +46 -0
- package/src/utils/fs-utils.js +10 -10
- package/src/utils/multi-repo-open.js +132 -0
- package/src/utils/open-gate.js +183 -0
- package/src/utils/ranged-read.js +65 -0
- package/src/utils/request-guards.js +77 -0
- package/src/utils/request-signing.js +342 -0
- package/src/utils/root-recovery.js +244 -0
- package/src/utils/safe-exec-args.js +58 -0
- package/src/utils/sandbox.js +182 -0
- package/src/utils/search-outcome.js +56 -0
- package/src/utils/workspace-root.js +80 -0
- package/src/validators/syntax-validator.js +149 -0
- package/src/workspace/detect-port.js +16 -16
- package/src/workspace/file-reader.js +1 -1
- package/src/workspace/process-manager.js +46 -46
- package/src/workspace-manager.js +86 -24
- package/test/adapters/as400/as400.test.js +121 -0
- package/test/adapters/cobol/cobol.test.js +217 -0
- package/test/adapters/ibmmq/ibmmq.test.js +122 -0
- package/test/adapters/oracle-forms/oracle-forms.test.js +180 -0
- package/test/adapters/registry.test.js +108 -0
- package/test/adapters/sap/sap.test.js +142 -0
- package/test/agent-auth.test.js +194 -0
- package/test/bind-host.test.js +26 -0
- package/test/branch-refspec.test.js +224 -0
- package/test/burst-suppressor.test.js +60 -0
- package/test/capabilities-access.test.js +47 -0
- package/test/capabilities.test.js +87 -0
- package/test/child-env.test.js +23 -0
- package/test/env-compat.test.js +71 -0
- package/test/exec-policy.test.js +463 -0
- package/test/file-count.test.js +64 -0
- package/test/incident-forwarder.test.js +231 -0
- package/test/multi-repo-open.test.js +180 -0
- package/test/multi-repo-pr.test.js +129 -0
- package/test/observability-detect.test.js +199 -0
- package/test/open-gate.test.js +341 -0
- package/test/ranged-read.test.js +60 -0
- package/test/request-guards.test.js +91 -0
- package/test/request-signing.test.js +284 -0
- package/test/root-recovery-sidecar.test.js +171 -0
- package/test/root-recovery.test.js +274 -0
- package/test/safe-exec-args.test.js +58 -0
- package/test/sandbox.test.js +98 -0
- package/test/search-outcome.test.js +50 -0
- package/test/workspace-root.test.js +64 -0
- package/tools/graphify-validation/README.md +92 -0
- package/tools/graphify-validation/config/questions.json +31 -0
- package/tools/graphify-validation/config/repos.json +9 -0
- package/tools/graphify-validation/config/symbols.json +6 -0
- package/tools/graphify-validation/fixtures/spring/expected.json +34 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AppConfig.java +29 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AuditLogger.java +15 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/Cache.java +7 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/CheckoutService.java +25 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/InMemoryCache.java +18 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/OrderController.java +22 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaymentGateway.java +5 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaypalGateway.java +11 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/RefundService.java +14 -0
- package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/StripeGateway.java +11 -0
- package/tools/graphify-validation/lib/affected_vs_rg.py +81 -0
- package/tools/graphify-validation/lib/audit_hook/sitecustomize.py +22 -0
- package/tools/graphify-validation/lib/graph_metrics.py +96 -0
- package/tools/graphify-validation/lib/gv_common.py +130 -0
- package/tools/graphify-validation/lib/licenses.py +47 -0
- package/tools/graphify-validation/lib/pip_audit_summary.py +18 -0
- package/tools/graphify-validation/lib/recall.py +115 -0
- package/tools/graphify-validation/lib/report.py +226 -0
- package/tools/graphify-validation/lib/run_all.py +237 -0
- package/tools/graphify-validation/lib/run_graphify.py +144 -0
- package/tools/graphify-validation/lib/rusage_helper.py +17 -0
- package/tools/graphify-validation/lib/sample_edges.py +246 -0
- package/tools/graphify-validation/lib/spring_fixture.py +81 -0
- package/tools/graphify-validation/requirements-py311.lock +508 -0
- package/tools/graphify-validation/requirements-tools.in +1 -0
- package/tools/graphify-validation/requirements-tools.lock +371 -0
- package/tools/graphify-validation/requirements.in +1 -0
- package/tools/graphify-validation/requirements.lock +502 -0
- package/tools/graphify-validation/run.sh +43 -0
- package/tools/graphify-validation/tests/test_harness.py +151 -0
- package/cloudbuild-agent-qa.yaml +0 -43
- package/cloudbuild-agent-staging.yaml +0 -40
- package/cloudbuild.yaml.deprecated +0 -42
- package/src/local-agent-source.tar.gz +0 -0
|
@@ -7,13 +7,13 @@ import path from "path";
|
|
|
7
7
|
/**
|
|
8
8
|
* ProcessManager
|
|
9
9
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* Manages the process lifecycle (start, stop, restart)
|
|
11
|
+
* Monitors CPU, Memory, Uptime
|
|
12
|
+
* Captures logs in real time
|
|
13
13
|
*
|
|
14
|
-
* UNIFIED VERSION:
|
|
15
|
-
* 1. command/args
|
|
16
|
-
* 2. language/framework/buildTool (
|
|
14
|
+
* UNIFIED VERSION: Accepts BOTH formats:
|
|
15
|
+
* 1. command/args directly (new)
|
|
16
|
+
* 2. language/framework/buildTool (legacy)
|
|
17
17
|
*/
|
|
18
18
|
export class ProcessManager extends EventEmitter {
|
|
19
19
|
constructor() {
|
|
@@ -22,7 +22,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* Checks whether a service is running
|
|
26
26
|
*/
|
|
27
27
|
isRunning(serviceId) {
|
|
28
28
|
const processData = this.processes.get(serviceId);
|
|
@@ -30,11 +30,11 @@ export class ProcessManager extends EventEmitter {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
/**
|
|
33
|
-
*
|
|
33
|
+
* Starts a service
|
|
34
34
|
*
|
|
35
|
-
*
|
|
36
|
-
* 1. { command, args, cwd, port, env } -
|
|
37
|
-
* 2. { language, framework, buildTool, cwd, port, profile } -
|
|
35
|
+
* ACCEPTS TWO FORMATS:
|
|
36
|
+
* 1. { command, args, cwd, port, env } - New direct format
|
|
37
|
+
* 2. { language, framework, buildTool, cwd, port, profile } - Legacy format
|
|
38
38
|
*/
|
|
39
39
|
async start(serviceId, config) {
|
|
40
40
|
const {
|
|
@@ -45,7 +45,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
45
45
|
language,
|
|
46
46
|
framework,
|
|
47
47
|
buildTool,
|
|
48
|
-
//
|
|
48
|
+
// Common
|
|
49
49
|
cwd,
|
|
50
50
|
port,
|
|
51
51
|
profile,
|
|
@@ -53,23 +53,23 @@ export class ProcessManager extends EventEmitter {
|
|
|
53
53
|
envVars = {}
|
|
54
54
|
} = config;
|
|
55
55
|
|
|
56
|
-
//
|
|
56
|
+
// If it is already running, stop it first
|
|
57
57
|
if (this.processes.has(serviceId)) {
|
|
58
58
|
console.log(`⚠️ Service ${serviceId} already running, stopping first...`);
|
|
59
59
|
await this.stop(serviceId);
|
|
60
60
|
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
//
|
|
63
|
+
// Determine which command to use
|
|
64
64
|
let cmd, cmdArgs;
|
|
65
65
|
|
|
66
66
|
if (command && args) {
|
|
67
|
-
//
|
|
67
|
+
// NEW FORMAT: command/args passed directly
|
|
68
68
|
cmd = command;
|
|
69
69
|
cmdArgs = args;
|
|
70
70
|
console.log(`🚀 Starting ${serviceId} (direct): ${cmd} ${cmdArgs.join(' ')}`);
|
|
71
71
|
} else if (language) {
|
|
72
|
-
//
|
|
72
|
+
// LEGACY FORMAT: derive from language/framework/buildTool
|
|
73
73
|
const startCommand = await this.getStartCommand(language, framework, buildTool, cwd, profile || 'dev');
|
|
74
74
|
cmd = startCommand.cmd;
|
|
75
75
|
cmdArgs = startCommand.args;
|
|
@@ -167,7 +167,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
167
167
|
|
|
168
168
|
this.emit("log", { serviceId, message: log, type: "stdout" });
|
|
169
169
|
|
|
170
|
-
//
|
|
170
|
+
// Detect when the service is ready
|
|
171
171
|
if (this.isServiceReady(log, language || 'java')) {
|
|
172
172
|
if (processData.status !== "running") {
|
|
173
173
|
processData.status = "running";
|
|
@@ -178,7 +178,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
178
178
|
}
|
|
179
179
|
});
|
|
180
180
|
|
|
181
|
-
//
|
|
181
|
+
// Capture stderr (Spring Boot also logs here)
|
|
182
182
|
child.stderr.on("data", (data) => {
|
|
183
183
|
const text = data.toString();
|
|
184
184
|
const lines = text.split('\n').filter(l => l.trim());
|
|
@@ -195,7 +195,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
195
195
|
|
|
196
196
|
this.emit("log", { serviceId, message: log, type: "stderr" });
|
|
197
197
|
|
|
198
|
-
// Spring Boot
|
|
198
|
+
// Spring Boot may log "Started" on stderr too
|
|
199
199
|
if (this.isServiceReady(log, language || 'java')) {
|
|
200
200
|
if (processData.status !== "running") {
|
|
201
201
|
processData.status = "running";
|
|
@@ -206,21 +206,21 @@ export class ProcessManager extends EventEmitter {
|
|
|
206
206
|
}
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
-
//
|
|
209
|
+
// Capture startup errors
|
|
210
210
|
child.on("error", (err) => {
|
|
211
211
|
console.error(`❌ Failed to start ${serviceId}:`, err.message);
|
|
212
212
|
processData.status = "failed";
|
|
213
213
|
this.emit("error", { serviceId, error: err.message });
|
|
214
214
|
});
|
|
215
215
|
|
|
216
|
-
//
|
|
216
|
+
// Capture termination
|
|
217
217
|
child.on("close", (code, signal) => {
|
|
218
218
|
console.log(`⏹️ Service ${serviceId} exited with code ${code}, signal ${signal}`);
|
|
219
219
|
this.processes.delete(serviceId);
|
|
220
220
|
this.emit("stopped", { serviceId, code, signal });
|
|
221
221
|
});
|
|
222
222
|
|
|
223
|
-
//
|
|
223
|
+
// Start resource monitoring
|
|
224
224
|
this.startResourceMonitoring(serviceId);
|
|
225
225
|
|
|
226
226
|
return {
|
|
@@ -234,7 +234,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
234
234
|
}
|
|
235
235
|
|
|
236
236
|
/**
|
|
237
|
-
*
|
|
237
|
+
* Stops a service
|
|
238
238
|
*/
|
|
239
239
|
async stop(serviceId) {
|
|
240
240
|
const processData = this.processes.get(serviceId);
|
|
@@ -272,14 +272,14 @@ export class ProcessManager extends EventEmitter {
|
|
|
272
272
|
child.once('exit', onExit);
|
|
273
273
|
child.once('close', onExit);
|
|
274
274
|
|
|
275
|
-
//
|
|
275
|
+
// Try a graceful shutdown
|
|
276
276
|
try {
|
|
277
277
|
child.kill('SIGTERM');
|
|
278
278
|
} catch (err) {
|
|
279
279
|
console.log(`⚠️ SIGTERM failed for ${serviceId}: ${err.message}`);
|
|
280
280
|
}
|
|
281
281
|
|
|
282
|
-
// Force kill
|
|
282
|
+
// Force kill after 5 seconds
|
|
283
283
|
setTimeout(() => {
|
|
284
284
|
if (!resolved && this.processes.has(serviceId)) {
|
|
285
285
|
console.log(`🔨 Force killing ${serviceId} (SIGKILL)`);
|
|
@@ -291,7 +291,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
291
291
|
}
|
|
292
292
|
}, 5000);
|
|
293
293
|
|
|
294
|
-
//
|
|
294
|
+
// Overall timeout after 10 seconds
|
|
295
295
|
setTimeout(() => {
|
|
296
296
|
if (!resolved) {
|
|
297
297
|
resolved = true;
|
|
@@ -309,7 +309,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
/**
|
|
312
|
-
*
|
|
312
|
+
* Returns the status of a service
|
|
313
313
|
*/
|
|
314
314
|
getStatus(serviceId) {
|
|
315
315
|
const processData = this.processes.get(serviceId);
|
|
@@ -333,7 +333,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
/**
|
|
336
|
-
*
|
|
336
|
+
* Returns the logs of a service
|
|
337
337
|
*/
|
|
338
338
|
getLogs(serviceId, limit = 100) {
|
|
339
339
|
const processData = this.processes.get(serviceId);
|
|
@@ -344,7 +344,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
344
344
|
}
|
|
345
345
|
|
|
346
346
|
/**
|
|
347
|
-
*
|
|
347
|
+
* Lists all services
|
|
348
348
|
*/
|
|
349
349
|
listServices() {
|
|
350
350
|
const services = [];
|
|
@@ -355,20 +355,20 @@ export class ProcessManager extends EventEmitter {
|
|
|
355
355
|
}
|
|
356
356
|
|
|
357
357
|
/**
|
|
358
|
-
* Alias
|
|
358
|
+
* Alias for compatibility
|
|
359
359
|
*/
|
|
360
360
|
listAll() {
|
|
361
361
|
return this.listServices();
|
|
362
362
|
}
|
|
363
363
|
|
|
364
364
|
/**
|
|
365
|
-
*
|
|
366
|
-
*
|
|
365
|
+
* Determines the start command based on the framework (LEGACY FORMAT)
|
|
366
|
+
* Used when server.js does not pass command/args directly
|
|
367
367
|
*/
|
|
368
368
|
async getStartCommand(language, framework, buildTool, cwd, profile = 'dev') {
|
|
369
369
|
console.log(`🔧 getStartCommand: language=${language}, framework=${framework}, buildTool=${buildTool}`);
|
|
370
370
|
|
|
371
|
-
// Java (
|
|
371
|
+
// Java (any framework or no framework)
|
|
372
372
|
if (language === "java") {
|
|
373
373
|
return await this.getJavaCommand(buildTool, cwd, profile);
|
|
374
374
|
}
|
|
@@ -393,14 +393,14 @@ export class ProcessManager extends EventEmitter {
|
|
|
393
393
|
return await this.getGoCommand(cwd);
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
-
//
|
|
396
|
+
// Generic fallback - try npm start
|
|
397
397
|
console.warn(`⚠️ Unknown language: ${language}, trying npm start`);
|
|
398
398
|
return { cmd: "npm", args: ["start"] };
|
|
399
399
|
}
|
|
400
400
|
|
|
401
401
|
/**
|
|
402
|
-
*
|
|
403
|
-
|
|
402
|
+
* Java/Spring Boot command
|
|
403
|
+
async getJavaCommand(buildTool, cwd, profile = null) {
|
|
404
404
|
async getJavaCommand(buildTool, cwd, profile = null) {
|
|
405
405
|
const targetDir = path.join(cwd, "target");
|
|
406
406
|
const buildDir = path.join(cwd, "build", "libs");
|
|
@@ -433,7 +433,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
433
433
|
return javaArgs;
|
|
434
434
|
};
|
|
435
435
|
|
|
436
|
-
// Maven:
|
|
436
|
+
// Maven: Look for a JAR in target/
|
|
437
437
|
if (buildTool === "maven") {
|
|
438
438
|
console.log(`📁 Target directory: ${targetDir} (exists: ${fs.existsSync(targetDir)})`);
|
|
439
439
|
|
|
@@ -470,11 +470,11 @@ export class ProcessManager extends EventEmitter {
|
|
|
470
470
|
}
|
|
471
471
|
}
|
|
472
472
|
|
|
473
|
-
//
|
|
473
|
+
// No JAR - ask to compile
|
|
474
474
|
throw new Error(`No JAR found. Please compile first: mvn clean install -DskipTests`);
|
|
475
475
|
}
|
|
476
476
|
|
|
477
|
-
// Gradle:
|
|
477
|
+
// Gradle: Look for a JAR in build/libs/
|
|
478
478
|
if (buildTool === "gradle") {
|
|
479
479
|
console.log(`📁 Build directory: ${buildDir} (exists: ${fs.existsSync(buildDir)})`);
|
|
480
480
|
|
|
@@ -509,8 +509,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
509
509
|
}
|
|
510
510
|
|
|
511
511
|
/**
|
|
512
|
-
*
|
|
513
|
-
|
|
512
|
+
* Node.js command
|
|
513
|
+
async getNodeCommand(framework, cwd) {
|
|
514
514
|
async getNodeCommand(framework, cwd) {
|
|
515
515
|
const nodeModules = path.join(cwd, "node_modules");
|
|
516
516
|
const hasNodeModules = fs.existsSync(nodeModules);
|
|
@@ -530,7 +530,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
530
530
|
}
|
|
531
531
|
|
|
532
532
|
/**
|
|
533
|
-
*
|
|
533
|
+
* Python command
|
|
534
534
|
*/
|
|
535
535
|
async getPythonCommand(framework, cwd) {
|
|
536
536
|
if (framework === "django") {
|
|
@@ -546,21 +546,21 @@ export class ProcessManager extends EventEmitter {
|
|
|
546
546
|
}
|
|
547
547
|
|
|
548
548
|
/**
|
|
549
|
-
*
|
|
549
|
+
* .NET command
|
|
550
550
|
*/
|
|
551
551
|
async getDotNetCommand(cwd) {
|
|
552
552
|
return { cmd: "dotnet", args: ["run"] };
|
|
553
553
|
}
|
|
554
554
|
|
|
555
555
|
/**
|
|
556
|
-
*
|
|
556
|
+
* Go command
|
|
557
557
|
*/
|
|
558
558
|
async getGoCommand(cwd) {
|
|
559
559
|
return { cmd: "go", args: ["run", "."] };
|
|
560
560
|
}
|
|
561
561
|
|
|
562
562
|
/**
|
|
563
|
-
*
|
|
563
|
+
* Detects when the service is ready
|
|
564
564
|
*/
|
|
565
565
|
isServiceReady(log, language) {
|
|
566
566
|
if (!log) return false;
|
|
@@ -591,7 +591,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
591
591
|
}
|
|
592
592
|
|
|
593
593
|
/**
|
|
594
|
-
*
|
|
594
|
+
* Monitors resources (CPU and Memory)
|
|
595
595
|
*/
|
|
596
596
|
startResourceMonitoring(serviceId) {
|
|
597
597
|
const interval = setInterval(async () => {
|
package/src/workspace-manager.js
CHANGED
|
@@ -1,30 +1,36 @@
|
|
|
1
1
|
import path from "path";
|
|
2
|
+
import fs from "fs";
|
|
2
3
|
import { exists, listRecursive, readFile, stat } from "./fs-utils.js";
|
|
4
|
+
import { WORKSPACE_SIDECAR, readWorkspaceSidecar } from "./utils/open-gate.js";
|
|
3
5
|
import { detectProject } from "./detectors.js";
|
|
4
6
|
import { detectPort } from "./ports.js";
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
|
-
* WorkspaceManager —
|
|
9
|
+
* WorkspaceManager — Manages multiple simultaneous workspaces.
|
|
8
10
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
+
* Replaces the global WORKSPACE_ROOT variable.
|
|
12
|
+
* Each workspace is identified by an ID and has its own root path.
|
|
11
13
|
*/
|
|
12
14
|
export class WorkspaceManager {
|
|
13
15
|
constructor() {
|
|
14
16
|
/** @type {Map<string, WorkspaceState>} */
|
|
15
17
|
this.workspaces = new Map();
|
|
16
18
|
|
|
17
|
-
/** Backward compat:
|
|
19
|
+
/** Backward compat: "default" workspace for legacy REST */
|
|
18
20
|
this.defaultWorkspaceId = null;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
|
-
*
|
|
23
|
-
* @param {string} workspaceId -
|
|
24
|
-
* @param {string} rootPath -
|
|
24
|
+
* Opens a workspace. If one already exists with the same ID, updates the root.
|
|
25
|
+
* @param {string} workspaceId - Unique identifier
|
|
26
|
+
* @param {string} rootPath - Absolute path
|
|
27
|
+
* @param {{tenantId?: string|null}} [meta] - Optional. tenantId = tenant this
|
|
28
|
+
* clone was hydrated for (TENANT GATE for ROOT-RECOVERY). When absent,
|
|
29
|
+
* a previously registered tenantId for the same workspaceId is kept;
|
|
30
|
+
* otherwise stays null (= unknown, recovery fails closed).
|
|
25
31
|
* @returns {Promise<WorkspaceState>}
|
|
26
32
|
*/
|
|
27
|
-
async open(workspaceId, rootPath) {
|
|
33
|
+
async open(workspaceId, rootPath, meta = {}) {
|
|
28
34
|
const abs = path.resolve(rootPath);
|
|
29
35
|
if (!(await exists(abs))) {
|
|
30
36
|
throw new Error(`Workspace path not found: ${abs}`);
|
|
@@ -33,9 +39,24 @@ export class WorkspaceManager {
|
|
|
33
39
|
const projectInfo = await detectProject(abs);
|
|
34
40
|
const port = await detectPort(abs);
|
|
35
41
|
|
|
42
|
+
// A previous registration under the same id only lends its tenant when
|
|
43
|
+
// it is for the SAME directory: ids like "default" are reused by
|
|
44
|
+
// different clones, and carrying tenant A over to tenant B's dir would
|
|
45
|
+
// mislabel it (registry AND sidecar, which ROOT-RECOVERY trusts).
|
|
46
|
+
const prevAny = this.workspaces.get(workspaceId);
|
|
47
|
+
const prev = prevAny && prevAny.root === abs ? prevAny : null;
|
|
48
|
+
// SIDECAR (QA 2026-09-22): after a restart the Map is empty, but the
|
|
49
|
+
// dir may still carry its registration on disk - keep that tenant
|
|
50
|
+
// instead of downgrading it to "unknown" (fail-closed elsewhere).
|
|
51
|
+
const sidecar = (!(meta && meta.tenantId) && !(prev && prev.tenantId))
|
|
52
|
+
? readWorkspaceSidecar(abs) : null;
|
|
53
|
+
const tenantId = (meta && meta.tenantId) || (prev && prev.tenantId)
|
|
54
|
+
|| (sidecar && sidecar.id === workspaceId ? sidecar.tenantId : null) || null;
|
|
55
|
+
|
|
36
56
|
const state = {
|
|
37
57
|
id: workspaceId,
|
|
38
58
|
root: abs,
|
|
59
|
+
tenantId,
|
|
39
60
|
projectInfo,
|
|
40
61
|
port,
|
|
41
62
|
openedAt: new Date().toISOString(),
|
|
@@ -44,8 +65,9 @@ export class WorkspaceManager {
|
|
|
44
65
|
};
|
|
45
66
|
|
|
46
67
|
this.workspaces.set(workspaceId, state);
|
|
68
|
+
this.writeSidecar(state);
|
|
47
69
|
|
|
48
|
-
//
|
|
70
|
+
// If it is the first workspace, it becomes the default
|
|
49
71
|
if (!this.defaultWorkspaceId) {
|
|
50
72
|
this.defaultWorkspaceId = workspaceId;
|
|
51
73
|
}
|
|
@@ -55,16 +77,55 @@ export class WorkspaceManager {
|
|
|
55
77
|
}
|
|
56
78
|
|
|
57
79
|
/**
|
|
58
|
-
*
|
|
80
|
+
* Persist the registration next to the clone so the OPEN GATE can still
|
|
81
|
+
* recognise this dir after the in-memory Map is gone (cold start / new
|
|
82
|
+
* revision / second instance). Best-effort: a write failure is logged and
|
|
83
|
+
* never breaks open(). NEVER stores tokens.
|
|
84
|
+
*/
|
|
85
|
+
writeSidecar(state) {
|
|
86
|
+
const file = path.join(state.root, WORKSPACE_SIDECAR);
|
|
87
|
+
try {
|
|
88
|
+
// Atomic (temp + rename) and owner-only: the sidecar's tenantId is
|
|
89
|
+
// read by ROOT-RECOVERY, so it must never be observed half-written.
|
|
90
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
91
|
+
fs.writeFileSync(tmp, JSON.stringify({
|
|
92
|
+
id: state.id,
|
|
93
|
+
tenantId: state.tenantId || null,
|
|
94
|
+
root: state.root,
|
|
95
|
+
openedAt: state.openedAt
|
|
96
|
+
}, null, 2) + "\n", { mode: 0o600 });
|
|
97
|
+
fs.renameSync(tmp, file);
|
|
98
|
+
// Single-repo clones ARE git repos and the PR path runs `git add -A`:
|
|
99
|
+
// keep the sidecar out of git via .git/info/exclude (never touches
|
|
100
|
+
// the repo's own .gitignore). Multi-repo parents have no .git.
|
|
101
|
+
const infoDir = path.join(state.root, ".git", "info");
|
|
102
|
+
if (fs.existsSync(infoDir)) {
|
|
103
|
+
const excludeFile = path.join(infoDir, "exclude");
|
|
104
|
+
let current = "";
|
|
105
|
+
try { current = fs.readFileSync(excludeFile, "utf8"); } catch { current = ""; }
|
|
106
|
+
if (!current.split(/\r?\n/).includes(WORKSPACE_SIDECAR)) {
|
|
107
|
+
fs.appendFileSync(excludeFile, (current && !current.endsWith("\n") ? "\n" : "") + WORKSPACE_SIDECAR + "\n");
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
console.warn(`[workspace] sidecar write failed (non-fatal) at ${file}: ${err.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Closes a workspace.
|
|
59
117
|
*/
|
|
60
118
|
close(workspaceId) {
|
|
61
119
|
const ws = this.workspaces.get(workspaceId);
|
|
62
120
|
if (!ws) return false;
|
|
63
121
|
|
|
64
122
|
this.workspaces.delete(workspaceId);
|
|
123
|
+
// A closed workspace is no longer registered anywhere: drop the sidecar
|
|
124
|
+
// so the OPEN GATE treats the dir exactly as before (orphan rules).
|
|
125
|
+
try { fs.rmSync(path.join(ws.root, WORKSPACE_SIDECAR), { force: true }); } catch { /* best-effort */ }
|
|
65
126
|
|
|
66
127
|
if (this.defaultWorkspaceId === workspaceId) {
|
|
67
|
-
//
|
|
128
|
+
// Elect a new default
|
|
68
129
|
const first = this.workspaces.keys().next().value;
|
|
69
130
|
this.defaultWorkspaceId = first || null;
|
|
70
131
|
}
|
|
@@ -74,7 +135,7 @@ export class WorkspaceManager {
|
|
|
74
135
|
}
|
|
75
136
|
|
|
76
137
|
/**
|
|
77
|
-
*
|
|
138
|
+
* Gets the state of a workspace. Updates lastAccessedAt.
|
|
78
139
|
* @param {string} workspaceId
|
|
79
140
|
* @returns {WorkspaceState|null}
|
|
80
141
|
*/
|
|
@@ -87,7 +148,7 @@ export class WorkspaceManager {
|
|
|
87
148
|
}
|
|
88
149
|
|
|
89
150
|
/**
|
|
90
|
-
*
|
|
151
|
+
* Gets the root path of a workspace. Throws if it does not exist.
|
|
91
152
|
* @param {string} workspaceId
|
|
92
153
|
* @returns {string}
|
|
93
154
|
*/
|
|
@@ -98,8 +159,8 @@ export class WorkspaceManager {
|
|
|
98
159
|
}
|
|
99
160
|
|
|
100
161
|
/**
|
|
101
|
-
*
|
|
102
|
-
* Backward compat
|
|
162
|
+
* Resolves a workspace ID — if null, uses the default.
|
|
163
|
+
* Backward compat for REST endpoints that do not send a workspaceId.
|
|
103
164
|
*/
|
|
104
165
|
resolveId(workspaceId) {
|
|
105
166
|
if (workspaceId) return workspaceId;
|
|
@@ -108,7 +169,7 @@ export class WorkspaceManager {
|
|
|
108
169
|
}
|
|
109
170
|
|
|
110
171
|
/**
|
|
111
|
-
*
|
|
172
|
+
* Resolves the root path — accepts a workspaceId or uses the default.
|
|
112
173
|
*/
|
|
113
174
|
resolveRoot(workspaceId) {
|
|
114
175
|
const id = this.resolveId(workspaceId);
|
|
@@ -116,29 +177,29 @@ export class WorkspaceManager {
|
|
|
116
177
|
}
|
|
117
178
|
|
|
118
179
|
/**
|
|
119
|
-
*
|
|
180
|
+
* Lists all open workspaces.
|
|
120
181
|
*/
|
|
121
182
|
list() {
|
|
122
183
|
return Array.from(this.workspaces.values());
|
|
123
184
|
}
|
|
124
185
|
|
|
125
186
|
/**
|
|
126
|
-
*
|
|
187
|
+
* Checks whether a workspace is open.
|
|
127
188
|
*/
|
|
128
189
|
isOpen(workspaceId) {
|
|
129
190
|
return this.workspaces.has(workspaceId);
|
|
130
191
|
}
|
|
131
192
|
|
|
132
193
|
/**
|
|
133
|
-
* Total
|
|
194
|
+
* Total number of open workspaces.
|
|
134
195
|
*/
|
|
135
196
|
get count() {
|
|
136
197
|
return this.workspaces.size;
|
|
137
198
|
}
|
|
138
199
|
|
|
139
200
|
/**
|
|
140
|
-
* Backward compat:
|
|
141
|
-
*
|
|
201
|
+
* Backward compat: gets WORKSPACE_ROOT (default workspace).
|
|
202
|
+
* Used by the legacy REST endpoints.
|
|
142
203
|
*/
|
|
143
204
|
get defaultRoot() {
|
|
144
205
|
if (!this.defaultWorkspaceId) return null;
|
|
@@ -147,12 +208,12 @@ export class WorkspaceManager {
|
|
|
147
208
|
}
|
|
148
209
|
|
|
149
210
|
/**
|
|
150
|
-
* Backward compat:
|
|
151
|
-
*
|
|
211
|
+
* Backward compat: sets WORKSPACE_ROOT (default workspace).
|
|
212
|
+
* Called by the legacy POST /workspace/open.
|
|
152
213
|
*/
|
|
153
214
|
async setDefault(rootPath) {
|
|
154
215
|
const id = `default-${Date.now()}`;
|
|
155
|
-
//
|
|
216
|
+
// If a default already exists, reuse its ID
|
|
156
217
|
const existingId = this.defaultWorkspaceId || id;
|
|
157
218
|
await this.open(existingId, rootPath);
|
|
158
219
|
this.defaultWorkspaceId = existingId;
|
|
@@ -164,6 +225,7 @@ export class WorkspaceManager {
|
|
|
164
225
|
* @typedef {Object} WorkspaceState
|
|
165
226
|
* @property {string} id
|
|
166
227
|
* @property {string} root
|
|
228
|
+
* @property {string|null} tenantId
|
|
167
229
|
* @property {Object} projectInfo
|
|
168
230
|
* @property {number|null} port
|
|
169
231
|
* @property {string} openedAt
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { test, describe } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { EventEmitter } from 'node:events';
|
|
4
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
|
+
import { As400LogAdapter } from '../../../src/adapters/as400/index.js';
|
|
6
|
+
import { AS400_MSGID_REGEX } from '../../../src/adapters/as400/as400-parser.js';
|
|
7
|
+
|
|
8
|
+
// Stub ssh2.Client — supports .connect(), .exec(callback), .on('close'/'error'/'ready'), .end()
|
|
9
|
+
class StubSshClient extends EventEmitter {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.connectCalls = [];
|
|
13
|
+
this.execCalls = [];
|
|
14
|
+
this._channels = [];
|
|
15
|
+
this._cannedOutput = '';
|
|
16
|
+
}
|
|
17
|
+
connect(opts) {
|
|
18
|
+
this.connectCalls.push(opts);
|
|
19
|
+
// emit 'ready' on next tick
|
|
20
|
+
process.nextTick(() => this.emit('ready'));
|
|
21
|
+
}
|
|
22
|
+
exec(cmd, cb) {
|
|
23
|
+
this.execCalls.push(cmd);
|
|
24
|
+
const stream = new EventEmitter();
|
|
25
|
+
stream.stderr = new EventEmitter();
|
|
26
|
+
process.nextTick(() => {
|
|
27
|
+
cb(null, stream);
|
|
28
|
+
// Push canned output then close.
|
|
29
|
+
stream.emit('data', Buffer.from(this._cannedOutput || ''));
|
|
30
|
+
stream.emit('close', 0);
|
|
31
|
+
});
|
|
32
|
+
this._channels.push(stream);
|
|
33
|
+
}
|
|
34
|
+
end() {
|
|
35
|
+
process.nextTick(() => this.emit('close'));
|
|
36
|
+
}
|
|
37
|
+
setCannedOutput(text) { this._cannedOutput = text; }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
describe('As400LogAdapter', () => {
|
|
41
|
+
test('constructor sets keepalive defaults (T-2-02)', () => {
|
|
42
|
+
const adapter = new As400LogAdapter({ host: '1.2.3.4', username: 'u', password: 'p' });
|
|
43
|
+
assert.equal(adapter._sshOpts.keepaliveInterval, 30000);
|
|
44
|
+
assert.equal(adapter._sshOpts.keepaliveCountMax, 3);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('emits 6-key event for CPF/MCH/SQL lines (AC-2)', async () => {
|
|
48
|
+
const stub = new StubSshClient();
|
|
49
|
+
stub.setCannedOutput([
|
|
50
|
+
'CPF1234 ERROR Job DAILY01 ended abnormally',
|
|
51
|
+
'INFO This line should not match',
|
|
52
|
+
'MCH3601 Pointer not set for location *N',
|
|
53
|
+
''
|
|
54
|
+
].join('\n'));
|
|
55
|
+
const adapter = new As400LogAdapter({
|
|
56
|
+
host: '1.2.3.4', username: 'u', password: 'p',
|
|
57
|
+
pollInterval: 100, sshClientFactory: () => stub,
|
|
58
|
+
});
|
|
59
|
+
const events = [];
|
|
60
|
+
adapter.on('error', (e) => events.push(e));
|
|
61
|
+
await adapter.start();
|
|
62
|
+
await sleep(400);
|
|
63
|
+
await adapter.stop();
|
|
64
|
+
assert.ok(events.length >= 2, `expected 2 events, got ${events.length}`);
|
|
65
|
+
const e = events[0];
|
|
66
|
+
assert.equal(Object.keys(e).sort().join(','), 'adapter,errorCode,file,message,source,timestamp');
|
|
67
|
+
assert.equal(e.adapter, 'as400');
|
|
68
|
+
assert.match(e.errorCode, /^(CPF|MCH|SQL)\d{4}$/);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('non-matching lines do not emit', async () => {
|
|
72
|
+
const stub = new StubSshClient();
|
|
73
|
+
stub.setCannedOutput('INFO Just an info line\nDEBUG diag\n');
|
|
74
|
+
const adapter = new As400LogAdapter({
|
|
75
|
+
host: '1.2.3.4', username: 'u', password: 'p',
|
|
76
|
+
pollInterval: 100, sshClientFactory: () => stub,
|
|
77
|
+
});
|
|
78
|
+
const events = [];
|
|
79
|
+
adapter.on('error', (e) => events.push(e));
|
|
80
|
+
await adapter.start();
|
|
81
|
+
await sleep(400);
|
|
82
|
+
await adapter.stop();
|
|
83
|
+
assert.equal(events.length, 0);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('reconnect on close with exponential backoff (T-2-02)', async () => {
|
|
87
|
+
const factories = [];
|
|
88
|
+
const adapter = new As400LogAdapter({
|
|
89
|
+
host: '1.2.3.4', username: 'u', password: 'p',
|
|
90
|
+
pollInterval: 60000, // no poll churn
|
|
91
|
+
sshClientFactory: () => { const s = new StubSshClient(); factories.push(s); return s; },
|
|
92
|
+
backoffSchedule: [50, 100, 200], // shortened for test
|
|
93
|
+
});
|
|
94
|
+
await adapter.start();
|
|
95
|
+
await sleep(50);
|
|
96
|
+
// First client connected; force a close.
|
|
97
|
+
assert.ok(factories.length >= 1);
|
|
98
|
+
factories[0].emit('close');
|
|
99
|
+
await sleep(200); // wait for first backoff (50ms) to fire reconnect
|
|
100
|
+
assert.ok(factories.length >= 2, 'reconnect must spawn a second client after close');
|
|
101
|
+
await adapter.stop();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test('lifecycle idempotent (AC-1)', async () => {
|
|
105
|
+
const stub = new StubSshClient();
|
|
106
|
+
const adapter = new As400LogAdapter({
|
|
107
|
+
host: '1.2.3.4', username: 'u', password: 'p',
|
|
108
|
+
pollInterval: 60000, sshClientFactory: () => stub,
|
|
109
|
+
});
|
|
110
|
+
await adapter.start();
|
|
111
|
+
await adapter.start();
|
|
112
|
+
await adapter.stop();
|
|
113
|
+
await adapter.stop();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test('parser regex exported and isolated', () => {
|
|
117
|
+
assert.ok(AS400_MSGID_REGEX instanceof RegExp);
|
|
118
|
+
assert.ok('CPF1234 ...'.match(AS400_MSGID_REGEX));
|
|
119
|
+
assert.equal('INFO ...'.match(AS400_MSGID_REGEX), null);
|
|
120
|
+
});
|
|
121
|
+
});
|