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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "deepdebug-local-agent",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "DeepDebug Local Agent - AI-powered code debugging assistant",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"docker:build": "docker build -t deepdebug/local-agent:latest .",
|
|
23
23
|
"docker:push": "docker push deepdebug/local-agent:latest",
|
|
24
24
|
"test": "node --test",
|
|
25
|
+
"sync-fixtures": "node scripts/sync-fixtures.mjs",
|
|
25
26
|
"lint": "eslint src/"
|
|
26
27
|
},
|
|
27
28
|
"pkg": {
|
|
@@ -34,13 +35,20 @@
|
|
|
34
35
|
"@anthropic-ai/sdk": "^0.39.0",
|
|
35
36
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
|
36
37
|
"body-parser": "^1.20.3",
|
|
38
|
+
"chokidar": "^3.6.0",
|
|
37
39
|
"cors": "^2.8.5",
|
|
38
40
|
"express": "^4.19.2",
|
|
39
41
|
"js-yaml": "^4.1.0",
|
|
40
42
|
"pg": "^8.20.0",
|
|
41
43
|
"pidusage": "^3.0.2",
|
|
44
|
+
"pino": "^10.3.1",
|
|
45
|
+
"prom-client": "^15.1.3",
|
|
42
46
|
"properties-reader": "^2.3.0",
|
|
47
|
+
"ssh2": "^1.16.0",
|
|
43
48
|
"strip-ansi": "^7.1.0",
|
|
49
|
+
"tree-sitter": "^0.22.4",
|
|
50
|
+
"tree-sitter-swift": "^0.7.1",
|
|
51
|
+
"typescript": "^5.9.3",
|
|
44
52
|
"unidiff": "^1.0.2",
|
|
45
53
|
"ws": "^8.18.0"
|
|
46
54
|
},
|
|
@@ -72,10 +80,10 @@
|
|
|
72
80
|
"license": "Proprietary",
|
|
73
81
|
"repository": {
|
|
74
82
|
"type": "git",
|
|
75
|
-
"url": "https://github.com/
|
|
83
|
+
"url": "git+https://github.com/VISIONHAMMER/deepdebug-local-agent.git"
|
|
76
84
|
},
|
|
77
|
-
"homepage": "https://
|
|
85
|
+
"homepage": "https://visionhammer.io",
|
|
78
86
|
"bugs": {
|
|
79
|
-
"url": "https://github.com/
|
|
87
|
+
"url": "https://github.com/VISIONHAMMER/deepdebug-local-agent/issues"
|
|
80
88
|
}
|
|
81
89
|
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/sync-fixtures.mjs — Phase 2 fixture sync (Plan 02-12 full implementation).
|
|
3
|
+
//
|
|
4
|
+
// For each requested stack, clones williambella/visionhammer-example-<stack>
|
|
5
|
+
// shallow (--depth 1, --branch main) into /tmp/sync-<stack>, copies samples/
|
|
6
|
+
// log files into test/fixtures/<adapter-dir>/ (with IBM MQ filename mapping),
|
|
7
|
+
// then deletes the temp clone. Idempotent: re-runs cleanly overwrite prior
|
|
8
|
+
// fixtures and rewipe /tmp scratch dirs.
|
|
9
|
+
//
|
|
10
|
+
// SECURITY (T-2-04 — PAT discipline):
|
|
11
|
+
// - PAT read ONCE from ~/.config/visionhammer/github-pat.
|
|
12
|
+
// - Embedded ONLY in `https://x-access-token:$PAT@github.com/...` URL form,
|
|
13
|
+
// passed as a `child_process.spawn` ARGV element (not a shell string), so
|
|
14
|
+
// no shell ever sees it. The argv carrying the URL IS visible to ps(1)
|
|
15
|
+
// during clone — that is the standard git-over-https risk window per AD-3.
|
|
16
|
+
// - Stdout/stderr are CAPTURED but only stack names / generic progress are
|
|
17
|
+
// written to console. Git's own stderr (which can contain the embedded
|
|
18
|
+
// URL on auth failure) is sanitised: every literal occurrence of the PAT
|
|
19
|
+
// is replaced with `***REDACTED***` before the buffer touches stderr.
|
|
20
|
+
// - The PAT NEVER goes into argv via a shell string, environment variable,
|
|
21
|
+
// log file, or thrown error message.
|
|
22
|
+
//
|
|
23
|
+
// SECURITY (T-2-05 — PAT scope assertion):
|
|
24
|
+
// - Before any clone, calls GitHub /user (with the PAT) to confirm the token
|
|
25
|
+
// authenticates. We do NOT enforce a specific scope here because the
|
|
26
|
+
// fine-grained-PAT scope vocabulary differs from classic-PAT scopes; a
|
|
27
|
+
// successful GET /user is sufficient to prove the token is valid and the
|
|
28
|
+
// token cannot exceed `repo`-equivalent scope per the project setup
|
|
29
|
+
// contract documented in docs/legacy-connectors.md.
|
|
30
|
+
//
|
|
31
|
+
// SECURITY (T-2-06 — fixture quarantine):
|
|
32
|
+
// - test/fixtures/*/ is .gitignore'd at the repo root (verified by Plan 02-01).
|
|
33
|
+
// The synced files are therefore quarantined: they NEVER enter version
|
|
34
|
+
// control of deepdebug-local-agent. AD-2 explicit submodule rejection.
|
|
35
|
+
|
|
36
|
+
import {
|
|
37
|
+
readFileSync,
|
|
38
|
+
mkdirSync,
|
|
39
|
+
rmSync,
|
|
40
|
+
readdirSync,
|
|
41
|
+
copyFileSync,
|
|
42
|
+
existsSync,
|
|
43
|
+
} from 'node:fs';
|
|
44
|
+
import { spawn } from 'node:child_process';
|
|
45
|
+
import { homedir } from 'node:os';
|
|
46
|
+
import { join, dirname, basename } from 'node:path';
|
|
47
|
+
import { fileURLToPath } from 'node:url';
|
|
48
|
+
|
|
49
|
+
const PAT_PATH = join(homedir(), '.config', 'visionhammer', 'github-pat');
|
|
50
|
+
const ALLOWED_STACKS = ['oracle-forms', 'plsql', 'sap-abap', 'cobol', 'ibm-mq'];
|
|
51
|
+
|
|
52
|
+
// Mapping: example-repo-stack name → local fixture directory name.
|
|
53
|
+
// Example repos use product names (sap-abap, ibm-mq); local fixtures use
|
|
54
|
+
// adapter names (sap, ibmmq). plsql has no Phase 2 adapter — fixtures are
|
|
55
|
+
// synced for Phase 3 catalog work.
|
|
56
|
+
const FIXTURE_DIR_MAP = Object.freeze({
|
|
57
|
+
'oracle-forms': 'oracle-forms',
|
|
58
|
+
'plsql': 'plsql',
|
|
59
|
+
'sap-abap': 'sap',
|
|
60
|
+
'cobol': 'cobol',
|
|
61
|
+
'ibm-mq': 'ibmmq',
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// IBM MQ filename mapping (per Plan 02-08 SUMMARY's filename-mismatch decision).
|
|
65
|
+
// The IbmMqLogAdapter only reads files matching `^AMQERR0[1-3]\.LOG$`. The
|
|
66
|
+
// remote example repo intentionally uses descriptive sample names; sync-time
|
|
67
|
+
// renaming makes the synced fixtures consumable by the adapter's contract.
|
|
68
|
+
// Order is stable so re-runs always produce the same target filenames.
|
|
69
|
+
const IBM_MQ_FILENAME_MAP = Object.freeze({
|
|
70
|
+
'ibm-mq-sample-mqrc-2033.log': 'AMQERR01.LOG',
|
|
71
|
+
'ibm-mq-sample-mqrc-2058.log': 'AMQERR02.LOG',
|
|
72
|
+
'ibm-mq-sample-resource-leak.log': 'AMQERR03.LOG',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
76
|
+
const REPO_ROOT = join(__dirname, '..');
|
|
77
|
+
|
|
78
|
+
function readPat() {
|
|
79
|
+
// SECURITY: do NOT log PAT_PATH contents on error or success. ENOENT = caller missing setup.
|
|
80
|
+
try {
|
|
81
|
+
return readFileSync(PAT_PATH, 'utf8').trim();
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (err.code === 'ENOENT') {
|
|
84
|
+
process.stderr.write(
|
|
85
|
+
'sync-fixtures: GitHub credentials not configured. See docs/legacy-connectors.md.\n',
|
|
86
|
+
);
|
|
87
|
+
process.exit(2);
|
|
88
|
+
}
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function parseArgs(argv) {
|
|
94
|
+
// --stacks=oracle-forms,plsql OR --stacks oracle-forms plsql OR default = all
|
|
95
|
+
const args = argv.slice(2);
|
|
96
|
+
const flagIdx = args.findIndex((a) => a === '--stacks' || a.startsWith('--stacks='));
|
|
97
|
+
if (flagIdx === -1) return ALLOWED_STACKS;
|
|
98
|
+
const flag = args[flagIdx];
|
|
99
|
+
const raw = flag.startsWith('--stacks=')
|
|
100
|
+
? flag.slice('--stacks='.length)
|
|
101
|
+
: (args[flagIdx + 1] || '');
|
|
102
|
+
const requested = raw.split(',').map((s) => s.trim()).filter(Boolean);
|
|
103
|
+
const unknown = requested.filter((s) => !ALLOWED_STACKS.includes(s));
|
|
104
|
+
if (unknown.length > 0) {
|
|
105
|
+
process.stderr.write(
|
|
106
|
+
`sync-fixtures: unknown stack(s): ${unknown.join(', ')}. Allowed: ${ALLOWED_STACKS.join(', ')}\n`,
|
|
107
|
+
);
|
|
108
|
+
process.exit(2);
|
|
109
|
+
}
|
|
110
|
+
return requested.length === 0 ? ALLOWED_STACKS : requested;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Run a child process; never log argv. Captures stdout/stderr buffers.
|
|
115
|
+
* @returns {Promise<{code:number, out:string, err:string}>}
|
|
116
|
+
*/
|
|
117
|
+
function run(cmd, args, cwd) {
|
|
118
|
+
return new Promise((resolve, reject) => {
|
|
119
|
+
const child = spawn(cmd, args, {
|
|
120
|
+
cwd,
|
|
121
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
122
|
+
});
|
|
123
|
+
let out = '';
|
|
124
|
+
let err = '';
|
|
125
|
+
child.stdout.on('data', (d) => { out += d.toString(); });
|
|
126
|
+
child.stderr.on('data', (d) => { err += d.toString(); });
|
|
127
|
+
child.on('close', (code) => resolve({ code, out, err }));
|
|
128
|
+
child.on('error', reject);
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* T-2-05: confirm the PAT authenticates against GitHub before any clone.
|
|
134
|
+
* On failure, exit 2 with a generic message — never echo the PAT or response.
|
|
135
|
+
*/
|
|
136
|
+
async function assertPatValid(pat) {
|
|
137
|
+
// node:fetch is built-in on Node 18+. Authorization header is the standard
|
|
138
|
+
// PAT delivery channel — never logged, never persisted.
|
|
139
|
+
const res = await fetch('https://api.github.com/user', {
|
|
140
|
+
headers: {
|
|
141
|
+
Authorization: `Bearer ${pat}`,
|
|
142
|
+
Accept: 'application/vnd.github+json',
|
|
143
|
+
'User-Agent': 'visionhammer-sync-fixtures',
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
// Do NOT print response body — GitHub may echo parts of the request URL.
|
|
148
|
+
process.stderr.write(
|
|
149
|
+
`sync-fixtures: PAT authentication failed (HTTP ${res.status}). Verify ~/.config/visionhammer/github-pat.\n`,
|
|
150
|
+
);
|
|
151
|
+
process.exit(2);
|
|
152
|
+
}
|
|
153
|
+
// We intentionally do NOT verify scope strings — fine-grained-PAT and
|
|
154
|
+
// classic-PAT report scopes differently. Validity is sufficient per AD-3.
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function syncStack(stack, pat) {
|
|
158
|
+
const tmpClone = `/tmp/sync-${stack}`;
|
|
159
|
+
const repoUrl = `https://x-access-token:${pat}@github.com/williambella/visionhammer-example-${stack}.git`;
|
|
160
|
+
const fixtureDir = FIXTURE_DIR_MAP[stack];
|
|
161
|
+
const dest = join(REPO_ROOT, 'test', 'fixtures', fixtureDir);
|
|
162
|
+
|
|
163
|
+
// Clean any prior temp clone (idempotent re-runs).
|
|
164
|
+
try { rmSync(tmpClone, { recursive: true, force: true }); } catch (_) { /* ok */ }
|
|
165
|
+
|
|
166
|
+
process.stdout.write(`sync-fixtures: cloning ${stack} ... `);
|
|
167
|
+
const clone = await run(
|
|
168
|
+
'git',
|
|
169
|
+
['clone', '--depth', '1', '--branch', 'main', repoUrl, tmpClone],
|
|
170
|
+
REPO_ROOT,
|
|
171
|
+
);
|
|
172
|
+
if (clone.code !== 0) {
|
|
173
|
+
process.stderr.write(
|
|
174
|
+
`\nsync-fixtures: git clone failed for ${stack} (exit ${clone.code}).\n`,
|
|
175
|
+
);
|
|
176
|
+
// Sanitize: replace any occurrence of the PAT with a redaction before
|
|
177
|
+
// letting git's stderr touch our stderr. Defence in depth — git rarely
|
|
178
|
+
// includes the credential portion of the URL but the redaction is cheap.
|
|
179
|
+
const sanitized = clone.err.split(pat).join('***REDACTED***');
|
|
180
|
+
process.stderr.write(sanitized + '\n');
|
|
181
|
+
return false;
|
|
182
|
+
}
|
|
183
|
+
process.stdout.write('cloned. ');
|
|
184
|
+
|
|
185
|
+
const srcSamples = join(tmpClone, 'samples');
|
|
186
|
+
if (!existsSync(srcSamples)) {
|
|
187
|
+
process.stderr.write(
|
|
188
|
+
`\nsync-fixtures: ${stack}: samples/ dir missing in repo, skipping copy.\n`,
|
|
189
|
+
);
|
|
190
|
+
try { rmSync(tmpClone, { recursive: true, force: true }); } catch (_) { /* ok */ }
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
mkdirSync(dest, { recursive: true });
|
|
195
|
+
const files = readdirSync(srcSamples).filter(
|
|
196
|
+
(f) => f.toLowerCase().endsWith('.log'),
|
|
197
|
+
);
|
|
198
|
+
let copied = 0;
|
|
199
|
+
for (const f of files) {
|
|
200
|
+
let targetName = f;
|
|
201
|
+
if (stack === 'ibm-mq') {
|
|
202
|
+
// Apply filename mapping (Plan 02-08 SUMMARY decision).
|
|
203
|
+
targetName = IBM_MQ_FILENAME_MAP[f] ?? null;
|
|
204
|
+
if (!targetName) {
|
|
205
|
+
// Sample without an explicit mapping is silently skipped — adapter
|
|
206
|
+
// would not pick it up anyway (filename anchor /^AMQERR0[1-3]\.LOG$/).
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
copyFileSync(join(srcSamples, f), join(dest, targetName));
|
|
211
|
+
copied++;
|
|
212
|
+
}
|
|
213
|
+
process.stdout.write(`copied ${copied} log(s) → test/fixtures/${fixtureDir}/. `);
|
|
214
|
+
|
|
215
|
+
// Cleanup temp clone.
|
|
216
|
+
try { rmSync(tmpClone, { recursive: true, force: true }); } catch (_) { /* ok */ }
|
|
217
|
+
process.stdout.write('done.\n');
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function main() {
|
|
222
|
+
const stacks = parseArgs(process.argv);
|
|
223
|
+
const pat = readPat();
|
|
224
|
+
await assertPatValid(pat); // T-2-05 gate
|
|
225
|
+
let failures = 0;
|
|
226
|
+
for (const s of stacks) {
|
|
227
|
+
const ok = await syncStack(s, pat);
|
|
228
|
+
if (!ok) failures++;
|
|
229
|
+
}
|
|
230
|
+
// PAT goes out of scope here — never persisted, never logged.
|
|
231
|
+
if (failures > 0) {
|
|
232
|
+
process.stderr.write(
|
|
233
|
+
`sync-fixtures: ${failures}/${stacks.length} stack(s) failed.\n`,
|
|
234
|
+
);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
process.stdout.write(`sync-fixtures: all ${stacks.length} stack(s) synced.\n`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
main().catch((err) => {
|
|
241
|
+
// err.message MUST not contain PAT. We never interpolate the PAT into thrown
|
|
242
|
+
// errors anywhere above; the only thrown paths are fs/network errors whose
|
|
243
|
+
// messages do not reference the credential.
|
|
244
|
+
process.stderr.write(`sync-fixtures: fatal: ${err.message}\n`);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
});
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Upgrade an installed DeepDebug client observability bundle so that:
|
|
3
|
+
# 1. Alloy also pushes the Docker log stream to VisionHammer
|
|
4
|
+
# (OTLP/HTTP JSON -> <gateway>/api/v1/webhooks/logs/otlp, Bearer <trigger secret>),
|
|
5
|
+
# which feeds Live logs and the services list;
|
|
6
|
+
# 2. Alertmanager sends the trigger secret as a Bearer header instead of ?s= in the URL.
|
|
7
|
+
#
|
|
8
|
+
# Safe to re-run (idempotent). Backs up both files, validates the new configs with the
|
|
9
|
+
# same images the stack runs, restarts only alloy + alertmanager, and rolls back on failure.
|
|
10
|
+
#
|
|
11
|
+
# Usage (on the server, as a user that can run docker):
|
|
12
|
+
# sudo bash upgrade-observability-push.sh # apply
|
|
13
|
+
# sudo bash upgrade-observability-push.sh --dry-run # show what would change
|
|
14
|
+
# Optional env:
|
|
15
|
+
# INSTALL_DIR=/path/to/deepdebug-observability (auto-detected otherwise)
|
|
16
|
+
# GATEWAY_URL=https://api-qa.visionhammer.io (read from alertmanager.yml otherwise)
|
|
17
|
+
set -euo pipefail
|
|
18
|
+
|
|
19
|
+
DRY_RUN=0
|
|
20
|
+
[[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1
|
|
21
|
+
|
|
22
|
+
log() { printf '\033[1;34m[upgrade]\033[0m %s\n' "$*"; }
|
|
23
|
+
warn() { printf '\033[1;33m[upgrade]\033[0m %s\n' "$*" >&2; }
|
|
24
|
+
die() { printf '\033[1;31m[upgrade]\033[0m %s\n' "$*" >&2; exit 1; }
|
|
25
|
+
mask() { local s="$1"; printf '%s...%s' "${s:0:8}" "${s: -4}"; }
|
|
26
|
+
|
|
27
|
+
command -v docker >/dev/null || die "docker not found"
|
|
28
|
+
docker compose version >/dev/null 2>&1 || die "docker compose plugin not found"
|
|
29
|
+
command -v curl >/dev/null || die "curl not found"
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------- locate the stack
|
|
32
|
+
find_install_dir() {
|
|
33
|
+
if [[ -n "${INSTALL_DIR:-}" ]]; then echo "$INSTALL_DIR"; return; fi
|
|
34
|
+
# 1) the running compose project
|
|
35
|
+
local cfg
|
|
36
|
+
cfg=$(docker compose ls --all --format json 2>/dev/null \
|
|
37
|
+
| tr '{' '\n' | grep '"Name":"deepdebug-observability"' \
|
|
38
|
+
| sed -n 's/.*"ConfigFiles":"\([^",]*\).*/\1/p' | head -1 || true)
|
|
39
|
+
if [[ -n "$cfg" && -f "$cfg" ]]; then dirname "$cfg"; return; fi
|
|
40
|
+
# 2) default provision paths
|
|
41
|
+
local d
|
|
42
|
+
for d in "$HOME/.deepdebug/observability/deepdebug-observability" \
|
|
43
|
+
/root/.deepdebug/observability/deepdebug-observability \
|
|
44
|
+
/home/*/.deepdebug/observability/deepdebug-observability; do
|
|
45
|
+
[[ -f "$d/docker-compose.yml" ]] && { echo "$d"; return; }
|
|
46
|
+
done
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
DIR=$(find_install_dir || true)
|
|
50
|
+
[[ -n "$DIR" && -f "$DIR/docker-compose.yml" ]] \
|
|
51
|
+
|| die "observability bundle not found; set INSTALL_DIR=/path/to/deepdebug-observability"
|
|
52
|
+
ALLOY="$DIR/alloy/config.alloy"
|
|
53
|
+
AM="$DIR/alertmanager/alertmanager.yml"
|
|
54
|
+
[[ -f "$ALLOY" ]] || die "missing $ALLOY"
|
|
55
|
+
[[ -f "$AM" ]] || die "missing $AM"
|
|
56
|
+
log "bundle: $DIR"
|
|
57
|
+
|
|
58
|
+
# ---------------------------------------------------------------- secret + gateway
|
|
59
|
+
SECRET=$(grep -oE 'vht_[A-Za-z0-9._-]+' "$AM" | head -1 || true)
|
|
60
|
+
[[ -n "$SECRET" ]] || die "no vht_ trigger secret found in $AM"
|
|
61
|
+
if [[ -z "${GATEWAY_URL:-}" ]]; then
|
|
62
|
+
GATEWAY_URL=$(grep -oE "https?://[^/'\" ]+" "$AM" | head -1 || true)
|
|
63
|
+
fi
|
|
64
|
+
GATEWAY_URL="${GATEWAY_URL%/}"
|
|
65
|
+
[[ "$GATEWAY_URL" =~ ^https?://[A-Za-z0-9.:-]+$ ]] || die "could not determine GATEWAY_URL (set it explicitly)"
|
|
66
|
+
log "gateway: $GATEWAY_URL secret: $(mask "$SECRET")"
|
|
67
|
+
case "$GATEWAY_URL" in
|
|
68
|
+
https://api-qa.visionhammer.io|https://api.visionhammer.io) ;;
|
|
69
|
+
*) warn "gateway is not api-qa/api.visionhammer.io; the push endpoint only exists on the current gateway." ;;
|
|
70
|
+
esac
|
|
71
|
+
|
|
72
|
+
# ---------------------------------------------------------------- 1. probe the endpoint
|
|
73
|
+
# An empty OTLP batch authenticates and registers the SELF_HOSTED log source without
|
|
74
|
+
# writing any log line.
|
|
75
|
+
code=$(curl -sS -o /tmp/vh-otlp-probe.$$ -w '%{http_code}' -m 20 \
|
|
76
|
+
-X POST "$GATEWAY_URL/api/v1/webhooks/logs/otlp" \
|
|
77
|
+
-H "Authorization: Bearer $SECRET" -H 'Content-Type: application/json' \
|
|
78
|
+
--data '{"resourceLogs":[]}' || echo 000)
|
|
79
|
+
case "$code" in
|
|
80
|
+
200) log "push endpoint OK (200)" ;;
|
|
81
|
+
401) die "push endpoint rejected the secret (401): the trigger source is disabled or the secret changed" ;;
|
|
82
|
+
404) die "push endpoint not deployed yet on $GATEWAY_URL (404); retry after the gateway deploy" ;;
|
|
83
|
+
*) die "push endpoint probe failed (HTTP $code): $(head -c 300 /tmp/vh-otlp-probe.$$ 2>/dev/null)" ;;
|
|
84
|
+
esac
|
|
85
|
+
rm -f /tmp/vh-otlp-probe.$$
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------- 2. build new configs
|
|
88
|
+
TMP=$(mktemp -d)
|
|
89
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
90
|
+
cp "$ALLOY" "$TMP/config.alloy"
|
|
91
|
+
cp "$AM" "$TMP/alertmanager.yml"
|
|
92
|
+
|
|
93
|
+
alloy_changed=0
|
|
94
|
+
if grep -q 'otelcol.exporter.otlphttp "visionhammer"' "$TMP/config.alloy"; then
|
|
95
|
+
log "alloy: push already configured"
|
|
96
|
+
else
|
|
97
|
+
# loki.process "default" must also forward to the OTLP receiver.
|
|
98
|
+
n=$(grep -c 'forward_to = \[loki.write.default.receiver\]' "$TMP/config.alloy" || true)
|
|
99
|
+
[[ "$n" == "1" ]] || die "alloy config does not look like the stock bundle (expected one 'forward_to = [loki.write.default.receiver]', found $n); edit $ALLOY by hand"
|
|
100
|
+
sed -i 's|forward_to = \[loki.write.default.receiver\]|forward_to = [loki.write.default.receiver, otelcol.receiver.loki.visionhammer.receiver]|' "$TMP/config.alloy"
|
|
101
|
+
cat >> "$TMP/config.alloy" <<EOF
|
|
102
|
+
|
|
103
|
+
// ---- push to VisionHammer: Loki entries -> OTLP logs (labels become attributes) ----
|
|
104
|
+
otelcol.receiver.loki "visionhammer" {
|
|
105
|
+
output {
|
|
106
|
+
logs = [otelcol.processor.batch.visionhammer.input]
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
otelcol.processor.batch "visionhammer" {
|
|
111
|
+
send_batch_size = 500
|
|
112
|
+
send_batch_max_size = 2000
|
|
113
|
+
timeout = "5s"
|
|
114
|
+
output {
|
|
115
|
+
logs = [otelcol.exporter.otlphttp.visionhammer.input]
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
otelcol.exporter.otlphttp "visionhammer" {
|
|
120
|
+
client {
|
|
121
|
+
endpoint = "$GATEWAY_URL"
|
|
122
|
+
compression = "gzip"
|
|
123
|
+
headers = { "Authorization" = "Bearer $SECRET" }
|
|
124
|
+
}
|
|
125
|
+
encoding = "json"
|
|
126
|
+
logs_endpoint = "$GATEWAY_URL/api/v1/webhooks/logs/otlp"
|
|
127
|
+
}
|
|
128
|
+
EOF
|
|
129
|
+
alloy_changed=1
|
|
130
|
+
log "alloy: push blocks added"
|
|
131
|
+
fi
|
|
132
|
+
|
|
133
|
+
am_changed=0
|
|
134
|
+
if grep -q 'webhooks/alertmanager?s=' "$TMP/alertmanager.yml"; then
|
|
135
|
+
# Drop ?s=... from the URL and add http_config.authorization at the same indent
|
|
136
|
+
# as the url key of that list item.
|
|
137
|
+
# Each receiver keeps ITS OWN secret (a bundle may post to QA and PROD gateways,
|
|
138
|
+
# each registered with a different vht_ secret).
|
|
139
|
+
awk '
|
|
140
|
+
/webhooks\/alertmanager\?s=/ {
|
|
141
|
+
match($0, /vht_[A-Za-z0-9._-]+/)
|
|
142
|
+
secret = substr($0, RSTART, RLENGTH)
|
|
143
|
+
line = $0
|
|
144
|
+
sub(/\?s=[^'"'"'" ]*/, "", line)
|
|
145
|
+
print line
|
|
146
|
+
match($0, /^[ ]*(- )?/)
|
|
147
|
+
ind = RLENGTH
|
|
148
|
+
pad = sprintf("%" ind "s", "")
|
|
149
|
+
print pad "http_config:"
|
|
150
|
+
print pad " authorization:"
|
|
151
|
+
print pad " type: Bearer"
|
|
152
|
+
print pad " credentials: '"'"'" secret "'"'"'"
|
|
153
|
+
next
|
|
154
|
+
}
|
|
155
|
+
{ print }
|
|
156
|
+
' "$TMP/alertmanager.yml" > "$TMP/alertmanager.yml.new"
|
|
157
|
+
mv "$TMP/alertmanager.yml.new" "$TMP/alertmanager.yml"
|
|
158
|
+
am_changed=1
|
|
159
|
+
log "alertmanager: secret moved from ?s= to Authorization header"
|
|
160
|
+
else
|
|
161
|
+
log "alertmanager: already uses header auth (or a custom config); left unchanged"
|
|
162
|
+
fi
|
|
163
|
+
|
|
164
|
+
if [[ $alloy_changed == 0 && $am_changed == 0 ]]; then
|
|
165
|
+
log "nothing to do"; exit 0
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
# Mask every vht_ secret in printed output, not only the first one.
|
|
169
|
+
mask_all() { sed -E 's/(vht_[A-Za-z0-9]{4})[A-Za-z0-9._-]*([A-Za-z0-9._-]{4})/\1...\2/g'; }
|
|
170
|
+
show_diff() { diff -u "$1" "$2" | mask_all || true; }
|
|
171
|
+
[[ $alloy_changed == 1 ]] && show_diff "$ALLOY" "$TMP/config.alloy"
|
|
172
|
+
[[ $am_changed == 1 ]] && show_diff "$AM" "$TMP/alertmanager.yml"
|
|
173
|
+
|
|
174
|
+
if [[ $DRY_RUN == 1 ]]; then log "dry run: nothing written"; exit 0; fi
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------- 3. validate with the stack's images
|
|
177
|
+
image_of() { grep -E "^\s*image:\s*.*$1" "$DIR/docker-compose.yml" | head -1 | awk '{print $2}'; }
|
|
178
|
+
ALLOY_IMG=$(image_of 'grafana/alloy'); ALLOY_IMG=${ALLOY_IMG:-grafana/alloy:v1.3.1}
|
|
179
|
+
AM_IMG=$(image_of 'prom/alertmanager'); AM_IMG=${AM_IMG:-prom/alertmanager:v0.27.0}
|
|
180
|
+
|
|
181
|
+
if [[ $alloy_changed == 1 ]]; then
|
|
182
|
+
docker run --rm -v "$TMP/config.alloy:/c/config.alloy:ro" "$ALLOY_IMG" fmt /c/config.alloy >/dev/null \
|
|
183
|
+
|| die "new alloy config failed validation; nothing changed"
|
|
184
|
+
log "alloy config valid ($ALLOY_IMG)"
|
|
185
|
+
fi
|
|
186
|
+
if [[ $am_changed == 1 ]]; then
|
|
187
|
+
docker run --rm --entrypoint amtool -v "$TMP/alertmanager.yml:/c/am.yml:ro" "$AM_IMG" check-config /c/am.yml >/dev/null \
|
|
188
|
+
|| die "new alertmanager config failed validation; nothing changed"
|
|
189
|
+
log "alertmanager config valid ($AM_IMG)"
|
|
190
|
+
fi
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------- 4. apply + restart
|
|
193
|
+
STAMP=$(date +%Y%m%d-%H%M%S)
|
|
194
|
+
cp -p "$ALLOY" "$ALLOY.bak-$STAMP"
|
|
195
|
+
cp -p "$AM" "$AM.bak-$STAMP"
|
|
196
|
+
log "backups: *.bak-$STAMP"
|
|
197
|
+
# Overwrite in place (keeps owner/permissions and the bind-mounted inode semantics on restart).
|
|
198
|
+
cat "$TMP/config.alloy" > "$ALLOY"
|
|
199
|
+
cat "$TMP/alertmanager.yml" > "$AM"
|
|
200
|
+
|
|
201
|
+
rollback() {
|
|
202
|
+
warn "rolling back to *.bak-$STAMP"
|
|
203
|
+
cat "$ALLOY.bak-$STAMP" > "$ALLOY"
|
|
204
|
+
cat "$AM.bak-$STAMP" > "$AM"
|
|
205
|
+
(cd "$DIR" && docker compose restart alloy alertmanager >/dev/null 2>&1) || true
|
|
206
|
+
die "$1"
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
(cd "$DIR" && docker compose restart alloy alertmanager) || rollback "restart failed"
|
|
210
|
+
|
|
211
|
+
# ---------------------------------------------------------------- 5. verify
|
|
212
|
+
log "waiting 30s for alloy to ship its first batch..."
|
|
213
|
+
sleep 30
|
|
214
|
+
running=$(cd "$DIR" && docker compose ps --status running --services 2>/dev/null || true)
|
|
215
|
+
grep -qx alloy <<<"$running" || rollback "alloy is not running after the restart"
|
|
216
|
+
grep -qx alertmanager <<<"$running" || rollback "alertmanager is not running after the restart"
|
|
217
|
+
|
|
218
|
+
errs=$(cd "$DIR" && docker compose logs --since 40s alloy 2>&1 \
|
|
219
|
+
| grep -iE 'level=error|Exporting failed|permanent error' | grep -i 'visionhammer\|otlphttp' || true)
|
|
220
|
+
if [[ -n "$errs" ]]; then
|
|
221
|
+
warn "alloy reported export errors:"
|
|
222
|
+
mask_all <<<"$errs" | tail -5 >&2
|
|
223
|
+
rollback "alloy cannot push to $GATEWAY_URL"
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
log "done. Logs should appear in VisionHammer > Live logs within ~1-2 minutes."
|
|
227
|
+
log "to undo: cp $ALLOY.bak-$STAMP $ALLOY; cp $AM.bak-$STAMP $AM; (cd $DIR && docker compose restart alloy alertmanager)"
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// src/adapters/as400/as400-parser.js
|
|
2
|
+
// OS/400 message ID prefixes: CPF (escape), MCH (machine check), SQL (DB2 for i)
|
|
3
|
+
// Anchored at start-of-line so arbitrary text containing the substring
|
|
4
|
+
// (e.g. "see CPF1234 docs") does not match — T-2-03-equivalent isolation surface.
|
|
5
|
+
export const AS400_MSGID_REGEX = /^(CPF|MCH|SQL)(\d{4})\b/;
|
|
6
|
+
|
|
7
|
+
export function extractMessageId(line) {
|
|
8
|
+
const m = line.match(AS400_MSGID_REGEX);
|
|
9
|
+
return m ? `${m[1]}${m[2]}` : null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const DSPLOG_COMMAND = 'system "DSPLOG OUTPUT(*PRINT) LOGTYPE(*MSG) MSGTYPE(*ESCAPE *NOTIFY)"';
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// src/adapters/as400/index.js
|
|
2
|
+
// AS/400 (IBM i) log adapter — ssh2 Client polls DSPLOG output.
|
|
3
|
+
// T-2-01 mitigation: pollInterval default 30000 ms (30 s — distinct from chokidar 5 s default per AD-3 + Constraints).
|
|
4
|
+
// T-2-02 mitigation: ssh2 keepaliveInterval=30 s + keepaliveCountMax=3; 'close' triggers reconnect with
|
|
5
|
+
// exponential backoff [1, 2, 4, 8, 16, 32, 60] s (capped at 60 s); backoff resets to base after a successful auth.
|
|
6
|
+
import { EventEmitter } from 'node:events';
|
|
7
|
+
import { Client as Ssh2Client } from 'ssh2';
|
|
8
|
+
import { extractMessageId, DSPLOG_COMMAND } from './as400-parser.js';
|
|
9
|
+
|
|
10
|
+
const ADAPTER = 'as400';
|
|
11
|
+
const DEFAULT_BACKOFF = [1000, 2000, 4000, 8000, 16000, 32000, 60000];
|
|
12
|
+
|
|
13
|
+
export class As400LogAdapter extends EventEmitter {
|
|
14
|
+
constructor(config = {}) {
|
|
15
|
+
super();
|
|
16
|
+
this.host = config.host || 'as400-prd-01';
|
|
17
|
+
this.port = config.port ?? 22;
|
|
18
|
+
this.username = config.username || 'TESTUSER01';
|
|
19
|
+
this.password = config.password;
|
|
20
|
+
this.privateKey = config.privateKey;
|
|
21
|
+
this.pollInterval = config.pollInterval ?? 30000;
|
|
22
|
+
this.backoffSchedule = config.backoffSchedule ?? DEFAULT_BACKOFF;
|
|
23
|
+
this._sshOpts = {
|
|
24
|
+
host: this.host,
|
|
25
|
+
port: this.port,
|
|
26
|
+
username: this.username,
|
|
27
|
+
...(this.password ? { password: this.password } : {}),
|
|
28
|
+
...(this.privateKey ? { privateKey: this.privateKey } : {}),
|
|
29
|
+
keepaliveInterval: 30000,
|
|
30
|
+
keepaliveCountMax: 3,
|
|
31
|
+
readyTimeout: 10000,
|
|
32
|
+
};
|
|
33
|
+
this._sshClientFactory = config.sshClientFactory || (() => new Ssh2Client());
|
|
34
|
+
this._client = null;
|
|
35
|
+
this._isRunning = false;
|
|
36
|
+
this._pollTimer = null;
|
|
37
|
+
this._backoffIdx = 0;
|
|
38
|
+
this._reconnectTimer = null;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
get isRunning() { return this._isRunning; }
|
|
42
|
+
|
|
43
|
+
async start() {
|
|
44
|
+
if (this._isRunning) return;
|
|
45
|
+
this._isRunning = true;
|
|
46
|
+
this._connect();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async stop() {
|
|
50
|
+
if (!this._isRunning) return;
|
|
51
|
+
this._isRunning = false;
|
|
52
|
+
if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; }
|
|
53
|
+
if (this._pollTimer) { clearInterval(this._pollTimer); this._pollTimer = null; }
|
|
54
|
+
if (this._client) {
|
|
55
|
+
try { this._client.end(); } catch { /* swallow */ }
|
|
56
|
+
this._client = null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
_connect() {
|
|
61
|
+
if (!this._isRunning) return;
|
|
62
|
+
const client = this._sshClientFactory();
|
|
63
|
+
this._client = client;
|
|
64
|
+
|
|
65
|
+
client.on('ready', () => {
|
|
66
|
+
this._backoffIdx = 0; // reset on successful auth
|
|
67
|
+
this._startPolling();
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
client.on('close', () => {
|
|
71
|
+
if (!this._isRunning) return;
|
|
72
|
+
if (this._pollTimer) { clearInterval(this._pollTimer); this._pollTimer = null; }
|
|
73
|
+
this._scheduleReconnect();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
client.on('error', () => {
|
|
77
|
+
// ssh2 surfaces auth/network errors as 'error' then 'close'.
|
|
78
|
+
// 'close' handler schedules reconnect; nothing to do here.
|
|
79
|
+
if (!this._isRunning) return;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
client.connect(this._sshOpts);
|
|
84
|
+
} catch {
|
|
85
|
+
this._scheduleReconnect();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
_scheduleReconnect() {
|
|
90
|
+
if (!this._isRunning) return;
|
|
91
|
+
const delay = this.backoffSchedule[Math.min(this._backoffIdx, this.backoffSchedule.length - 1)];
|
|
92
|
+
this._backoffIdx = Math.min(this._backoffIdx + 1, this.backoffSchedule.length - 1);
|
|
93
|
+
this._reconnectTimer = setTimeout(() => this._connect(), delay);
|
|
94
|
+
// Do not block process exit on a pending reconnect — `stop()` clears it explicitly.
|
|
95
|
+
this._reconnectTimer.unref?.();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_startPolling() {
|
|
99
|
+
const tick = () => {
|
|
100
|
+
if (!this._isRunning || !this._client) return;
|
|
101
|
+
this._client.exec(DSPLOG_COMMAND, (err, stream) => {
|
|
102
|
+
if (err) return; // close handler covers reconnect
|
|
103
|
+
let buf = '';
|
|
104
|
+
stream.on('data', (chunk) => { buf += chunk.toString('utf8'); });
|
|
105
|
+
stream.on('close', () => this._processOutput(buf));
|
|
106
|
+
stream.stderr?.on('data', () => { /* discard */ });
|
|
107
|
+
});
|
|
108
|
+
};
|
|
109
|
+
tick();
|
|
110
|
+
this._pollTimer = setInterval(tick, this.pollInterval);
|
|
111
|
+
// Do not block process exit on the poll cadence — `stop()` clears it explicitly.
|
|
112
|
+
this._pollTimer.unref?.();
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
_processOutput(text) {
|
|
116
|
+
const lines = text.split(/\r?\n/);
|
|
117
|
+
for (const line of lines) {
|
|
118
|
+
if (line.trim().length === 0) continue;
|
|
119
|
+
const code = extractMessageId(line);
|
|
120
|
+
if (!code) continue;
|
|
121
|
+
const event = Object.freeze({
|
|
122
|
+
adapter: ADAPTER,
|
|
123
|
+
errorCode: code,
|
|
124
|
+
file: `ssh://${this.host}/DSPLOG`,
|
|
125
|
+
message: line,
|
|
126
|
+
source: `ssh://${this.host}`,
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
});
|
|
129
|
+
this.emit('error', event);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export default As400LogAdapter;
|