deepdebug-local-agent 1.0.23 → 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/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/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 +9 -4
- package/scripts/upgrade-observability-push.sh +227 -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 +1366 -333
- 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/registry.test.js +9 -2
- 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
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* observability/provision.js
|
|
3
|
+
*
|
|
4
|
+
* Materialises the rendered bundle under a target dir and brings the stack up
|
|
5
|
+
* with `docker compose up -d`. Idempotent: re-running re-renders the files and
|
|
6
|
+
* `up -d` reconciles (no-op when already current).
|
|
7
|
+
*
|
|
8
|
+
* The step-by-step this performs is exactly the client runbook:
|
|
9
|
+
* 1. render the bundle for this tenant (gateway URL + trigger secret + env)
|
|
10
|
+
* 2. write files to <baseDir>/deepdebug-observability/
|
|
11
|
+
* 3. write .env (Grafana admin creds, static environment label)
|
|
12
|
+
* 4. docker compose up -d
|
|
13
|
+
* 5. report container status + the (loopback) Grafana URL
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { exec } from 'child_process';
|
|
17
|
+
import { promisify } from 'util';
|
|
18
|
+
import fs from 'fs';
|
|
19
|
+
import path from 'path';
|
|
20
|
+
import os from 'os';
|
|
21
|
+
import crypto from 'crypto';
|
|
22
|
+
import { renderBundle, COMPOSE_PROJECT } from './bundle-files.js';
|
|
23
|
+
import { dockerAvailable } from './detect.js';
|
|
24
|
+
|
|
25
|
+
const execAsync = promisify(exec);
|
|
26
|
+
|
|
27
|
+
/** Default install root: ~/.deepdebug/observability */
|
|
28
|
+
export function defaultBaseDir() {
|
|
29
|
+
return path.join(os.homedir(), '.deepdebug', 'observability');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Write the rendered file map under installDir, creating parent dirs. */
|
|
33
|
+
export function writeBundle(installDir, files) {
|
|
34
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
35
|
+
const abs = path.join(installDir, rel);
|
|
36
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
37
|
+
fs.writeFileSync(abs, content, 'utf8');
|
|
38
|
+
}
|
|
39
|
+
return installDir;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Provision the bundle.
|
|
44
|
+
*
|
|
45
|
+
* @param {object} opts
|
|
46
|
+
* @param {string} opts.gatewayUrl e.g. https://api.visionhammer.io
|
|
47
|
+
* @param {string} opts.triggerSecret tenant alertmanager trigger secret (vht_...)
|
|
48
|
+
* @param {string} [opts.environment] static env label (default 'production')
|
|
49
|
+
* @param {string} [opts.grafanaUser] Grafana admin user (default 'admin')
|
|
50
|
+
* @param {string} [opts.grafanaPassword] Grafana admin password (default generated)
|
|
51
|
+
* @param {string} [opts.baseDir] install root (default ~/.deepdebug/observability)
|
|
52
|
+
* @param {boolean}[opts.start=true] run `docker compose up -d`
|
|
53
|
+
*/
|
|
54
|
+
export async function provisionObservability(opts = {}) {
|
|
55
|
+
const {
|
|
56
|
+
gatewayUrl,
|
|
57
|
+
triggerSecret,
|
|
58
|
+
environment = 'production',
|
|
59
|
+
grafanaUser = 'admin',
|
|
60
|
+
grafanaPassword,
|
|
61
|
+
baseDir = defaultBaseDir(),
|
|
62
|
+
start = true,
|
|
63
|
+
} = opts;
|
|
64
|
+
|
|
65
|
+
if (!gatewayUrl || !triggerSecret) {
|
|
66
|
+
return { ok: false, error: 'gatewayUrl and triggerSecret are required' };
|
|
67
|
+
}
|
|
68
|
+
const invalid = validateProvisionInput({ gatewayUrl, triggerSecret, environment, grafanaUser, grafanaPassword });
|
|
69
|
+
if (invalid) {
|
|
70
|
+
return { ok: false, error: invalid };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const avail = await dockerAvailable();
|
|
74
|
+
if (!avail.docker || !avail.compose) {
|
|
75
|
+
return {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: 'docker or the compose plugin is not available on this host',
|
|
78
|
+
docker: avail,
|
|
79
|
+
hint: 'install Docker Engine + the compose plugin, then retry',
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const installDir = path.join(baseDir, COMPOSE_PROJECT);
|
|
84
|
+
const files = renderBundle({ gatewayUrl, triggerSecret, environment });
|
|
85
|
+
writeBundle(installDir, files);
|
|
86
|
+
|
|
87
|
+
// Grafana admin password: caller-supplied, else a random one we report back.
|
|
88
|
+
const pw = grafanaPassword || randomPassword();
|
|
89
|
+
const envFile = [
|
|
90
|
+
`GF_ADMIN_USER=${grafanaUser}`,
|
|
91
|
+
`GF_ADMIN_PASSWORD=${pw}`,
|
|
92
|
+
`DDO_ENVIRONMENT=${environment}`,
|
|
93
|
+
'',
|
|
94
|
+
].join('\n');
|
|
95
|
+
fs.writeFileSync(path.join(installDir, '.env'), envFile, 'utf8');
|
|
96
|
+
|
|
97
|
+
const result = {
|
|
98
|
+
ok: true,
|
|
99
|
+
installDir,
|
|
100
|
+
project: COMPOSE_PROJECT,
|
|
101
|
+
environment,
|
|
102
|
+
filesWritten: Object.keys(files).length,
|
|
103
|
+
grafana: { url: 'http://127.0.0.1:3000', user: grafanaUser, password: pw },
|
|
104
|
+
started: false,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
if (!start) return result;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const { stdout, stderr } = await execAsync(
|
|
111
|
+
'docker compose up -d --remove-orphans',
|
|
112
|
+
{ cwd: installDir, timeout: 300000 }
|
|
113
|
+
);
|
|
114
|
+
result.started = true;
|
|
115
|
+
result.composeOutput = (stderr || stdout || '').trim().split('\n').slice(-12).join('\n');
|
|
116
|
+
result.status = await composeStatus(installDir);
|
|
117
|
+
} catch (e) {
|
|
118
|
+
result.ok = false;
|
|
119
|
+
result.started = false;
|
|
120
|
+
result.error = `docker compose up failed: ${e.message}`;
|
|
121
|
+
result.composeOutput = (e.stderr || e.stdout || '').toString().trim().split('\n').slice(-12).join('\n');
|
|
122
|
+
}
|
|
123
|
+
return result;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** `docker compose ps` for the installed project, as structured rows. */
|
|
127
|
+
export async function composeStatus(installDir) {
|
|
128
|
+
try {
|
|
129
|
+
const { stdout } = await execAsync('docker compose ps --format json', {
|
|
130
|
+
cwd: installDir,
|
|
131
|
+
timeout: 30000,
|
|
132
|
+
});
|
|
133
|
+
// compose emits either a JSON array or one JSON object per line.
|
|
134
|
+
const rows = [];
|
|
135
|
+
const trimmed = stdout.trim();
|
|
136
|
+
if (trimmed.startsWith('[')) {
|
|
137
|
+
for (const r of JSON.parse(trimmed)) rows.push(pickRow(r));
|
|
138
|
+
} else {
|
|
139
|
+
for (const line of trimmed.split('\n').filter(Boolean)) {
|
|
140
|
+
try { rows.push(pickRow(JSON.parse(line))); } catch { /* skip */ }
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return rows;
|
|
144
|
+
} catch (e) {
|
|
145
|
+
return { error: e.message };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Tear the stack down (optionally removing data volumes). */
|
|
150
|
+
export async function teardownObservability({ baseDir = defaultBaseDir(), purge = false } = {}) {
|
|
151
|
+
const installDir = path.join(baseDir, COMPOSE_PROJECT);
|
|
152
|
+
if (!fs.existsSync(path.join(installDir, 'docker-compose.yml'))) {
|
|
153
|
+
return { ok: true, note: 'nothing installed at ' + installDir };
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const cmd = purge ? 'docker compose down -v' : 'docker compose down';
|
|
157
|
+
const { stdout, stderr } = await execAsync(cmd, { cwd: installDir, timeout: 120000 });
|
|
158
|
+
return { ok: true, purged: purge, output: (stderr || stdout || '').trim() };
|
|
159
|
+
} catch (e) {
|
|
160
|
+
return { ok: false, error: e.message };
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function pickRow(r) {
|
|
165
|
+
return {
|
|
166
|
+
name: r.Name || r.name,
|
|
167
|
+
service: r.Service || r.service,
|
|
168
|
+
state: r.State || r.state,
|
|
169
|
+
status: r.Status || r.status,
|
|
170
|
+
health: r.Health || r.health,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function randomPassword() {
|
|
175
|
+
// Reported back once; the operator may rotate it in Grafana.
|
|
176
|
+
return 'ddo-' + crypto.randomBytes(18).toString('base64url');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const SAFE_TOKEN = /^[A-Za-z0-9._~-]+$/;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Every value below is written verbatim into YAML, Alloy and .env files, so a quote
|
|
183
|
+
* or newline would let a caller rewrite the config. Returns an error message or null.
|
|
184
|
+
*/
|
|
185
|
+
export function validateProvisionInput({ gatewayUrl, triggerSecret, environment, grafanaUser, grafanaPassword }) {
|
|
186
|
+
let url;
|
|
187
|
+
try {
|
|
188
|
+
url = new URL(String(gatewayUrl));
|
|
189
|
+
} catch {
|
|
190
|
+
return 'gatewayUrl is not a URL';
|
|
191
|
+
}
|
|
192
|
+
const local = url.hostname === 'localhost' || url.hostname === '127.0.0.1' || url.hostname === 'host.docker.internal';
|
|
193
|
+
if (url.protocol !== 'https:' && !(local && url.protocol === 'http:')) {
|
|
194
|
+
return 'gatewayUrl must be https';
|
|
195
|
+
}
|
|
196
|
+
if (url.search || url.hash || url.username || url.password || !/^[A-Za-z0-9.:\/_-]+$/.test(String(gatewayUrl))) {
|
|
197
|
+
return 'gatewayUrl must be a plain origin (no query, credentials or special characters)';
|
|
198
|
+
}
|
|
199
|
+
if (!/^vht_[A-Za-z0-9._-]+$/.test(String(triggerSecret))) {
|
|
200
|
+
return 'triggerSecret must be a vht_ trigger secret';
|
|
201
|
+
}
|
|
202
|
+
if (environment != null && !/^[a-z0-9_-]{1,32}$/.test(String(environment))) {
|
|
203
|
+
return 'environment must match [a-z0-9_-]{1,32}';
|
|
204
|
+
}
|
|
205
|
+
if (grafanaUser != null && !SAFE_TOKEN.test(String(grafanaUser))) {
|
|
206
|
+
return 'grafanaUser contains unsupported characters';
|
|
207
|
+
}
|
|
208
|
+
if (grafanaPassword != null && grafanaPassword !== '' && !SAFE_TOKEN.test(String(grafanaPassword))) {
|
|
209
|
+
return 'grafanaPassword contains unsupported characters';
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Write the client observability bundle to a directory, without starting it.
|
|
4
|
+
*
|
|
5
|
+
* Used by the self-hosted installer (gateway /api/v1/self-hosted/install.sh), which
|
|
6
|
+
* runs it inside the public agent image so the templates have one source of truth:
|
|
7
|
+
*
|
|
8
|
+
* docker run --rm --user 0 --entrypoint node \
|
|
9
|
+
* -e VH_GATEWAY_URL -e VH_TRIGGER_SECRET -e VH_ENVIRONMENT \
|
|
10
|
+
* -v /opt/visionhammer/observability:/out <image> /app/src/observability/render-cli.js /out
|
|
11
|
+
*
|
|
12
|
+
* The secret comes from the environment, never argv (argv is visible in `ps`).
|
|
13
|
+
* Prints a JSON summary (including the generated Grafana password) to stdout.
|
|
14
|
+
*/
|
|
15
|
+
import fs from 'fs';
|
|
16
|
+
import path from 'path';
|
|
17
|
+
import crypto from 'crypto';
|
|
18
|
+
import { renderBundle, COMPOSE_PROJECT } from './bundle-files.js';
|
|
19
|
+
import { validateProvisionInput } from './provision.js';
|
|
20
|
+
|
|
21
|
+
function main() {
|
|
22
|
+
const outRoot = process.argv[2];
|
|
23
|
+
if (!outRoot) {
|
|
24
|
+
console.error('usage: render-cli.js <out-dir> (env: VH_GATEWAY_URL, VH_TRIGGER_SECRET, VH_ENVIRONMENT)');
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
const gatewayUrl = process.env.VH_GATEWAY_URL;
|
|
28
|
+
const triggerSecret = process.env.VH_TRIGGER_SECRET;
|
|
29
|
+
const environment = process.env.VH_ENVIRONMENT || 'production';
|
|
30
|
+
const invalid = validateProvisionInput({ gatewayUrl, triggerSecret, environment });
|
|
31
|
+
if (invalid) {
|
|
32
|
+
console.error(`render-cli: ${invalid}`);
|
|
33
|
+
process.exit(2);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const dir = path.join(outRoot, COMPOSE_PROJECT);
|
|
37
|
+
const files = renderBundle({ gatewayUrl, triggerSecret, environment });
|
|
38
|
+
for (const [rel, content] of Object.entries(files)) {
|
|
39
|
+
const target = path.join(dir, rel);
|
|
40
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
41
|
+
fs.writeFileSync(target, content, 'utf8');
|
|
42
|
+
}
|
|
43
|
+
const grafanaPassword = 'ddo-' + crypto.randomBytes(18).toString('base64url');
|
|
44
|
+
fs.writeFileSync(path.join(dir, '.env'),
|
|
45
|
+
`GF_ADMIN_USER=admin\nGF_ADMIN_PASSWORD=${grafanaPassword}\nDDO_ENVIRONMENT=${environment}\n`,
|
|
46
|
+
{ encoding: 'utf8', mode: 0o600 });
|
|
47
|
+
// The bundle holds the trigger secret: keep it root-only on the host.
|
|
48
|
+
fs.chmodSync(dir, 0o700);
|
|
49
|
+
|
|
50
|
+
process.stdout.write(JSON.stringify({
|
|
51
|
+
ok: true,
|
|
52
|
+
dir,
|
|
53
|
+
project: COMPOSE_PROJECT,
|
|
54
|
+
files: Object.keys(files).length,
|
|
55
|
+
grafana: { url: 'http://127.0.0.1:3000', user: 'admin', password: grafanaPassword },
|
|
56
|
+
}) + '\n');
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
main();
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* observability/routes.js
|
|
3
|
+
*
|
|
4
|
+
* Local-agent HTTP surface for the "zero observability -> provision" flow the
|
|
5
|
+
* gateway drives during onboarding.
|
|
6
|
+
*
|
|
7
|
+
* GET /observability/status probe the host; classify; recommend
|
|
8
|
+
* POST /observability/provision apply the bundle (guarded: skips if the
|
|
9
|
+
* client already has a stack, unless force)
|
|
10
|
+
* POST /observability/teardown docker compose down (optionally -v)
|
|
11
|
+
*
|
|
12
|
+
* Mount in server.js with:
|
|
13
|
+
* import { registerObservabilityRoutes } from './observability/routes.js';
|
|
14
|
+
* registerObservabilityRoutes(app);
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { detectObservability } from './detect.js';
|
|
18
|
+
import { provisionObservability, teardownObservability, validateProvisionInput } from './provision.js';
|
|
19
|
+
|
|
20
|
+
export function registerObservabilityRoutes(app) {
|
|
21
|
+
// ---- GET /observability/status ----
|
|
22
|
+
app.get('/observability/status', async (_req, res) => {
|
|
23
|
+
try {
|
|
24
|
+
const probe = await detectObservability();
|
|
25
|
+
res.json({ ok: true, ...probe });
|
|
26
|
+
} catch (e) {
|
|
27
|
+
console.error('[Observability] status failed:', e.message);
|
|
28
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ---- POST /observability/provision ----
|
|
33
|
+
// Body: { gatewayUrl, triggerSecret, environment?, grafanaUser?,
|
|
34
|
+
// grafanaPassword?, force?, start? }
|
|
35
|
+
app.post('/observability/provision', async (req, res) => {
|
|
36
|
+
const {
|
|
37
|
+
gatewayUrl, triggerSecret, environment,
|
|
38
|
+
grafanaUser, grafanaPassword, force = false, start = true,
|
|
39
|
+
} = req.body || {};
|
|
40
|
+
|
|
41
|
+
if (!gatewayUrl || !triggerSecret) {
|
|
42
|
+
return res.status(400).json({ ok: false, error: 'gatewayUrl and triggerSecret are required' });
|
|
43
|
+
}
|
|
44
|
+
const invalid = validateProvisionInput({ gatewayUrl, triggerSecret, environment, grafanaUser, grafanaPassword });
|
|
45
|
+
if (invalid) {
|
|
46
|
+
return res.status(400).json({ ok: false, error: invalid });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
// Guard: never deploy on top of an existing stack unless explicitly forced.
|
|
51
|
+
// Our own already-running stack does NOT block (re-provision is idempotent).
|
|
52
|
+
const probe = await detectObservability();
|
|
53
|
+
if (probe.hasObservability && !probe.ownStackRunning && !force) {
|
|
54
|
+
return res.status(409).json({
|
|
55
|
+
ok: false,
|
|
56
|
+
skipped: true,
|
|
57
|
+
reason: 'client already has observability',
|
|
58
|
+
found: probe.found,
|
|
59
|
+
hint: 'pass force:true to provision anyway',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
console.log(`[Observability] Provisioning bundle (env=${environment || 'production'}, force=${force})`);
|
|
64
|
+
const result = await provisionObservability({
|
|
65
|
+
gatewayUrl, triggerSecret, environment, grafanaUser, grafanaPassword, start,
|
|
66
|
+
});
|
|
67
|
+
result.detectedBefore = { found: probe.found, hadObservability: probe.hasObservability };
|
|
68
|
+
return res.status(result.ok ? 200 : 500).json(result);
|
|
69
|
+
} catch (e) {
|
|
70
|
+
console.error('[Observability] provision failed:', e.message);
|
|
71
|
+
return res.status(500).json({ ok: false, error: e.message });
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// ---- POST /observability/teardown ----
|
|
76
|
+
app.post('/observability/teardown', async (req, res) => {
|
|
77
|
+
const { purge = false } = req.body || {};
|
|
78
|
+
try {
|
|
79
|
+
const result = await teardownObservability({ purge });
|
|
80
|
+
res.status(result.ok ? 200 : 500).json(result);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
console.error('[Observability] teardown failed:', e.message);
|
|
83
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
console.log('[Observability] routes registered: /observability/{status,provision,teardown}');
|
|
88
|
+
}
|
|
@@ -4,12 +4,12 @@ import { readFile, writeFile, exists, ensureDir, copyFile } from '../utils/fs-ut
|
|
|
4
4
|
/**
|
|
5
5
|
* PatchEngine
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Patch application system with backup and rollback.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
* @param {string} diffText -
|
|
11
|
+
* Parses a unified diff
|
|
12
|
+
* @param {string} diffText - Diff text
|
|
13
13
|
* @returns {Patch[]}
|
|
14
14
|
*/
|
|
15
15
|
export function parsePatch(diffText) {
|
|
@@ -22,7 +22,7 @@ export function parsePatch(diffText) {
|
|
|
22
22
|
for (let i = 0; i < lines.length; i++) {
|
|
23
23
|
const line = lines[i];
|
|
24
24
|
|
|
25
|
-
//
|
|
25
|
+
// Start of a new file
|
|
26
26
|
if (line.startsWith('---')) {
|
|
27
27
|
if (currentPatch) {
|
|
28
28
|
patches.push(currentPatch);
|
|
@@ -35,12 +35,12 @@ export function parsePatch(diffText) {
|
|
|
35
35
|
};
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
-
//
|
|
38
|
+
// Name of the new file
|
|
39
39
|
else if (line.startsWith('+++') && currentPatch) {
|
|
40
40
|
currentPatch.newFileName = extractFileName(line);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
//
|
|
43
|
+
// Start of a hunk
|
|
44
44
|
else if (line.startsWith('@@') && currentPatch) {
|
|
45
45
|
const hunkMatch = line.match(/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@/);
|
|
46
46
|
|
|
@@ -56,7 +56,7 @@ export function parsePatch(diffText) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
//
|
|
59
|
+
// Content line of the hunk
|
|
60
60
|
else if (currentHunk && (line.startsWith(' ') || line.startsWith('+') || line.startsWith('-'))) {
|
|
61
61
|
currentHunk.lines.push(line);
|
|
62
62
|
}
|
|
@@ -70,16 +70,16 @@ export function parsePatch(diffText) {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* Extracts the file name from the --- or +++ line
|
|
74
74
|
*/
|
|
75
75
|
function extractFileName(line) {
|
|
76
76
|
return line.replace(/^[-+]{3}\s+[ab]\//, '').replace(/^[-+]{3}\s+/, '');
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
*
|
|
81
|
-
* @param {string} original -
|
|
82
|
-
* @param {Patch} patch - Patch
|
|
80
|
+
* Applies a patch to a file
|
|
81
|
+
* @param {string} original - Original content
|
|
82
|
+
* @param {Patch} patch - Patch to apply
|
|
83
83
|
* @returns {string|false}
|
|
84
84
|
*/
|
|
85
85
|
export function applyPatch(original, patch) {
|
|
@@ -94,7 +94,7 @@ export function applyPatch(original, patch) {
|
|
|
94
94
|
let removedCount = 0;
|
|
95
95
|
let addedCount = 0;
|
|
96
96
|
|
|
97
|
-
//
|
|
97
|
+
// Check the context
|
|
98
98
|
let contextMatches = true;
|
|
99
99
|
let checkLine = startLine;
|
|
100
100
|
|
|
@@ -118,7 +118,7 @@ export function applyPatch(original, patch) {
|
|
|
118
118
|
return false;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
//
|
|
121
|
+
// Apply the hunk
|
|
122
122
|
const linesToRemove = [];
|
|
123
123
|
const linesToAdd = [];
|
|
124
124
|
|
|
@@ -138,12 +138,12 @@ export function applyPatch(original, patch) {
|
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
//
|
|
141
|
+
// Remove lines (back to front)
|
|
142
142
|
for (let i = linesToRemove.length - 1; i >= 0; i--) {
|
|
143
143
|
resultLines.splice(linesToRemove[i], 1);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
//
|
|
146
|
+
// Add lines
|
|
147
147
|
for (let i = 0; i < linesToAdd.length; i++) {
|
|
148
148
|
const { position, content } = linesToAdd[i];
|
|
149
149
|
const adjustedPosition = position - removedCount + i;
|
|
@@ -157,9 +157,9 @@ export function applyPatch(original, patch) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
/**
|
|
160
|
-
*
|
|
161
|
-
* @param {string} root -
|
|
162
|
-
* @param {string} diffText -
|
|
160
|
+
* Applies a unified diff to a workspace
|
|
161
|
+
* @param {string} root - Workspace root
|
|
162
|
+
* @param {string} diffText - Diff text
|
|
163
163
|
* @returns {Promise<PatchResult>}
|
|
164
164
|
*/
|
|
165
165
|
export async function applyUnifiedDiff(root, diffText) {
|
|
@@ -231,10 +231,10 @@ export async function applyUnifiedDiff(root, diffText) {
|
|
|
231
231
|
}
|
|
232
232
|
|
|
233
233
|
/**
|
|
234
|
-
*
|
|
235
|
-
* @param {string} root -
|
|
236
|
-
* @param {string[]} files -
|
|
237
|
-
* @param {string} backupId - ID
|
|
234
|
+
* Creates a backup of files before applying a patch
|
|
235
|
+
* @param {string} root - Workspace root
|
|
236
|
+
* @param {string[]} files - List of files
|
|
237
|
+
* @param {string} backupId - Backup ID
|
|
238
238
|
* @returns {Promise<BackupResult>}
|
|
239
239
|
*/
|
|
240
240
|
export async function createBackup(root, files, backupId) {
|
|
@@ -254,7 +254,7 @@ export async function createBackup(root, files, backupId) {
|
|
|
254
254
|
}
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
-
//
|
|
257
|
+
// Create the manifest
|
|
258
258
|
const manifest = {
|
|
259
259
|
id: backupId,
|
|
260
260
|
timestamp: new Date().toISOString(),
|
|
@@ -275,9 +275,9 @@ export async function createBackup(root, files, backupId) {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
/**
|
|
278
|
-
*
|
|
279
|
-
* @param {string} root -
|
|
280
|
-
* @param {string} backupId - ID
|
|
278
|
+
* Restores a backup
|
|
279
|
+
* @param {string} root - Workspace root
|
|
280
|
+
* @param {string} backupId - Backup ID
|
|
281
281
|
* @returns {Promise<RestoreResult>}
|
|
282
282
|
*/
|
|
283
283
|
export async function restoreBackup(root, backupId) {
|
|
@@ -309,8 +309,8 @@ export async function restoreBackup(root, backupId) {
|
|
|
309
309
|
}
|
|
310
310
|
|
|
311
311
|
/**
|
|
312
|
-
*
|
|
313
|
-
* @param {string} diffText -
|
|
312
|
+
* Extracts the files affected by a diff
|
|
313
|
+
* @param {string} diffText - Diff text
|
|
314
314
|
* @returns {string[]}
|
|
315
315
|
*/
|
|
316
316
|
export function extractAffectedFiles(diffText) {
|