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.
Files changed (168) hide show
  1. package/.claude/context.md +76 -0
  2. package/.claude/skills/README.md +25 -0
  3. package/.claude/skills/tdd-debugging/SKILL.md +53 -0
  4. package/.claude/skills/tdd-debugging/references/anti-patterns.md +43 -0
  5. package/.claude/skills/tdd-debugging/references/command-reference.md +46 -0
  6. package/.claude/standards/README.md +51 -0
  7. package/.claude/standards/clean-code.md +75 -0
  8. package/.claude/standards/code-review-checklist.md +61 -0
  9. package/.claude/standards/layered-architecture.md +163 -0
  10. package/.claude/standards/node.md +45 -0
  11. package/.claude/standards/security-owasp.md +114 -0
  12. package/.claude/standards/simplicity.md +42 -0
  13. package/.claude/standards/solid.md +64 -0
  14. package/.claude/standards/testing.md +59 -0
  15. package/.cursor/rules/standards.mdc +17 -0
  16. package/.github/workflows/build-byoc-image.yml +315 -0
  17. package/.github/workflows/deploy-gcp.yml +102 -0
  18. package/.github/workflows/npm-publish.yml +69 -8
  19. package/AGENTS.md +21 -0
  20. package/CHANGELOG.md +69 -0
  21. package/CLAUDE.md +13 -0
  22. package/CLAUDE_CODE_AGENT_INFINITE.md +5 -3
  23. package/Dockerfile +4 -0
  24. package/Dockerfile.full +55 -3
  25. package/bin/install.js +16 -5
  26. package/cloudbuild-full.yaml +57 -0
  27. package/docker-compose.yml +5 -3
  28. package/docs/OBSERVABILITY.md +164 -0
  29. package/docs/byoc-agent-image.md +70 -0
  30. package/docs/graphify-packaging-plan.md +133 -0
  31. package/docs/graphify-validation-run-2026-10-28.md +243 -0
  32. package/docs/releasing.md +43 -0
  33. package/docs/signed-commands.md +163 -0
  34. package/env.example +30 -0
  35. package/helm/Chart.yaml +2 -2
  36. package/helm/templates/deployment.yaml +13 -3
  37. package/helm/templates/networkpolicy.yaml +22 -0
  38. package/helm/templates/secret.yaml +1 -0
  39. package/helm/values.yaml +13 -14
  40. package/mcp-server.js +4 -1
  41. package/package.json +9 -4
  42. package/scripts/upgrade-observability-push.sh +227 -0
  43. package/src/ai-engine.js +22 -22
  44. package/src/config/trusted-signing-keys.json +3 -0
  45. package/src/detectors/build-tool-detector.js +50 -0
  46. package/src/detectors/multi-project-detector.js +22 -22
  47. package/src/detectors/service-detector.js +24 -24
  48. package/src/exec-utils.js +26 -4
  49. package/src/git/base-git-provider.js +117 -117
  50. package/src/git/bitbucket-provider.js +2 -2
  51. package/src/git/git-provider-registry.js +18 -18
  52. package/src/git/github-provider.js +13 -13
  53. package/src/git/multi-repo-pr.js +161 -0
  54. package/src/mcp-http-server.js +32 -16
  55. package/src/observability/bundle-files.js +418 -0
  56. package/src/observability/detect.js +151 -0
  57. package/src/observability/provision.js +212 -0
  58. package/src/observability/render-cli.js +59 -0
  59. package/src/observability/routes.js +88 -0
  60. package/src/patch/patch-engine.js +28 -28
  61. package/src/patch-manager.js +85 -85
  62. package/src/patch_bkp.js +14 -14
  63. package/src/ports.js +4 -4
  64. package/src/routes/workspace.route.js +47 -16
  65. package/src/runtimes/base-runtime.js +75 -75
  66. package/src/runtimes/java/gradle-runtime.js +17 -17
  67. package/src/runtimes/java/java-integrations.js +25 -25
  68. package/src/runtimes/java/maven-runtime.js +34 -34
  69. package/src/runtimes/node/node-integrations.js +1 -1
  70. package/src/runtimes/node/npm-runtime.js +14 -14
  71. package/src/runtimes/node/yarn-runtime.js +4 -4
  72. package/src/runtimes/runtime-registry.js +25 -25
  73. package/src/server-local.js +4 -4
  74. package/src/server.js +1366 -333
  75. package/src/siblings/burst-suppressor.js +89 -0
  76. package/src/siblings/capabilities.js +273 -0
  77. package/src/siblings/env-compat.cjs +58 -0
  78. package/src/siblings/incident-forwarder.js +320 -0
  79. package/src/siblings/logger.js +15 -0
  80. package/src/siblings/metrics.js +94 -0
  81. package/src/utils/agent-auth.js +184 -0
  82. package/src/utils/approval-token.js +56 -0
  83. package/src/utils/bind-host.js +29 -0
  84. package/src/utils/branch-refspec.js +98 -0
  85. package/src/utils/child-env.js +34 -0
  86. package/src/utils/environment-diagnostics.js +55 -55
  87. package/src/utils/exec-policy.js +962 -0
  88. package/src/utils/exec-utils.js +15 -15
  89. package/src/utils/file-count.js +46 -0
  90. package/src/utils/fs-utils.js +10 -10
  91. package/src/utils/multi-repo-open.js +132 -0
  92. package/src/utils/open-gate.js +183 -0
  93. package/src/utils/ranged-read.js +65 -0
  94. package/src/utils/request-guards.js +77 -0
  95. package/src/utils/request-signing.js +342 -0
  96. package/src/utils/root-recovery.js +244 -0
  97. package/src/utils/safe-exec-args.js +58 -0
  98. package/src/utils/sandbox.js +182 -0
  99. package/src/utils/search-outcome.js +56 -0
  100. package/src/utils/workspace-root.js +80 -0
  101. package/src/validators/syntax-validator.js +149 -0
  102. package/src/workspace/detect-port.js +16 -16
  103. package/src/workspace/file-reader.js +1 -1
  104. package/src/workspace/process-manager.js +46 -46
  105. package/src/workspace-manager.js +86 -24
  106. package/test/adapters/registry.test.js +9 -2
  107. package/test/agent-auth.test.js +194 -0
  108. package/test/bind-host.test.js +26 -0
  109. package/test/branch-refspec.test.js +224 -0
  110. package/test/burst-suppressor.test.js +60 -0
  111. package/test/capabilities-access.test.js +47 -0
  112. package/test/capabilities.test.js +87 -0
  113. package/test/child-env.test.js +23 -0
  114. package/test/env-compat.test.js +71 -0
  115. package/test/exec-policy.test.js +463 -0
  116. package/test/file-count.test.js +64 -0
  117. package/test/incident-forwarder.test.js +231 -0
  118. package/test/multi-repo-open.test.js +180 -0
  119. package/test/multi-repo-pr.test.js +129 -0
  120. package/test/observability-detect.test.js +199 -0
  121. package/test/open-gate.test.js +341 -0
  122. package/test/ranged-read.test.js +60 -0
  123. package/test/request-guards.test.js +91 -0
  124. package/test/request-signing.test.js +284 -0
  125. package/test/root-recovery-sidecar.test.js +171 -0
  126. package/test/root-recovery.test.js +274 -0
  127. package/test/safe-exec-args.test.js +58 -0
  128. package/test/sandbox.test.js +98 -0
  129. package/test/search-outcome.test.js +50 -0
  130. package/test/workspace-root.test.js +64 -0
  131. package/tools/graphify-validation/README.md +92 -0
  132. package/tools/graphify-validation/config/questions.json +31 -0
  133. package/tools/graphify-validation/config/repos.json +9 -0
  134. package/tools/graphify-validation/config/symbols.json +6 -0
  135. package/tools/graphify-validation/fixtures/spring/expected.json +34 -0
  136. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AppConfig.java +29 -0
  137. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/AuditLogger.java +15 -0
  138. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/Cache.java +7 -0
  139. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/CheckoutService.java +25 -0
  140. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/InMemoryCache.java +18 -0
  141. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/OrderController.java +22 -0
  142. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaymentGateway.java +5 -0
  143. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/PaypalGateway.java +11 -0
  144. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/RefundService.java +14 -0
  145. package/tools/graphify-validation/fixtures/spring/src/main/java/com/example/fx/StripeGateway.java +11 -0
  146. package/tools/graphify-validation/lib/affected_vs_rg.py +81 -0
  147. package/tools/graphify-validation/lib/audit_hook/sitecustomize.py +22 -0
  148. package/tools/graphify-validation/lib/graph_metrics.py +96 -0
  149. package/tools/graphify-validation/lib/gv_common.py +130 -0
  150. package/tools/graphify-validation/lib/licenses.py +47 -0
  151. package/tools/graphify-validation/lib/pip_audit_summary.py +18 -0
  152. package/tools/graphify-validation/lib/recall.py +115 -0
  153. package/tools/graphify-validation/lib/report.py +226 -0
  154. package/tools/graphify-validation/lib/run_all.py +237 -0
  155. package/tools/graphify-validation/lib/run_graphify.py +144 -0
  156. package/tools/graphify-validation/lib/rusage_helper.py +17 -0
  157. package/tools/graphify-validation/lib/sample_edges.py +246 -0
  158. package/tools/graphify-validation/lib/spring_fixture.py +81 -0
  159. package/tools/graphify-validation/requirements-py311.lock +508 -0
  160. package/tools/graphify-validation/requirements-tools.in +1 -0
  161. package/tools/graphify-validation/requirements-tools.lock +371 -0
  162. package/tools/graphify-validation/requirements.in +1 -0
  163. package/tools/graphify-validation/requirements.lock +502 -0
  164. package/tools/graphify-validation/run.sh +43 -0
  165. package/tools/graphify-validation/tests/test_harness.py +151 -0
  166. package/cloudbuild-agent-qa.yaml +0 -43
  167. package/cloudbuild-agent-staging.yaml +0 -40
  168. 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
- * Sistema de aplicação de patches com backup e rollback.
7
+ * Patch application system with backup and rollback.
8
8
  */
9
9
 
10
10
  /**
11
- * Parseia unified diff
12
- * @param {string} diffText - Texto do diff
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
- // Início de novo arquivo
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
- // Nome do novo arquivo
38
+ // Name of the new file
39
39
  else if (line.startsWith('+++') && currentPatch) {
40
40
  currentPatch.newFileName = extractFileName(line);
41
41
  }
42
42
 
43
- // Início de hunk
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
- // Linha de conteúdo do hunk
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
- * Extrai nome do arquivo da linha --- ou +++
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
- * Aplica patch a um arquivo
81
- * @param {string} original - Conteúdo original
82
- * @param {Patch} patch - Patch a aplicar
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
- // Verificar contexto
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
- // Aplicar hunk
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
- // Remover linhas (de trás para frente)
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
- // Adicionar linhas
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
- * Aplica unified diff a um workspace
161
- * @param {string} root - Raiz do workspace
162
- * @param {string} diffText - Texto do diff
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
- * Cria backup de arquivos antes de aplicar patch
235
- * @param {string} root - Raiz do workspace
236
- * @param {string[]} files - Lista de arquivos
237
- * @param {string} backupId - ID do backup
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
- // Criar manifest
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
- * Restaura backup
279
- * @param {string} root - Raiz do workspace
280
- * @param {string} backupId - ID do backup
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
- * Extrai arquivos afetados por um diff
313
- * @param {string} diffText - Texto do diff
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) {