codeharness 0.25.1 → 0.25.2

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.
@@ -0,0 +1,3699 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/docker/health.ts
4
+ import { execFileSync } from "child_process";
5
+ function isDockerAvailable() {
6
+ try {
7
+ execFileSync("docker", ["--version"], { stdio: "pipe", timeout: 1e4 });
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
12
+ }
13
+ function isDockerComposeAvailable() {
14
+ try {
15
+ execFileSync("docker", ["compose", "version"], { stdio: "pipe", timeout: 1e4 });
16
+ return true;
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+ function getStackHealth(composeFile, projectName) {
22
+ const expectedServices = ["victoria-logs", "victoria-metrics", "victoria-traces", "otel-collector"];
23
+ try {
24
+ const args = projectName ? ["compose", "-p", projectName, "-f", composeFile, "ps", "--format", "json"] : ["compose", "-f", composeFile, "ps", "--format", "json"];
25
+ const output = execFileSync("docker", args, {
26
+ stdio: "pipe",
27
+ timeout: 15e3
28
+ });
29
+ const text = output.toString().trim();
30
+ const runningNames = /* @__PURE__ */ new Set();
31
+ if (text) {
32
+ const lines = text.split("\n").filter((l) => l.trim());
33
+ for (const line of lines) {
34
+ const svc = JSON.parse(line);
35
+ if (svc.State === "running" && svc.Service) {
36
+ runningNames.add(svc.Service);
37
+ }
38
+ }
39
+ }
40
+ const services = expectedServices.map((name) => ({
41
+ name,
42
+ running: runningNames.has(name)
43
+ }));
44
+ const healthy = services.every((s) => s.running);
45
+ const remedyCmd = projectName ? `docker compose -p ${projectName} -f ${composeFile} up -d` : `docker compose -f ${composeFile} up -d`;
46
+ return {
47
+ healthy,
48
+ services,
49
+ remedy: healthy ? void 0 : `Restart: ${remedyCmd}`
50
+ };
51
+ } catch {
52
+ const remedyCmd = projectName ? `docker compose -p ${projectName} -f ${composeFile} up -d` : `docker compose -f ${composeFile} up -d`;
53
+ const services = expectedServices.map((name) => ({
54
+ name,
55
+ running: false
56
+ }));
57
+ return {
58
+ healthy: false,
59
+ services,
60
+ remedy: `Restart: ${remedyCmd}`
61
+ };
62
+ }
63
+ }
64
+ function getCollectorHealth(composeFile) {
65
+ const expectedServices = ["otel-collector"];
66
+ try {
67
+ const output = execFileSync("docker", ["compose", "-p", "codeharness-collector", "-f", composeFile, "ps", "--format", "json"], {
68
+ stdio: "pipe",
69
+ timeout: 15e3
70
+ });
71
+ const text = output.toString().trim();
72
+ const runningNames = /* @__PURE__ */ new Set();
73
+ if (text) {
74
+ const lines = text.split("\n").filter((l) => l.trim());
75
+ for (const line of lines) {
76
+ const svc = JSON.parse(line);
77
+ if (svc.State === "running" && svc.Service) {
78
+ runningNames.add(svc.Service);
79
+ }
80
+ }
81
+ }
82
+ const services = expectedServices.map((name) => ({
83
+ name,
84
+ running: runningNames.has(name)
85
+ }));
86
+ const healthy = services.every((s) => s.running);
87
+ return {
88
+ healthy,
89
+ services,
90
+ remedy: healthy ? void 0 : `Restart: docker compose -p codeharness-collector -f ${composeFile} up -d`
91
+ };
92
+ } catch {
93
+ const services = expectedServices.map((name) => ({
94
+ name,
95
+ running: false
96
+ }));
97
+ return {
98
+ healthy: false,
99
+ services,
100
+ remedy: `Restart: docker compose -p codeharness-collector -f ${composeFile} up -d`
101
+ };
102
+ }
103
+ }
104
+ async function checkRemoteEndpoint(url) {
105
+ try {
106
+ const controller = new AbortController();
107
+ const timeout = setTimeout(() => controller.abort(), 5e3);
108
+ try {
109
+ await fetch(url, { signal: controller.signal });
110
+ return { reachable: true };
111
+ } finally {
112
+ clearTimeout(timeout);
113
+ }
114
+ } catch (err) {
115
+ const message = err instanceof Error ? err.message : String(err);
116
+ return { reachable: false, error: message };
117
+ }
118
+ }
119
+
120
+ // src/lib/docker/compose.ts
121
+ import { execFileSync as execFileSync2 } from "child_process";
122
+ import { writeFileSync as writeFileSync2 } from "fs";
123
+
124
+ // src/lib/stack-path.ts
125
+ import { homedir } from "os";
126
+ import { join, isAbsolute } from "path";
127
+ import { mkdirSync } from "fs";
128
+ function getStackDir() {
129
+ const xdgDataHome = process.env["XDG_DATA_HOME"];
130
+ if (xdgDataHome && xdgDataHome.trim() !== "" && isAbsolute(xdgDataHome)) {
131
+ return join(xdgDataHome, "codeharness", "stack");
132
+ }
133
+ return join(homedir(), ".codeharness", "stack");
134
+ }
135
+ function getComposeFilePath() {
136
+ return join(getStackDir(), "docker-compose.harness.yml");
137
+ }
138
+ function getElkComposeFilePath() {
139
+ return join(getStackDir(), "docker-compose.elk.yml");
140
+ }
141
+ function getOtelConfigPath() {
142
+ return join(getStackDir(), "otel-collector-config.yaml");
143
+ }
144
+ function ensureStackDir() {
145
+ mkdirSync(getStackDir(), { recursive: true });
146
+ }
147
+
148
+ // src/lib/templates.ts
149
+ import { mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "fs";
150
+ import { dirname, resolve } from "path";
151
+ import { fileURLToPath } from "url";
152
+ var __dirname = dirname(fileURLToPath(import.meta.url));
153
+ function getPackageRoot() {
154
+ return resolve(__dirname, "..", "..");
155
+ }
156
+ function generateFile(targetPath, content) {
157
+ mkdirSync2(dirname(targetPath), { recursive: true });
158
+ writeFileSync(targetPath, content, "utf-8");
159
+ }
160
+ function renderTemplate(template, vars) {
161
+ return template.replace(/\{\{(\w+)\}\}/g, (match, key) => vars[key] ?? match);
162
+ }
163
+ function renderTemplateFile(templatePath, vars = {}) {
164
+ const fullPath = resolve(getPackageRoot(), templatePath);
165
+ const content = readFileSync(fullPath, "utf-8");
166
+ if (Object.keys(vars).length === 0) return content;
167
+ return renderTemplate(content, vars);
168
+ }
169
+
170
+ // src/templates/docker-compose.ts
171
+ function dockerComposeCollectorOnlyTemplate() {
172
+ return renderTemplateFile("templates/compose/collector-only.yml");
173
+ }
174
+ function dockerComposeTemplate(_config) {
175
+ return renderTemplateFile("templates/compose/victoria.yml");
176
+ }
177
+
178
+ // src/templates/otel-config.ts
179
+ function otelCollectorRemoteTemplate(config) {
180
+ const logsUrl = config.logsUrl.replace(/\/+$/, "");
181
+ const metricsUrl = config.metricsUrl.replace(/\/+$/, "");
182
+ const tracesUrl = config.tracesUrl.replace(/\/+$/, "");
183
+ return renderTemplateFile("templates/compose/otel-collector-remote.yaml", {
184
+ LOGS_URL: logsUrl,
185
+ METRICS_URL: metricsUrl,
186
+ TRACES_URL: tracesUrl
187
+ });
188
+ }
189
+ function otelCollectorConfigTemplate() {
190
+ return renderTemplateFile("templates/compose/otel-collector-base.yaml");
191
+ }
192
+
193
+ // src/lib/docker/compose.ts
194
+ function isStackRunning(composeFile) {
195
+ try {
196
+ const output = execFileSync2("docker", ["compose", "-f", composeFile, "ps", "--format", "json"], {
197
+ stdio: "pipe",
198
+ timeout: 15e3
199
+ });
200
+ const text = output.toString().trim();
201
+ if (!text) return false;
202
+ const lines = text.split("\n").filter((l) => l.trim());
203
+ for (const line of lines) {
204
+ const svc = JSON.parse(line);
205
+ if (svc.State !== "running") return false;
206
+ }
207
+ return lines.length > 0;
208
+ } catch {
209
+ return false;
210
+ }
211
+ }
212
+ function isSharedStackRunning() {
213
+ try {
214
+ const composeFile = getComposeFilePath();
215
+ const output = execFileSync2("docker", ["compose", "-p", "codeharness-shared", "-f", composeFile, "ps", "--format", "json"], {
216
+ stdio: "pipe",
217
+ timeout: 15e3
218
+ });
219
+ const text = output.toString().trim();
220
+ if (!text) return false;
221
+ const lines = text.split("\n").filter((l) => l.trim());
222
+ for (const line of lines) {
223
+ const svc = JSON.parse(line);
224
+ if (svc.State !== "running") return false;
225
+ }
226
+ return lines.length > 0;
227
+ } catch {
228
+ return false;
229
+ }
230
+ }
231
+ function startStack(composeFile) {
232
+ try {
233
+ execFileSync2("docker", ["compose", "-f", composeFile, "up", "-d"], {
234
+ stdio: "pipe",
235
+ timeout: 3e4
236
+ });
237
+ const services = getRunningServices(composeFile);
238
+ return {
239
+ started: true,
240
+ services
241
+ };
242
+ } catch (err) {
243
+ const message = err instanceof Error ? err.message : String(err);
244
+ return {
245
+ started: false,
246
+ services: [],
247
+ error: message
248
+ };
249
+ }
250
+ }
251
+ function startSharedStack() {
252
+ try {
253
+ ensureStackDir();
254
+ const composeFile = getComposeFilePath();
255
+ const otelConfigFile = getOtelConfigPath();
256
+ const composeContent = dockerComposeTemplate({ shared: true });
257
+ writeFileSync2(composeFile, composeContent, "utf-8");
258
+ const otelContent = otelCollectorConfigTemplate();
259
+ writeFileSync2(otelConfigFile, otelContent, "utf-8");
260
+ execFileSync2("docker", ["compose", "-p", "codeharness-shared", "-f", composeFile, "up", "-d"], {
261
+ stdio: "pipe",
262
+ timeout: 3e4
263
+ });
264
+ const services = getRunningServices(composeFile, "codeharness-shared");
265
+ return {
266
+ started: true,
267
+ services
268
+ };
269
+ } catch (err) {
270
+ const message = err instanceof Error ? err.message : String(err);
271
+ return {
272
+ started: false,
273
+ services: [],
274
+ error: message
275
+ };
276
+ }
277
+ }
278
+ function stopStack(composeFile) {
279
+ execFileSync2("docker", ["compose", "-f", composeFile, "down", "-v"], {
280
+ stdio: "pipe",
281
+ timeout: 3e4
282
+ });
283
+ }
284
+ function stopSharedStack() {
285
+ const composeFile = getComposeFilePath();
286
+ execFileSync2("docker", ["compose", "-p", "codeharness-shared", "-f", composeFile, "down"], {
287
+ stdio: "pipe",
288
+ timeout: 3e4
289
+ });
290
+ }
291
+ function startCollectorOnly(logsUrl, metricsUrl, tracesUrl) {
292
+ try {
293
+ ensureStackDir();
294
+ const composeFile = getComposeFilePath();
295
+ const otelConfigFile = getOtelConfigPath();
296
+ const composeContent = dockerComposeCollectorOnlyTemplate();
297
+ writeFileSync2(composeFile, composeContent, "utf-8");
298
+ const otelContent = otelCollectorRemoteTemplate({ logsUrl, metricsUrl, tracesUrl });
299
+ writeFileSync2(otelConfigFile, otelContent, "utf-8");
300
+ execFileSync2("docker", ["compose", "-p", "codeharness-collector", "-f", composeFile, "up", "-d"], {
301
+ stdio: "pipe",
302
+ timeout: 3e4
303
+ });
304
+ const services = getRunningServices(composeFile, "codeharness-collector");
305
+ return {
306
+ started: true,
307
+ services
308
+ };
309
+ } catch (err) {
310
+ const message = err instanceof Error ? err.message : String(err);
311
+ return {
312
+ started: false,
313
+ services: [],
314
+ error: message
315
+ };
316
+ }
317
+ }
318
+ function isCollectorRunning() {
319
+ try {
320
+ const composeFile = getComposeFilePath();
321
+ const output = execFileSync2("docker", ["compose", "-p", "codeharness-collector", "-f", composeFile, "ps", "--format", "json"], {
322
+ stdio: "pipe",
323
+ timeout: 15e3
324
+ });
325
+ const text = output.toString().trim();
326
+ if (!text) return false;
327
+ const lines = text.split("\n").filter((l) => l.trim());
328
+ for (const line of lines) {
329
+ const svc = JSON.parse(line);
330
+ if (svc.State !== "running") return false;
331
+ }
332
+ return lines.length > 0;
333
+ } catch {
334
+ return false;
335
+ }
336
+ }
337
+ function stopCollectorOnly() {
338
+ const composeFile = getComposeFilePath();
339
+ execFileSync2("docker", ["compose", "-p", "codeharness-collector", "-f", composeFile, "down"], {
340
+ stdio: "pipe",
341
+ timeout: 3e4
342
+ });
343
+ }
344
+ function getRunningServices(composeFile, projectName) {
345
+ try {
346
+ const args = projectName ? ["compose", "-p", projectName, "-f", composeFile, "ps", "--format", "json"] : ["compose", "-f", composeFile, "ps", "--format", "json"];
347
+ const output = execFileSync2("docker", args, {
348
+ stdio: "pipe",
349
+ timeout: 15e3
350
+ });
351
+ const text = output.toString().trim();
352
+ if (!text) return [];
353
+ const lines = text.split("\n").filter((l) => l.trim());
354
+ const services = [];
355
+ for (const line of lines) {
356
+ const svc = JSON.parse(line);
357
+ const ports = (svc.Publishers ?? []).filter((p) => p.PublishedPort && p.PublishedPort > 0).map((p) => String(p.PublishedPort));
358
+ services.push({
359
+ name: svc.Service ?? "unknown",
360
+ status: svc.State ?? "unknown",
361
+ port: ports.join(",")
362
+ });
363
+ }
364
+ return services;
365
+ } catch {
366
+ return [];
367
+ }
368
+ }
369
+
370
+ // src/modules/infra/init-project.ts
371
+ import { existsSync as existsSync14 } from "fs";
372
+ import { basename as basename3, join as join15 } from "path";
373
+
374
+ // src/lib/output.ts
375
+ function ok(message, options) {
376
+ if (options?.json) {
377
+ jsonOutput({ status: "ok", message });
378
+ return;
379
+ }
380
+ console.log(`[OK] ${message}`);
381
+ }
382
+ function fail(message, options) {
383
+ if (options?.json) {
384
+ jsonOutput({ status: "fail", message });
385
+ return;
386
+ }
387
+ console.log(`[FAIL] ${message}`);
388
+ }
389
+ function warn(message, options) {
390
+ if (options?.json) {
391
+ jsonOutput({ status: "warn", message });
392
+ return;
393
+ }
394
+ console.log(`[WARN] ${message}`);
395
+ }
396
+ function info(message, options) {
397
+ if (options?.json) {
398
+ jsonOutput({ status: "info", message });
399
+ return;
400
+ }
401
+ console.log(`[INFO] ${message}`);
402
+ }
403
+ function jsonOutput(data) {
404
+ console.log(JSON.stringify(data));
405
+ }
406
+
407
+ // src/lib/stacks/registry.ts
408
+ import { existsSync, readdirSync } from "fs";
409
+ import { join as join2 } from "path";
410
+ var providers = /* @__PURE__ */ new Map();
411
+ var SKIP_DIRS = /* @__PURE__ */ new Set([
412
+ "node_modules",
413
+ ".git",
414
+ "target",
415
+ "__pycache__",
416
+ "dist",
417
+ "build",
418
+ "coverage",
419
+ ".venv",
420
+ "venv",
421
+ ".tox",
422
+ ".mypy_cache",
423
+ ".cache"
424
+ ]);
425
+ var STACK_PRIORITY = ["nodejs", "python", "rust"];
426
+ function registerProvider(provider) {
427
+ providers.set(provider.name, provider);
428
+ }
429
+ function getStackProvider(name) {
430
+ return providers.get(name);
431
+ }
432
+ function detectStacks(dir = process.cwd()) {
433
+ const results = [];
434
+ const ordered = getOrderedProviders();
435
+ for (const provider of ordered) {
436
+ if (hasMarker(dir, provider.markers)) {
437
+ results.push({ stack: provider.name, dir: "." });
438
+ }
439
+ }
440
+ let entries;
441
+ try {
442
+ entries = readdirSync(dir, { withFileTypes: true });
443
+ } catch (_err) {
444
+ entries = [];
445
+ }
446
+ const subdirs = entries.filter((e) => e.isDirectory() && !SKIP_DIRS.has(e.name)).map((e) => e.name).sort();
447
+ for (const subdir of subdirs) {
448
+ const subdirPath = join2(dir, subdir);
449
+ for (const provider of ordered) {
450
+ if (hasMarker(subdirPath, provider.markers)) {
451
+ results.push({ stack: provider.name, dir: subdir });
452
+ }
453
+ }
454
+ }
455
+ return results;
456
+ }
457
+ function detectStack(dir = process.cwd()) {
458
+ const stacks = detectStacks(dir);
459
+ const rootStack = stacks.find((s) => s.dir === ".");
460
+ return rootStack ? rootStack.stack : null;
461
+ }
462
+ function hasMarker(dir, markers) {
463
+ return markers.some((marker) => existsSync(join2(dir, marker)));
464
+ }
465
+ function getOrderedProviders() {
466
+ const ordered = [];
467
+ for (const name of STACK_PRIORITY) {
468
+ const p = providers.get(name);
469
+ if (p) ordered.push(p);
470
+ }
471
+ for (const [name, provider] of providers) {
472
+ if (!STACK_PRIORITY.includes(name)) {
473
+ ordered.push(provider);
474
+ }
475
+ }
476
+ return ordered;
477
+ }
478
+
479
+ // src/lib/stacks/nodejs.ts
480
+ import { execFileSync as execFileSync3 } from "child_process";
481
+ import { existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "fs";
482
+ import { join as join4 } from "path";
483
+
484
+ // src/lib/stacks/utils.ts
485
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
486
+ import { join as join3 } from "path";
487
+ function readJsonSafe(path) {
488
+ try {
489
+ if (!existsSync2(path)) return null;
490
+ return JSON.parse(readFileSync2(path, "utf-8"));
491
+ } catch {
492
+ return null;
493
+ }
494
+ }
495
+ function readTextSafe(path) {
496
+ try {
497
+ if (!existsSync2(path)) return null;
498
+ return readFileSync2(path, "utf-8");
499
+ } catch {
500
+ return null;
501
+ }
502
+ }
503
+ function getNodeDeps(pkg) {
504
+ const deps = /* @__PURE__ */ new Set();
505
+ for (const field of ["dependencies", "devDependencies"]) {
506
+ const section = pkg[field];
507
+ if (section && typeof section === "object") {
508
+ for (const key of Object.keys(section)) {
509
+ deps.add(key);
510
+ }
511
+ }
512
+ }
513
+ return deps;
514
+ }
515
+ function getPythonDepsContent(dir) {
516
+ const files = ["requirements.txt", "pyproject.toml", "setup.py"];
517
+ const parts = [];
518
+ for (const file of files) {
519
+ const content = readTextSafe(join3(dir, file));
520
+ if (content) parts.push(content);
521
+ }
522
+ return parts.join("\n");
523
+ }
524
+ function hasPythonDep(content, dep) {
525
+ const escaped = dep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
526
+ const pattern = new RegExp(`(?:^|[\\s"',])${escaped}(?:[\\[>=<~!;\\s"',]|$)`, "m");
527
+ return pattern.test(content);
528
+ }
529
+ function getCargoDepsSection(content) {
530
+ const match = content.match(/^\[dependencies\]\s*$/m);
531
+ if (!match || match.index === void 0) return "";
532
+ const start = match.index + match[0].length;
533
+ const nextSection = content.slice(start).search(/^\[/m);
534
+ return nextSection === -1 ? content.slice(start) : content.slice(start, start + nextSection);
535
+ }
536
+ function hasCargoDep(depsSection, dep) {
537
+ const escaped = dep.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
538
+ const pattern = new RegExp(`(?:^|\\s)${escaped}(?:\\s*=|\\s*\\{)`, "m");
539
+ return pattern.test(depsSection);
540
+ }
541
+
542
+ // src/lib/stacks/nodejs.ts
543
+ var AGENT_DEPS = [
544
+ "anthropic",
545
+ "@anthropic-ai/sdk",
546
+ "openai",
547
+ "langchain",
548
+ "@langchain/core",
549
+ "llamaindex"
550
+ ];
551
+ var WEB_FRAMEWORK_DEPS = [
552
+ "react",
553
+ "vue",
554
+ "svelte",
555
+ "angular",
556
+ "@angular/core",
557
+ "next",
558
+ "nuxt",
559
+ "vite",
560
+ "webpack"
561
+ ];
562
+ var NODE_OTLP_PACKAGES = [
563
+ "@opentelemetry/auto-instrumentations-node",
564
+ "@opentelemetry/sdk-node",
565
+ "@opentelemetry/exporter-trace-otlp-http",
566
+ "@opentelemetry/exporter-metrics-otlp-http"
567
+ ];
568
+ var NODE_REQUIRE_FLAG = "--require @opentelemetry/auto-instrumentations-node/register";
569
+ var NodejsProvider = class {
570
+ name = "nodejs";
571
+ markers = ["package.json"];
572
+ displayName = "Node.js (package.json)";
573
+ // ── Task 1: detectAppType ──────────────────────────────────────────────
574
+ detectAppType(dir) {
575
+ const pkg = readJsonSafe(join4(dir, "package.json"));
576
+ if (!pkg) return "generic";
577
+ const deps = getNodeDeps(pkg);
578
+ if (AGENT_DEPS.some((d) => deps.has(d))) {
579
+ return "agent";
580
+ }
581
+ if (WEB_FRAMEWORK_DEPS.some((d) => deps.has(d))) {
582
+ return "web";
583
+ }
584
+ if (existsSync3(join4(dir, "index.html")) || existsSync3(join4(dir, "public", "index.html")) || existsSync3(join4(dir, "src", "index.html"))) {
585
+ return "web";
586
+ }
587
+ const bin = pkg["bin"];
588
+ const scripts = pkg["scripts"];
589
+ const hasStart = scripts?.["start"] !== void 0;
590
+ if (bin && !hasStart) {
591
+ return "cli";
592
+ }
593
+ if (hasStart) {
594
+ return "server";
595
+ }
596
+ return "generic";
597
+ }
598
+ // ── Task 2: getCoverageTool ────────────────────────────────────────────
599
+ getCoverageTool() {
600
+ return "c8";
601
+ }
602
+ // ── Task 3: detectCoverageConfig ───────────────────────────────────────
603
+ detectCoverageConfig(dir) {
604
+ const vitestConfigTs = join4(dir, "vitest.config.ts");
605
+ const vitestConfigJs = join4(dir, "vitest.config.js");
606
+ const hasVitestConfig = existsSync3(vitestConfigTs) || existsSync3(vitestConfigJs);
607
+ const pkg = readJsonSafe(join4(dir, "package.json"));
608
+ let hasVitestCoverageV8 = false;
609
+ let hasVitestCoverageIstanbul = false;
610
+ let hasC8 = false;
611
+ let hasJest = false;
612
+ if (pkg) {
613
+ const allDeps = getNodeDeps(pkg);
614
+ hasVitestCoverageV8 = allDeps.has("@vitest/coverage-v8");
615
+ hasVitestCoverageIstanbul = allDeps.has("@vitest/coverage-istanbul");
616
+ hasC8 = allDeps.has("c8");
617
+ hasJest = allDeps.has("jest");
618
+ }
619
+ if (hasVitestConfig || hasVitestCoverageV8 || hasVitestCoverageIstanbul) {
620
+ const configFile = existsSync3(vitestConfigTs) ? vitestConfigTs : existsSync3(vitestConfigJs) ? vitestConfigJs : void 0;
621
+ return { tool: "c8", configFile };
622
+ }
623
+ if (hasC8) {
624
+ return { tool: "c8" };
625
+ }
626
+ if (hasJest) {
627
+ return { tool: "c8" };
628
+ }
629
+ return { tool: "none" };
630
+ }
631
+ // ── Task 4: getOtlpPackages ────────────────────────────────────────────
632
+ getOtlpPackages() {
633
+ return [...NODE_OTLP_PACKAGES];
634
+ }
635
+ // ── Task 5: installOtlp ───────────────────────────────────────────────
636
+ installOtlp(dir) {
637
+ try {
638
+ execFileSync3("npm", ["install", ...NODE_OTLP_PACKAGES], {
639
+ cwd: dir,
640
+ stdio: "pipe",
641
+ timeout: 3e5
642
+ });
643
+ return {
644
+ success: true,
645
+ packagesInstalled: [...NODE_OTLP_PACKAGES]
646
+ };
647
+ } catch (err) {
648
+ const message = err instanceof Error ? err.message : "Unknown error";
649
+ return {
650
+ success: false,
651
+ packagesInstalled: [],
652
+ error: `Failed to install Node.js OTLP packages: ${message.length > 200 ? message.slice(0, 200) + "... (truncated)" : message}`
653
+ };
654
+ }
655
+ }
656
+ // ── Task 6: patchStartScript ──────────────────────────────────────────
657
+ patchStartScript(dir) {
658
+ const pkgPath = join4(dir, "package.json");
659
+ if (!existsSync3(pkgPath)) return false;
660
+ let raw;
661
+ let pkg;
662
+ try {
663
+ raw = readFileSync3(pkgPath, "utf-8");
664
+ pkg = JSON.parse(raw);
665
+ } catch {
666
+ return false;
667
+ }
668
+ const scripts = pkg["scripts"];
669
+ if (!scripts) return false;
670
+ const targetKey = scripts["start"] ? "start" : scripts["dev"] ? "dev" : null;
671
+ if (!targetKey) return false;
672
+ const instrumentedKey = `${targetKey}:instrumented`;
673
+ if (scripts[instrumentedKey]?.includes(NODE_REQUIRE_FLAG)) {
674
+ return false;
675
+ }
676
+ scripts[instrumentedKey] = `NODE_OPTIONS='${NODE_REQUIRE_FLAG}' ${scripts[targetKey]}`;
677
+ writeFileSync3(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
678
+ return true;
679
+ }
680
+ // ── Task 7: getDockerfileTemplate ─────────────────────────────────────
681
+ getDockerfileTemplate() {
682
+ return renderTemplateFile("templates/dockerfiles/Dockerfile.nodejs");
683
+ }
684
+ // ── Task 8: getDockerBuildStage ───────────────────────────────────────
685
+ getDockerBuildStage() {
686
+ return `# === Build stage: nodejs ===
687
+ FROM node:22-slim AS build-nodejs
688
+ WORKDIR /build
689
+ COPY package*.json ./
690
+ RUN npm ci --production
691
+ COPY . .
692
+ `;
693
+ }
694
+ // ── Task 9: getRuntimeCopyDirectives ──────────────────────────────────
695
+ getRuntimeCopyDirectives() {
696
+ return [
697
+ "COPY --from=build-nodejs /build/node_modules ./node_modules",
698
+ "COPY --from=build-nodejs /build/ ./app/"
699
+ ].join("\n");
700
+ }
701
+ // ── Task 10: getBuildCommands ──────────────────────────────────────────
702
+ getBuildCommands() {
703
+ return ["npm install", "npm run build"];
704
+ }
705
+ // ── Task 11: getTestCommands ──────────────────────────────────────────
706
+ getTestCommands() {
707
+ return ["npm test"];
708
+ }
709
+ // ── Task 12: getSemgrepLanguages ──────────────────────────────────────
710
+ getSemgrepLanguages() {
711
+ return ["javascript", "typescript"];
712
+ }
713
+ // ── Task 13: parseTestOutput ──────────────────────────────────────────
714
+ parseTestOutput(output) {
715
+ const vitestMatch = /Tests\s+(\d+)\s+passed(?:\s*\|\s*(\d+)\s+failed)?(?:\s*\|\s*(\d+)\s+skipped)?/i.exec(output);
716
+ if (vitestMatch) {
717
+ const passed = parseInt(vitestMatch[1], 10);
718
+ const failed = vitestMatch[2] ? parseInt(vitestMatch[2], 10) : 0;
719
+ const skipped = vitestMatch[3] ? parseInt(vitestMatch[3], 10) : 0;
720
+ return { passed, failed, skipped, total: passed + failed + skipped };
721
+ }
722
+ const jestPassedMatch = /Tests:.*?(\d+)\s+passed/i.exec(output);
723
+ if (jestPassedMatch) {
724
+ const passed = parseInt(jestPassedMatch[1], 10);
725
+ const failedMatch = /Tests:.*?(\d+)\s+failed/i.exec(output);
726
+ const skippedMatch = /Tests:.*?(\d+)\s+skipped/i.exec(output);
727
+ const totalMatch = /Tests:.*?(\d+)\s+total/i.exec(output);
728
+ const failed = failedMatch ? parseInt(failedMatch[1], 10) : 0;
729
+ const skipped = skippedMatch ? parseInt(skippedMatch[1], 10) : 0;
730
+ const total = totalMatch ? parseInt(totalMatch[1], 10) : passed + failed + skipped;
731
+ return { passed, failed, skipped, total };
732
+ }
733
+ return { passed: 0, failed: 0, skipped: 0, total: 0 };
734
+ }
735
+ // ── Task 14: parseCoverageReport ──────────────────────────────────────
736
+ parseCoverageReport(dir) {
737
+ const candidates = [
738
+ join4(dir, "coverage", "coverage-summary.json"),
739
+ join4(dir, "src", "coverage", "coverage-summary.json")
740
+ ];
741
+ let reportPath = null;
742
+ for (const p of candidates) {
743
+ if (existsSync3(p)) {
744
+ reportPath = p;
745
+ break;
746
+ }
747
+ }
748
+ if (!reportPath) return 0;
749
+ try {
750
+ const report = JSON.parse(readFileSync3(reportPath, "utf-8"));
751
+ return report.total?.statements?.pct ?? 0;
752
+ } catch {
753
+ return 0;
754
+ }
755
+ }
756
+ // ── Task 15: getProjectName ───────────────────────────────────────────
757
+ // ── getVerifyDockerfileSection ──────────────────────────────────────
758
+ getVerifyDockerfileSection(_projectDir) {
759
+ return [
760
+ "# --- Node.js tooling ---",
761
+ "RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \\",
762
+ " && apt-get install -y --no-install-recommends nodejs \\",
763
+ " && rm -rf /var/lib/apt/lists/*",
764
+ "RUN npm install -g showboat @anthropic-ai/claude-code"
765
+ ].join("\n");
766
+ }
767
+ getProjectName(dir) {
768
+ const pkg = readJsonSafe(join4(dir, "package.json"));
769
+ if (pkg && typeof pkg.name === "string") {
770
+ return pkg.name;
771
+ }
772
+ return null;
773
+ }
774
+ };
775
+
776
+ // src/lib/stacks/python.ts
777
+ import { execFileSync as execFileSync4 } from "child_process";
778
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
779
+ import { join as join5 } from "path";
780
+ var AGENT_DEPS2 = [
781
+ "anthropic",
782
+ "openai",
783
+ "langchain",
784
+ "llama-index",
785
+ "traceloop-sdk"
786
+ ];
787
+ var WEB_FRAMEWORK_DEPS2 = ["flask", "django", "fastapi", "streamlit"];
788
+ var PYTHON_OTLP_PACKAGES = [
789
+ "opentelemetry-distro",
790
+ "opentelemetry-exporter-otlp"
791
+ ];
792
+ var PythonProvider = class {
793
+ name = "python";
794
+ markers = ["requirements.txt", "pyproject.toml", "setup.py"];
795
+ displayName = "Python";
796
+ // ── Task 2: detectAppType ──────────────────────────────────────────────
797
+ detectAppType(dir) {
798
+ const content = getPythonDepsContent(dir);
799
+ if (AGENT_DEPS2.some((d) => hasPythonDep(content, d))) {
800
+ return "agent";
801
+ }
802
+ const hasWebFramework = WEB_FRAMEWORK_DEPS2.some((d) => hasPythonDep(content, d));
803
+ if (hasWebFramework) {
804
+ const hasTemplates = existsSync4(join5(dir, "templates"));
805
+ const hasStatic = existsSync4(join5(dir, "static"));
806
+ if (hasTemplates || hasStatic) {
807
+ return "web";
808
+ }
809
+ return "server";
810
+ }
811
+ if (existsSync4(join5(dir, "app.py")) || existsSync4(join5(dir, "main.py")) || existsSync4(join5(dir, "manage.py"))) {
812
+ return "server";
813
+ }
814
+ return "generic";
815
+ }
816
+ // ── Task 4: getCoverageTool ────────────────────────────────────────────
817
+ getCoverageTool() {
818
+ return "coverage-py";
819
+ }
820
+ // ── Task 5: detectCoverageConfig ───────────────────────────────────────
821
+ detectCoverageConfig(dir) {
822
+ const reqPath = join5(dir, "requirements.txt");
823
+ const reqContent = readTextSafe(reqPath);
824
+ if (reqContent) {
825
+ if (hasPythonDep(reqContent, "pytest-cov") || hasPythonDep(reqContent, "coverage")) {
826
+ return { tool: "coverage-py", configFile: reqPath };
827
+ }
828
+ }
829
+ const pyprojectPath = join5(dir, "pyproject.toml");
830
+ const pyprojectContent = readTextSafe(pyprojectPath);
831
+ if (pyprojectContent) {
832
+ if (hasPythonDep(pyprojectContent, "pytest-cov") || hasPythonDep(pyprojectContent, "coverage") || pyprojectContent.includes("[tool.coverage")) {
833
+ return { tool: "coverage-py", configFile: pyprojectPath };
834
+ }
835
+ }
836
+ return { tool: "none" };
837
+ }
838
+ // ── Task 6: getOtlpPackages ────────────────────────────────────────────
839
+ getOtlpPackages() {
840
+ return [...PYTHON_OTLP_PACKAGES];
841
+ }
842
+ // ── Task 7: installOtlp ───────────────────────────────────────────────
843
+ installOtlp(dir) {
844
+ try {
845
+ execFileSync4("pip", ["install", ...PYTHON_OTLP_PACKAGES], {
846
+ cwd: dir,
847
+ stdio: "pipe",
848
+ timeout: 3e5
849
+ });
850
+ return {
851
+ success: true,
852
+ packagesInstalled: [...PYTHON_OTLP_PACKAGES]
853
+ };
854
+ } catch {
855
+ }
856
+ const installed = [];
857
+ try {
858
+ for (const pkg of PYTHON_OTLP_PACKAGES) {
859
+ execFileSync4("pipx", ["install", pkg], {
860
+ cwd: dir,
861
+ stdio: "pipe",
862
+ timeout: 3e5
863
+ });
864
+ installed.push(pkg);
865
+ }
866
+ return {
867
+ success: true,
868
+ packagesInstalled: installed
869
+ };
870
+ } catch (err) {
871
+ const message = err instanceof Error ? err.message : "Unknown error";
872
+ return {
873
+ success: false,
874
+ packagesInstalled: installed,
875
+ error: `Failed to install Python OTLP packages: ${message.length > 200 ? message.slice(0, 200) + "... (truncated)" : message}`
876
+ };
877
+ }
878
+ }
879
+ // ── Task 8: getDockerfileTemplate ─────────────────────────────────────
880
+ getDockerfileTemplate() {
881
+ return renderTemplateFile("templates/dockerfiles/Dockerfile.python");
882
+ }
883
+ // ── Task 9: getDockerBuildStage ───────────────────────────────────────
884
+ getDockerBuildStage() {
885
+ return `# === Build stage: python ===
886
+ FROM python:3.12-slim AS build-python
887
+ WORKDIR /build
888
+ COPY . .
889
+ RUN pip install --target=/build/dist .
890
+ `;
891
+ }
892
+ // ── Task 10: getRuntimeCopyDirectives ──────────────────────────────────
893
+ getRuntimeCopyDirectives() {
894
+ return "COPY --from=build-python /build/dist /opt/app/python/";
895
+ }
896
+ // ── Task 11: getBuildCommands ──────────────────────────────────────────
897
+ getBuildCommands() {
898
+ return ["pip install -r requirements.txt"];
899
+ }
900
+ // ── Task 12: getTestCommands ──────────────────────────────────────────
901
+ getTestCommands() {
902
+ return ["python -m pytest"];
903
+ }
904
+ // ── Task 13: getSemgrepLanguages ──────────────────────────────────────
905
+ getSemgrepLanguages() {
906
+ return ["python"];
907
+ }
908
+ // ── Task 14: parseTestOutput ──────────────────────────────────────────
909
+ parseTestOutput(output) {
910
+ const passedMatch = /(\d+)\s+passed/i.exec(output);
911
+ if (passedMatch) {
912
+ const passed = parseInt(passedMatch[1], 10);
913
+ const failedMatch = /(\d+)\s+failed/i.exec(output);
914
+ const skippedMatch = /(\d+)\s+skipped/i.exec(output);
915
+ const failed = failedMatch ? parseInt(failedMatch[1], 10) : 0;
916
+ const skipped = skippedMatch ? parseInt(skippedMatch[1], 10) : 0;
917
+ return { passed, failed, skipped, total: passed + failed + skipped };
918
+ }
919
+ return { passed: 0, failed: 0, skipped: 0, total: 0 };
920
+ }
921
+ // ── Task 15: parseCoverageReport ──────────────────────────────────────
922
+ parseCoverageReport(dir) {
923
+ const reportPath = join5(dir, "coverage.json");
924
+ if (!existsSync4(reportPath)) return 0;
925
+ try {
926
+ const report = JSON.parse(readFileSync4(reportPath, "utf-8"));
927
+ return report.totals?.percent_covered ?? 0;
928
+ } catch {
929
+ return 0;
930
+ }
931
+ }
932
+ // ── Task 16: getProjectName ───────────────────────────────────────────
933
+ // ── getVerifyDockerfileSection ──────────────────────────────────────
934
+ getVerifyDockerfileSection(_projectDir) {
935
+ return [
936
+ "# --- Python tooling ---",
937
+ "RUN apt-get update && apt-get install -y --no-install-recommends \\",
938
+ " python3-pip python3-venv \\",
939
+ " && rm -rf /var/lib/apt/lists/*",
940
+ "RUN pip install --break-system-packages coverage pytest"
941
+ ].join("\n");
942
+ }
943
+ getProjectName(dir) {
944
+ const pyprojectContent = readTextSafe(join5(dir, "pyproject.toml"));
945
+ if (pyprojectContent) {
946
+ const projectIdx = pyprojectContent.search(/^\[project\]\s*$/m);
947
+ if (projectIdx !== -1) {
948
+ const afterHeader = pyprojectContent.slice(
949
+ projectIdx + pyprojectContent.slice(projectIdx).indexOf("\n") + 1
950
+ );
951
+ const nextSectionIdx = afterHeader.search(/^\[/m);
952
+ const section = nextSectionIdx === -1 ? afterHeader : afterHeader.slice(0, nextSectionIdx);
953
+ const nameMatch = /^\s*name\s*=\s*["']([^"']+)["']/m.exec(section);
954
+ if (nameMatch) return nameMatch[1];
955
+ }
956
+ }
957
+ const setupContent = readTextSafe(join5(dir, "setup.py"));
958
+ if (setupContent) {
959
+ const match = /name\s*=\s*['"]([^'"]+)['"]/m.exec(setupContent);
960
+ if (match) return match[1];
961
+ }
962
+ return null;
963
+ }
964
+ };
965
+
966
+ // src/lib/stacks/rust.ts
967
+ import { execFileSync as execFileSync5 } from "child_process";
968
+ import { existsSync as existsSync5, readFileSync as readFileSync5 } from "fs";
969
+ import { join as join6 } from "path";
970
+ var RUST_AGENT_DEPS = ["async-openai", "anthropic", "llm-chain"];
971
+ var RUST_WEB_FRAMEWORKS = ["actix-web", "axum", "rocket", "tide", "warp"];
972
+ var RUST_OTLP_PACKAGES = [
973
+ "opentelemetry",
974
+ "opentelemetry-otlp",
975
+ "tracing-opentelemetry",
976
+ "tracing-subscriber"
977
+ ];
978
+ var RustProvider = class {
979
+ name = "rust";
980
+ markers = ["Cargo.toml"];
981
+ displayName = "Rust (Cargo.toml)";
982
+ // ── Task 3: detectAppType ──────────────────────────────────────────────
983
+ detectAppType(dir) {
984
+ const cargoContent = readTextSafe(join6(dir, "Cargo.toml"));
985
+ if (!cargoContent) return "generic";
986
+ const depsSection = getCargoDepsSection(cargoContent);
987
+ if (RUST_AGENT_DEPS.some((d) => hasCargoDep(depsSection, d))) {
988
+ return "agent";
989
+ }
990
+ if (RUST_WEB_FRAMEWORKS.some((d) => hasCargoDep(depsSection, d))) {
991
+ return "server";
992
+ }
993
+ if (/^\[\[bin\]\]\s*$/m.test(cargoContent)) {
994
+ return "cli";
995
+ }
996
+ if (/^\[lib\]\s*$/m.test(cargoContent)) {
997
+ return "generic";
998
+ }
999
+ return "generic";
1000
+ }
1001
+ // ── Task 4: getCoverageTool ────────────────────────────────────────────
1002
+ getCoverageTool() {
1003
+ return "tarpaulin";
1004
+ }
1005
+ // ── Task 5: detectCoverageConfig ───────────────────────────────────────
1006
+ detectCoverageConfig(dir) {
1007
+ const cargoPath = join6(dir, "Cargo.toml");
1008
+ if (!existsSync5(cargoPath)) {
1009
+ return { tool: "none" };
1010
+ }
1011
+ return { tool: "tarpaulin", configFile: cargoPath };
1012
+ }
1013
+ // ── Task 6: getOtlpPackages ────────────────────────────────────────────
1014
+ getOtlpPackages() {
1015
+ return [...RUST_OTLP_PACKAGES];
1016
+ }
1017
+ // ── Task 7: installOtlp ───────────────────────────────────────────────
1018
+ installOtlp(dir) {
1019
+ try {
1020
+ execFileSync5("cargo", ["add", ...RUST_OTLP_PACKAGES], {
1021
+ cwd: dir,
1022
+ stdio: "pipe",
1023
+ timeout: 3e5
1024
+ });
1025
+ return {
1026
+ success: true,
1027
+ packagesInstalled: [...RUST_OTLP_PACKAGES]
1028
+ };
1029
+ } catch (err) {
1030
+ const message = err instanceof Error ? err.message : "Unknown error";
1031
+ return {
1032
+ success: false,
1033
+ packagesInstalled: [],
1034
+ error: `Failed to install Rust OTLP packages: ${message.length > 200 ? message.slice(0, 200) + "... (truncated)" : message}`
1035
+ };
1036
+ }
1037
+ }
1038
+ // ── Task 8: getDockerfileTemplate ─────────────────────────────────────
1039
+ getDockerfileTemplate() {
1040
+ return renderTemplateFile("templates/dockerfiles/Dockerfile.rust");
1041
+ }
1042
+ // ── Task 9: getDockerBuildStage ───────────────────────────────────────
1043
+ getDockerBuildStage() {
1044
+ return `# === Build stage: rust ===
1045
+ FROM rust:1.82-slim AS build-rust
1046
+ WORKDIR /build
1047
+ COPY . .
1048
+ RUN cargo build --release
1049
+ `;
1050
+ }
1051
+ // ── Task 10: getRuntimeCopyDirectives ──────────────────────────────────
1052
+ getRuntimeCopyDirectives() {
1053
+ return "COPY --from=build-rust /build/target/release/myapp /usr/local/bin/myapp";
1054
+ }
1055
+ // ── Task 11: getBuildCommands ──────────────────────────────────────────
1056
+ getBuildCommands() {
1057
+ return ["cargo build", "cargo test"];
1058
+ }
1059
+ // ── Task 12: getTestCommands ──────────────────────────────────────────
1060
+ getTestCommands() {
1061
+ return ["cargo test"];
1062
+ }
1063
+ // ── Task 13: getSemgrepLanguages ──────────────────────────────────────
1064
+ getSemgrepLanguages() {
1065
+ return ["rust"];
1066
+ }
1067
+ // ── Task 14: parseTestOutput ──────────────────────────────────────────
1068
+ parseTestOutput(output) {
1069
+ const pattern = /test result:.*?(\d+)\s+passed;\s*(\d+)\s+failed(?:;\s*(\d+)\s+ignored)?/g;
1070
+ let passed = 0;
1071
+ let failed = 0;
1072
+ let skipped = 0;
1073
+ let match;
1074
+ while ((match = pattern.exec(output)) !== null) {
1075
+ passed += parseInt(match[1], 10);
1076
+ failed += parseInt(match[2], 10);
1077
+ skipped += match[3] ? parseInt(match[3], 10) : 0;
1078
+ }
1079
+ const total = passed + failed + skipped;
1080
+ return { passed, failed, skipped, total };
1081
+ }
1082
+ // ── Task 15: parseCoverageReport ──────────────────────────────────────
1083
+ parseCoverageReport(dir) {
1084
+ const reportPath = join6(dir, "coverage", "tarpaulin-report.json");
1085
+ if (!existsSync5(reportPath)) return 0;
1086
+ try {
1087
+ const report = JSON.parse(readFileSync5(reportPath, "utf-8"));
1088
+ return report.coverage ?? 0;
1089
+ } catch {
1090
+ return 0;
1091
+ }
1092
+ }
1093
+ // ── Task 16: getProjectName ───────────────────────────────────────────
1094
+ // ── getVerifyDockerfileSection ──────────────────────────────────────
1095
+ getVerifyDockerfileSection(projectDir) {
1096
+ const lines = [
1097
+ "# --- Rust tooling ---",
1098
+ 'RUN curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable',
1099
+ 'ENV PATH="/root/.cargo/bin:$PATH"',
1100
+ "RUN rustup component add clippy",
1101
+ "RUN cargo install cargo-tarpaulin"
1102
+ ];
1103
+ const cargoContent = readTextSafe(join6(projectDir, "Cargo.toml"));
1104
+ if (cargoContent) {
1105
+ const depsSection = getCargoDepsSection(cargoContent);
1106
+ if (hasCargoDep(depsSection, "bevy")) {
1107
+ lines.push(
1108
+ "RUN apt-get update && apt-get install -y --no-install-recommends \\",
1109
+ " libudev-dev libasound2-dev libwayland-dev libxkbcommon-dev \\",
1110
+ " libfontconfig1-dev libx11-dev \\",
1111
+ " && rm -rf /var/lib/apt/lists/*"
1112
+ );
1113
+ }
1114
+ }
1115
+ return lines.join("\n");
1116
+ }
1117
+ getProjectName(dir) {
1118
+ const cargoContent = readTextSafe(join6(dir, "Cargo.toml"));
1119
+ if (!cargoContent) return null;
1120
+ const packageIdx = cargoContent.search(/^\[package\]\s*$/m);
1121
+ if (packageIdx === -1) return null;
1122
+ const afterHeader = cargoContent.slice(
1123
+ packageIdx + cargoContent.slice(packageIdx).indexOf("\n") + 1
1124
+ );
1125
+ const nextSectionIdx = afterHeader.search(/^\[/m);
1126
+ const section = nextSectionIdx === -1 ? afterHeader : afterHeader.slice(0, nextSectionIdx);
1127
+ const nameMatch = /^\s*name\s*=\s*["']([^"']+)["']/m.exec(section);
1128
+ return nameMatch ? nameMatch[1] : null;
1129
+ }
1130
+ };
1131
+
1132
+ // src/lib/stacks/index.ts
1133
+ registerProvider(new NodejsProvider());
1134
+ registerProvider(new PythonProvider());
1135
+ registerProvider(new RustProvider());
1136
+ function detectAppType(dir, stack) {
1137
+ if (!stack) return "generic";
1138
+ const provider = getStackProvider(stack);
1139
+ if (!provider) return "generic";
1140
+ return provider.detectAppType(dir);
1141
+ }
1142
+
1143
+ // src/lib/state.ts
1144
+ import { existsSync as existsSync6, mkdirSync as mkdirSync3, readFileSync as readFileSync6, writeFileSync as writeFileSync4 } from "fs";
1145
+ import { join as join7 } from "path";
1146
+ import { parse, stringify } from "yaml";
1147
+ function migrateState(state) {
1148
+ const raw = state;
1149
+ if (state.otlp && !state.otlp.backend) {
1150
+ state.otlp.backend = "victoria";
1151
+ }
1152
+ if (Array.isArray(raw.stacks) && raw.stacks.length > 0) {
1153
+ state.stacks = raw.stacks;
1154
+ state.stack = state.stacks[0] ?? null;
1155
+ return state;
1156
+ }
1157
+ if (typeof raw.stack === "string" && raw.stack) {
1158
+ state.stacks = [raw.stack];
1159
+ return state;
1160
+ }
1161
+ state.stacks = [];
1162
+ state.stack = null;
1163
+ return state;
1164
+ }
1165
+ var STATE_DIR = ".claude";
1166
+ var STATE_FILE = "codeharness.local.md";
1167
+ var DEFAULT_BODY = "\n# Codeharness State\n\nThis file is managed by the codeharness CLI. Do not edit manually.\n";
1168
+ var COVERAGE_TOOL_DEFAULTS = {
1169
+ python: "coverage.py",
1170
+ rust: "cargo-tarpaulin"
1171
+ };
1172
+ function getDefaultCoverageTool(stack) {
1173
+ if (stack && COVERAGE_TOOL_DEFAULTS[stack]) return COVERAGE_TOOL_DEFAULTS[stack];
1174
+ return "c8";
1175
+ }
1176
+ function getDefaultState(stack) {
1177
+ return {
1178
+ harness_version: "0.1.0",
1179
+ initialized: false,
1180
+ stack: stack ?? null,
1181
+ stacks: stack ? [stack] : [],
1182
+ enforcement: {
1183
+ frontend: true,
1184
+ database: true,
1185
+ api: true
1186
+ },
1187
+ coverage: {
1188
+ target: 90,
1189
+ baseline: null,
1190
+ current: null,
1191
+ tool: getDefaultCoverageTool(stack)
1192
+ },
1193
+ session_flags: {
1194
+ logs_queried: false,
1195
+ tests_passed: false,
1196
+ coverage_met: false,
1197
+ verification_run: false
1198
+ },
1199
+ verification_log: []
1200
+ };
1201
+ }
1202
+ function getStatePath(dir) {
1203
+ return join7(dir, STATE_DIR, STATE_FILE);
1204
+ }
1205
+ function writeState(state, dir, body) {
1206
+ const baseDir = dir ?? process.cwd();
1207
+ const claudeDir = join7(baseDir, STATE_DIR);
1208
+ mkdirSync3(claudeDir, { recursive: true });
1209
+ const toWrite = { ...state };
1210
+ if (toWrite.stacks && toWrite.stacks.length > 0) {
1211
+ toWrite.stack = toWrite.stacks[0];
1212
+ }
1213
+ const yamlContent = stringify(toWrite, { nullStr: "null" });
1214
+ const markdownBody = body ?? DEFAULT_BODY;
1215
+ const fileContent = `---
1216
+ ${yamlContent}---
1217
+ ${markdownBody}`;
1218
+ writeFileSync4(join7(claudeDir, STATE_FILE), fileContent, "utf-8");
1219
+ }
1220
+ function readState(dir) {
1221
+ const baseDir = dir ?? process.cwd();
1222
+ const filePath = getStatePath(baseDir);
1223
+ if (!existsSync6(filePath)) {
1224
+ throw new StateFileNotFoundError();
1225
+ }
1226
+ const raw = readFileSync6(filePath, "utf-8");
1227
+ const parts = raw.split("---");
1228
+ if (parts.length < 3) {
1229
+ return recoverCorruptedState(baseDir);
1230
+ }
1231
+ try {
1232
+ const state = parse(parts[1]);
1233
+ if (!isValidState(state)) {
1234
+ return recoverCorruptedState(baseDir);
1235
+ }
1236
+ return migrateState(state);
1237
+ } catch {
1238
+ return recoverCorruptedState(baseDir);
1239
+ }
1240
+ }
1241
+ function readStateWithBody(dir) {
1242
+ const baseDir = dir ?? process.cwd();
1243
+ const filePath = getStatePath(baseDir);
1244
+ if (!existsSync6(filePath)) {
1245
+ throw new StateFileNotFoundError();
1246
+ }
1247
+ const raw = readFileSync6(filePath, "utf-8");
1248
+ const parts = raw.split("---");
1249
+ if (parts.length < 3) {
1250
+ const state = recoverCorruptedState(baseDir);
1251
+ return { state, body: DEFAULT_BODY };
1252
+ }
1253
+ try {
1254
+ const state = parse(parts[1]);
1255
+ if (!isValidState(state)) {
1256
+ const recovered = recoverCorruptedState(baseDir);
1257
+ return { state: recovered, body: DEFAULT_BODY };
1258
+ }
1259
+ const body = parts.slice(2).join("---");
1260
+ return { state: migrateState(state), body: body || DEFAULT_BODY };
1261
+ } catch {
1262
+ const state = recoverCorruptedState(baseDir);
1263
+ return { state, body: DEFAULT_BODY };
1264
+ }
1265
+ }
1266
+ function isValidState(state) {
1267
+ if (!state || typeof state !== "object") return false;
1268
+ const s = state;
1269
+ if (typeof s.harness_version !== "string") return false;
1270
+ if (typeof s.initialized !== "boolean") return false;
1271
+ if (s.stack !== null && typeof s.stack !== "string") return false;
1272
+ if (!s.enforcement || typeof s.enforcement !== "object") return false;
1273
+ if (!s.coverage || typeof s.coverage !== "object") return false;
1274
+ if (!s.session_flags || typeof s.session_flags !== "object") return false;
1275
+ if (!Array.isArray(s.verification_log)) return false;
1276
+ if (s.stacks !== void 0 && !Array.isArray(s.stacks)) return false;
1277
+ if (Array.isArray(s.stacks) && s.stacks.some((v) => typeof v !== "string")) return false;
1278
+ return true;
1279
+ }
1280
+ function recoverCorruptedState(dir) {
1281
+ warn("State file corrupted \u2014 recreating from detected config");
1282
+ const stack = detectStack(dir);
1283
+ const allStacks = detectStacks(dir);
1284
+ const state = getDefaultState(stack);
1285
+ const uniqueStackNames = [...new Set(allStacks.map((s) => s.stack))];
1286
+ state.stacks = uniqueStackNames;
1287
+ if (state.stack === null && uniqueStackNames.length > 0) {
1288
+ state.stack = uniqueStackNames[0];
1289
+ }
1290
+ writeState(state, dir);
1291
+ return state;
1292
+ }
1293
+ var StateFileNotFoundError = class extends Error {
1294
+ constructor() {
1295
+ super("No state file found. Run 'codeharness init' first.");
1296
+ this.name = "StateFileNotFoundError";
1297
+ }
1298
+ };
1299
+ var DANGEROUS_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
1300
+ function validateKeyPath(keyPath) {
1301
+ const keys = keyPath.split(".");
1302
+ for (const key of keys) {
1303
+ if (DANGEROUS_KEYS.has(key)) {
1304
+ throw new Error(`Invalid key path: '${key}' is not allowed`);
1305
+ }
1306
+ }
1307
+ }
1308
+ function getNestedValue(obj, keyPath) {
1309
+ validateKeyPath(keyPath);
1310
+ const keys = keyPath.split(".");
1311
+ let current = obj;
1312
+ for (const key of keys) {
1313
+ if (current === null || current === void 0 || typeof current !== "object") {
1314
+ return void 0;
1315
+ }
1316
+ current = current[key];
1317
+ }
1318
+ return current;
1319
+ }
1320
+ function setNestedValue(obj, keyPath, value) {
1321
+ validateKeyPath(keyPath);
1322
+ const keys = keyPath.split(".");
1323
+ let current = obj;
1324
+ for (let i = 0; i < keys.length - 1; i++) {
1325
+ const key = keys[i];
1326
+ if (current[key] === void 0 || current[key] === null || typeof current[key] !== "object") {
1327
+ current[key] = {};
1328
+ }
1329
+ current = current[key];
1330
+ }
1331
+ current[keys[keys.length - 1]] = value;
1332
+ }
1333
+ function parseValue(raw) {
1334
+ if (raw === "true") return true;
1335
+ if (raw === "false") return false;
1336
+ if (raw === "null") return null;
1337
+ const num = Number(raw);
1338
+ if (!Number.isNaN(num) && raw.trim() !== "") return num;
1339
+ return raw;
1340
+ }
1341
+
1342
+ // src/lib/observability/instrument.ts
1343
+ import { execFileSync as execFileSync7 } from "child_process";
1344
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
1345
+ import { join as join9 } from "path";
1346
+
1347
+ // src/lib/observability/config.ts
1348
+ import { execFileSync as execFileSync6 } from "child_process";
1349
+ import { existsSync as existsSync7, readFileSync as readFileSync7, writeFileSync as writeFileSync5 } from "fs";
1350
+ import { join as join8, basename } from "path";
1351
+ var WEB_OTLP_PACKAGES = [
1352
+ "@opentelemetry/sdk-trace-web",
1353
+ "@opentelemetry/instrumentation-fetch",
1354
+ "@opentelemetry/instrumentation-xml-http-request"
1355
+ ];
1356
+ var AGENT_OTLP_PACKAGES_NODE = ["@traceloop/node-server-sdk"];
1357
+ var AGENT_OTLP_PACKAGES_PYTHON = ["traceloop-sdk"];
1358
+ var NODE_REQUIRE_FLAG2 = "--require @opentelemetry/auto-instrumentations-node/register";
1359
+ function ensureServiceNameEnvVar(projectDir, serviceName) {
1360
+ const envFilePath = join8(projectDir, ".env.codeharness");
1361
+ const sanitized = serviceName.replace(/[^a-zA-Z0-9._-]/g, "-");
1362
+ const envLine = `OTEL_SERVICE_NAME=${sanitized}`;
1363
+ if (existsSync7(envFilePath)) {
1364
+ const content = readFileSync7(envFilePath, "utf-8");
1365
+ const lines = content.split("\n").filter((l, i, arr) => i < arr.length - 1 || l.trim() !== "");
1366
+ const idx = lines.findIndex((l) => l.startsWith("OTEL_SERVICE_NAME="));
1367
+ if (idx !== -1) {
1368
+ lines[idx] = envLine;
1369
+ } else {
1370
+ lines.push(envLine);
1371
+ }
1372
+ writeFileSync5(envFilePath, lines.join("\n") + "\n", "utf-8");
1373
+ } else {
1374
+ writeFileSync5(envFilePath, envLine + "\n", "utf-8");
1375
+ }
1376
+ }
1377
+ function ensureEndpointEnvVar(projectDir, endpoint) {
1378
+ const envFilePath = join8(projectDir, ".env.codeharness");
1379
+ const envLine = `OTEL_EXPORTER_OTLP_ENDPOINT=${endpoint}`;
1380
+ if (existsSync7(envFilePath)) {
1381
+ const content = readFileSync7(envFilePath, "utf-8");
1382
+ const lines = content.split("\n").filter((l, i, arr) => i < arr.length - 1 || l.trim() !== "");
1383
+ const idx = lines.findIndex((l) => l.startsWith("OTEL_EXPORTER_OTLP_ENDPOINT="));
1384
+ if (idx !== -1) {
1385
+ lines[idx] = envLine;
1386
+ } else {
1387
+ lines.push(envLine);
1388
+ }
1389
+ writeFileSync5(envFilePath, lines.join("\n") + "\n", "utf-8");
1390
+ } else {
1391
+ writeFileSync5(envFilePath, envLine + "\n", "utf-8");
1392
+ }
1393
+ }
1394
+ function configureOtlpEnvVars(projectDir, stack, opts) {
1395
+ const projectName = basename(projectDir);
1396
+ const { state, body } = readStateWithBody(projectDir);
1397
+ const stackOtlpFields = {
1398
+ nodejs: { node_require: NODE_REQUIRE_FLAG2 },
1399
+ python: { python_wrapper: "opentelemetry-instrument" },
1400
+ rust: { rust_env_hint: "OTEL_EXPORTER_OTLP_ENDPOINT" }
1401
+ };
1402
+ state.otlp = {
1403
+ enabled: true,
1404
+ endpoint: opts?.endpoint ?? "http://localhost:4318",
1405
+ service_name: projectName,
1406
+ mode: state.otlp?.mode ?? "local-shared",
1407
+ ...stack && stackOtlpFields[stack] ? stackOtlpFields[stack] : {}
1408
+ };
1409
+ state.otlp.resource_attributes = "service.instance.id=$(hostname)-$$";
1410
+ writeState(state, projectDir, body);
1411
+ ensureServiceNameEnvVar(projectDir, projectName);
1412
+ const needsEndpointEnv = /* @__PURE__ */ new Set(["rust"]);
1413
+ if (stack && needsEndpointEnv.has(stack)) {
1414
+ ensureEndpointEnvVar(projectDir, state.otlp.endpoint);
1415
+ }
1416
+ }
1417
+ function configureCli(projectDir) {
1418
+ const { state, body } = readStateWithBody(projectDir);
1419
+ if (!state.otlp) return;
1420
+ state.otlp.cli_env_vars = {
1421
+ OTEL_BSP_SCHEDULE_DELAY: "100",
1422
+ OTEL_TRACES_SAMPLER: "always_on",
1423
+ OTEL_BLRP_SCHEDULE_DELAY: "100"
1424
+ };
1425
+ writeState(state, projectDir, body);
1426
+ }
1427
+ function configureWeb(projectDir, stack) {
1428
+ const webInstaller = {
1429
+ nodejs: () => {
1430
+ try {
1431
+ execFileSync6("npm", ["install", ...WEB_OTLP_PACKAGES], { cwd: projectDir, stdio: "pipe", timeout: 3e5 });
1432
+ } catch {
1433
+ }
1434
+ }
1435
+ };
1436
+ if (stack && webInstaller[stack]) {
1437
+ webInstaller[stack]();
1438
+ }
1439
+ let endpoint = "http://localhost:4318";
1440
+ try {
1441
+ const currentState = readState(projectDir);
1442
+ if (currentState.otlp?.endpoint) {
1443
+ endpoint = currentState.otlp.endpoint;
1444
+ }
1445
+ } catch {
1446
+ }
1447
+ const snippet = `// OpenTelemetry Web SDK initialization \u2014 generated by codeharness
1448
+ import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
1449
+ import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
1450
+ import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
1451
+ import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
1452
+ import { XMLHttpRequestInstrumentation } from '@opentelemetry/instrumentation-xml-http-request';
1453
+ import { registerInstrumentations } from '@opentelemetry/instrumentation';
1454
+
1455
+ const exporter = new OTLPTraceExporter({
1456
+ url: '${endpoint}/v1/traces',
1457
+ });
1458
+
1459
+ const provider = new WebTracerProvider();
1460
+ provider.addSpanProcessor(new BatchSpanProcessor(exporter));
1461
+ provider.register();
1462
+
1463
+ registerInstrumentations({
1464
+ instrumentations: [
1465
+ new FetchInstrumentation(),
1466
+ new XMLHttpRequestInstrumentation(),
1467
+ ],
1468
+ });
1469
+ `;
1470
+ const snippetPath = join8(projectDir, "otel-web-init.js");
1471
+ writeFileSync5(snippetPath, snippet, "utf-8");
1472
+ const { state, body } = readStateWithBody(projectDir);
1473
+ if (state.otlp) {
1474
+ state.otlp.web_snippet_path = "otel-web-init.js";
1475
+ }
1476
+ writeState(state, projectDir, body);
1477
+ }
1478
+ function configureAgent(projectDir, stack) {
1479
+ const agentInstallers = {
1480
+ nodejs: () => {
1481
+ try {
1482
+ execFileSync6("npm", ["install", ...AGENT_OTLP_PACKAGES_NODE], { cwd: projectDir, stdio: "pipe", timeout: 3e5 });
1483
+ } catch {
1484
+ }
1485
+ return true;
1486
+ },
1487
+ python: () => {
1488
+ try {
1489
+ execFileSync6("pip", ["install", ...AGENT_OTLP_PACKAGES_PYTHON], { cwd: projectDir, stdio: "pipe", timeout: 3e5 });
1490
+ } catch {
1491
+ try {
1492
+ for (const pkg of AGENT_OTLP_PACKAGES_PYTHON) {
1493
+ execFileSync6("pipx", ["install", pkg], { cwd: projectDir, stdio: "pipe", timeout: 3e5 });
1494
+ }
1495
+ } catch {
1496
+ }
1497
+ }
1498
+ return true;
1499
+ },
1500
+ rust: () => {
1501
+ info("Rust agent SDK not yet supported \u2014 skipping agent configuration");
1502
+ return false;
1503
+ }
1504
+ };
1505
+ if (stack) {
1506
+ const installer = agentInstallers[stack];
1507
+ if (installer) {
1508
+ const proceed = installer();
1509
+ if (!proceed) return;
1510
+ }
1511
+ }
1512
+ const { state, body } = readStateWithBody(projectDir);
1513
+ if (state.otlp) {
1514
+ state.otlp.agent_sdk = "traceloop";
1515
+ }
1516
+ writeState(state, projectDir, body);
1517
+ }
1518
+
1519
+ // src/lib/observability/instrument.ts
1520
+ var NODE_OTLP_PACKAGES2 = [
1521
+ "@opentelemetry/auto-instrumentations-node",
1522
+ "@opentelemetry/sdk-node",
1523
+ "@opentelemetry/exporter-trace-otlp-http",
1524
+ "@opentelemetry/exporter-metrics-otlp-http"
1525
+ ];
1526
+ var PYTHON_OTLP_PACKAGES2 = [
1527
+ "opentelemetry-distro",
1528
+ "opentelemetry-exporter-otlp"
1529
+ ];
1530
+ var RUST_OTLP_PACKAGES2 = [
1531
+ "opentelemetry",
1532
+ "opentelemetry-otlp",
1533
+ "tracing-opentelemetry",
1534
+ "tracing-subscriber"
1535
+ ];
1536
+ function truncateError(message, maxLength = 200) {
1537
+ if (message.length <= maxLength) return message;
1538
+ return message.slice(0, maxLength) + "... (truncated)";
1539
+ }
1540
+ function installNodeOtlp(projectDir) {
1541
+ try {
1542
+ execFileSync7("npm", ["install", ...NODE_OTLP_PACKAGES2], { cwd: projectDir, stdio: "pipe", timeout: 3e5 });
1543
+ return {
1544
+ status: "configured",
1545
+ packages_installed: true,
1546
+ start_script_patched: false,
1547
+ env_vars_configured: false
1548
+ };
1549
+ } catch (err) {
1550
+ const message = err instanceof Error ? err.message : "Unknown error";
1551
+ return {
1552
+ status: "failed",
1553
+ packages_installed: false,
1554
+ start_script_patched: false,
1555
+ env_vars_configured: false,
1556
+ error: `Failed to install Node.js OTLP packages: ${truncateError(message)}`
1557
+ };
1558
+ }
1559
+ }
1560
+ function patchNodeStartScript(projectDir) {
1561
+ const pkgPath = join9(projectDir, "package.json");
1562
+ if (!existsSync8(pkgPath)) {
1563
+ return false;
1564
+ }
1565
+ let raw;
1566
+ let pkg;
1567
+ try {
1568
+ raw = readFileSync8(pkgPath, "utf-8");
1569
+ pkg = JSON.parse(raw);
1570
+ } catch {
1571
+ return false;
1572
+ }
1573
+ const scripts = pkg["scripts"];
1574
+ if (!scripts) {
1575
+ return false;
1576
+ }
1577
+ const targetKey = scripts["start"] ? "start" : scripts["dev"] ? "dev" : null;
1578
+ if (!targetKey) {
1579
+ return false;
1580
+ }
1581
+ const instrumentedKey = `${targetKey}:instrumented`;
1582
+ if (scripts[instrumentedKey]?.includes(NODE_REQUIRE_FLAG2)) {
1583
+ return false;
1584
+ }
1585
+ scripts[instrumentedKey] = `NODE_OPTIONS='${NODE_REQUIRE_FLAG2}' ${scripts[targetKey]}`;
1586
+ writeFileSync6(pkgPath, JSON.stringify(pkg, null, 2) + "\n", "utf-8");
1587
+ return true;
1588
+ }
1589
+ function installPythonOtlp(projectDir) {
1590
+ const installChains = [
1591
+ [{ cmd: "pip", args: ["install", ...PYTHON_OTLP_PACKAGES2] }],
1592
+ PYTHON_OTLP_PACKAGES2.map((pkg) => ({ cmd: "pipx", args: ["install", pkg] }))
1593
+ ];
1594
+ for (const chain of installChains) {
1595
+ try {
1596
+ for (const step of chain) {
1597
+ execFileSync7(step.cmd, step.args, { cwd: projectDir, stdio: "pipe", timeout: 3e5 });
1598
+ }
1599
+ return {
1600
+ status: "configured",
1601
+ packages_installed: true,
1602
+ start_script_patched: false,
1603
+ env_vars_configured: false
1604
+ };
1605
+ } catch {
1606
+ continue;
1607
+ }
1608
+ }
1609
+ return {
1610
+ status: "failed",
1611
+ packages_installed: false,
1612
+ start_script_patched: false,
1613
+ env_vars_configured: false,
1614
+ error: "Failed to install Python OTLP packages"
1615
+ };
1616
+ }
1617
+ function installRustOtlp(projectDir) {
1618
+ try {
1619
+ execFileSync7("cargo", ["add", ...RUST_OTLP_PACKAGES2], { cwd: projectDir, stdio: "pipe", timeout: 3e5 });
1620
+ return {
1621
+ status: "configured",
1622
+ packages_installed: true,
1623
+ start_script_patched: false,
1624
+ env_vars_configured: false
1625
+ };
1626
+ } catch (err) {
1627
+ const message = err instanceof Error ? err.message : "Unknown error";
1628
+ return {
1629
+ status: "failed",
1630
+ packages_installed: false,
1631
+ start_script_patched: false,
1632
+ env_vars_configured: false,
1633
+ error: `Failed to install Rust OTLP packages: ${truncateError(message)}`
1634
+ };
1635
+ }
1636
+ }
1637
+ function instrumentProject(projectDir, stack, opts) {
1638
+ const isJson = opts?.json === true;
1639
+ const appType = opts?.appType;
1640
+ const stackInstallers = {
1641
+ nodejs: () => {
1642
+ const r = installNodeOtlp(projectDir);
1643
+ if (r.status === "configured") {
1644
+ const patched = patchNodeStartScript(projectDir);
1645
+ r.start_script_patched = patched;
1646
+ if (!isJson) {
1647
+ ok("OTLP: Node.js packages installed");
1648
+ if (patched) {
1649
+ ok("OTLP: start script patched with --require flag");
1650
+ } else {
1651
+ info("OTLP: no start/dev script found or already patched");
1652
+ }
1653
+ }
1654
+ }
1655
+ return r;
1656
+ },
1657
+ python: () => {
1658
+ const r = installPythonOtlp(projectDir);
1659
+ if (r.status === "configured" && !isJson) {
1660
+ ok("OTLP: Python packages installed");
1661
+ info("OTLP: wrap your command with: opentelemetry-instrument <command>");
1662
+ }
1663
+ return r;
1664
+ },
1665
+ rust: () => {
1666
+ const r = installRustOtlp(projectDir);
1667
+ if (r.status === "configured" && !isJson) {
1668
+ ok("OTLP: Rust packages installed");
1669
+ }
1670
+ return r;
1671
+ }
1672
+ };
1673
+ if (!stack || !stackInstallers[stack]) {
1674
+ return {
1675
+ status: "skipped",
1676
+ packages_installed: false,
1677
+ start_script_patched: false,
1678
+ env_vars_configured: false,
1679
+ error: "Unsupported stack for OTLP instrumentation"
1680
+ };
1681
+ }
1682
+ const result = stackInstallers[stack]();
1683
+ configureOtlpEnvVars(projectDir, stack, { appType });
1684
+ result.env_vars_configured = true;
1685
+ if (!isJson) {
1686
+ ok("OTLP: environment variables configured");
1687
+ }
1688
+ if (result.status === "configured") {
1689
+ if (appType === "cli") {
1690
+ configureCli(projectDir);
1691
+ if (!isJson) {
1692
+ ok("OTLP: CLI instrumentation configured (fast flush, always_on sampler)");
1693
+ }
1694
+ } else if (appType === "web") {
1695
+ configureWeb(projectDir, stack);
1696
+ if (!isJson) {
1697
+ ok("OTLP: Web instrumentation configured (browser SDK + CORS)");
1698
+ }
1699
+ } else if (appType === "agent") {
1700
+ configureAgent(projectDir, stack);
1701
+ if (!isJson) {
1702
+ ok("OTLP: Agent/LLM instrumentation configured (OpenLLMetry/Traceloop)");
1703
+ }
1704
+ } else if (appType === "generic" && !isJson) {
1705
+ info("App type: generic (manual OTLP setup may be needed)");
1706
+ }
1707
+ } else if (!isJson && result.error) {
1708
+ info(`OTLP: ${result.error}`);
1709
+ }
1710
+ return result;
1711
+ }
1712
+
1713
+ // src/types/result.ts
1714
+ function ok2(data) {
1715
+ return { success: true, data };
1716
+ }
1717
+ function fail2(error, context) {
1718
+ if (context !== void 0) {
1719
+ return { success: false, error, context };
1720
+ }
1721
+ return { success: false, error };
1722
+ }
1723
+ function isOk(result) {
1724
+ return result.success === true;
1725
+ }
1726
+
1727
+ // src/modules/infra/types.ts
1728
+ var DEFAULT_PORTS = {
1729
+ logs: 9428,
1730
+ metrics: 8428,
1731
+ traces: 16686,
1732
+ otel_grpc: 4317,
1733
+ otel_http: 4318
1734
+ };
1735
+ var ALL_STACK_PORTS = [
1736
+ DEFAULT_PORTS.otel_grpc,
1737
+ DEFAULT_PORTS.otel_http,
1738
+ DEFAULT_PORTS.metrics,
1739
+ DEFAULT_PORTS.logs,
1740
+ DEFAULT_PORTS.traces
1741
+ ];
1742
+
1743
+ // src/modules/infra/docker-setup.ts
1744
+ function checkDocker(opts) {
1745
+ if (opts.otelEndpoint || opts.logsUrl || opts.opensearchUrl) {
1746
+ return ok2({ available: true, criticalFailure: false });
1747
+ }
1748
+ if (!isDockerAvailable()) {
1749
+ if (opts.observability) {
1750
+ const dockerResult = {
1751
+ compose_file: "",
1752
+ stack_running: false,
1753
+ services: [],
1754
+ ports: DEFAULT_PORTS
1755
+ };
1756
+ return ok2({ available: false, criticalFailure: true, dockerResult });
1757
+ }
1758
+ return ok2({ available: false, criticalFailure: false });
1759
+ }
1760
+ if (!opts.isJson) {
1761
+ ok("Docker: available");
1762
+ }
1763
+ return ok2({ available: true, criticalFailure: false });
1764
+ }
1765
+ function setupDocker(opts) {
1766
+ try {
1767
+ let state = { ...opts.state };
1768
+ if (!opts.observability) {
1769
+ writeState(state, opts.projectDir);
1770
+ if (!opts.isJson) {
1771
+ info("Observability: disabled, skipping Docker stack");
1772
+ }
1773
+ return ok2({ docker: null, state });
1774
+ }
1775
+ if (opts.opensearchUrl) {
1776
+ return handleOpenSearch(opts, state);
1777
+ }
1778
+ if (opts.otelEndpoint) {
1779
+ return handleRemoteDirect(opts, state);
1780
+ }
1781
+ if (opts.logsUrl && opts.metricsUrl && opts.tracesUrl) {
1782
+ return handleRemoteRouted(opts, state);
1783
+ }
1784
+ if (opts.dockerAvailable) {
1785
+ return handleLocalShared(opts, state);
1786
+ }
1787
+ const docker = {
1788
+ compose_file: "",
1789
+ stack_running: false,
1790
+ services: [],
1791
+ ports: DEFAULT_PORTS
1792
+ };
1793
+ writeState(state, opts.projectDir);
1794
+ if (!opts.isJson) {
1795
+ info("Observability: deferred (configure Docker or remote endpoint to activate)");
1796
+ }
1797
+ return ok2({ docker, state });
1798
+ } catch (err) {
1799
+ const message = err instanceof Error ? err.message : String(err);
1800
+ return fail2(`Docker setup failed: ${message}`);
1801
+ }
1802
+ }
1803
+ function handleOpenSearch(opts, state) {
1804
+ state = {
1805
+ ...state,
1806
+ otlp: {
1807
+ ...state.otlp,
1808
+ mode: "remote-direct"
1809
+ },
1810
+ opensearch: {
1811
+ url: opts.opensearchUrl
1812
+ }
1813
+ };
1814
+ writeState(state, opts.projectDir);
1815
+ if (!opts.isJson) {
1816
+ ok(`Observability: OpenSearch backend at ${opts.opensearchUrl}`);
1817
+ }
1818
+ return ok2({ docker: null, state });
1819
+ }
1820
+ function handleRemoteDirect(opts, state) {
1821
+ state = {
1822
+ ...state,
1823
+ otlp: {
1824
+ ...state.otlp,
1825
+ endpoint: opts.otelEndpoint,
1826
+ mode: "remote-direct"
1827
+ }
1828
+ };
1829
+ writeState(state, opts.projectDir);
1830
+ if (!opts.isJson) {
1831
+ ok(`OTLP: configured for remote endpoint ${opts.otelEndpoint}`);
1832
+ }
1833
+ return ok2({ docker: null, state });
1834
+ }
1835
+ function handleRemoteRouted(opts, state) {
1836
+ const collectorResult = startCollectorOnly(opts.logsUrl, opts.metricsUrl, opts.tracesUrl);
1837
+ const sharedComposeFile = getComposeFilePath();
1838
+ if (collectorResult.started) {
1839
+ state = {
1840
+ ...state,
1841
+ otlp: { ...state.otlp, mode: "remote-routed" },
1842
+ docker: {
1843
+ compose_file: sharedComposeFile,
1844
+ stack_running: true,
1845
+ remote_endpoints: {
1846
+ logs_url: opts.logsUrl,
1847
+ metrics_url: opts.metricsUrl,
1848
+ traces_url: opts.tracesUrl
1849
+ },
1850
+ ports: DEFAULT_PORTS
1851
+ }
1852
+ };
1853
+ const docker2 = {
1854
+ compose_file: sharedComposeFile,
1855
+ stack_running: true,
1856
+ services: collectorResult.services,
1857
+ ports: DEFAULT_PORTS
1858
+ };
1859
+ writeState(state, opts.projectDir);
1860
+ if (!opts.isJson) {
1861
+ ok("Observability: OTel Collector started (routing to remote backends)");
1862
+ }
1863
+ return ok2({ docker: docker2, state });
1864
+ }
1865
+ if (!opts.isJson) {
1866
+ fail("OTel Collector: failed to start");
1867
+ if (collectorResult.error) {
1868
+ info(`Error: ${collectorResult.error}`);
1869
+ }
1870
+ }
1871
+ const docker = {
1872
+ compose_file: sharedComposeFile,
1873
+ stack_running: false,
1874
+ services: [],
1875
+ ports: DEFAULT_PORTS
1876
+ };
1877
+ return ok2({ docker, state });
1878
+ }
1879
+ function handleLocalShared(opts, state) {
1880
+ const backend = state.otlp?.backend ?? "victoria";
1881
+ const sharedComposeFile = backend === "elk" ? getElkComposeFilePath() : getComposeFilePath();
1882
+ if (isSharedStackRunning()) {
1883
+ if (!opts.isJson) {
1884
+ ok("Observability stack: already running (shared)");
1885
+ if (opts.appType === "web") {
1886
+ info("Web app detected \u2014 verify OTel Collector has CORS enabled");
1887
+ }
1888
+ }
1889
+ const docker2 = {
1890
+ compose_file: sharedComposeFile,
1891
+ stack_running: true,
1892
+ services: [],
1893
+ ports: DEFAULT_PORTS
1894
+ };
1895
+ state = {
1896
+ ...state,
1897
+ otlp: { ...state.otlp, mode: "local-shared" },
1898
+ docker: {
1899
+ compose_file: sharedComposeFile,
1900
+ stack_running: true,
1901
+ ports: DEFAULT_PORTS
1902
+ }
1903
+ };
1904
+ writeState(state, opts.projectDir);
1905
+ return ok2({ docker: docker2, state });
1906
+ }
1907
+ const startResult = startSharedStack();
1908
+ if (startResult.started) {
1909
+ if (!opts.isJson) {
1910
+ ok("Observability stack: started (shared at ~/.codeharness/stack/)");
1911
+ }
1912
+ const docker2 = {
1913
+ compose_file: sharedComposeFile,
1914
+ stack_running: true,
1915
+ services: startResult.services,
1916
+ ports: DEFAULT_PORTS
1917
+ };
1918
+ state = {
1919
+ ...state,
1920
+ otlp: { ...state.otlp, mode: "local-shared" },
1921
+ docker: {
1922
+ compose_file: sharedComposeFile,
1923
+ stack_running: true,
1924
+ ports: DEFAULT_PORTS
1925
+ }
1926
+ };
1927
+ writeState(state, opts.projectDir);
1928
+ return ok2({ docker: docker2, state });
1929
+ }
1930
+ if (!opts.isJson) {
1931
+ fail("Observability stack: failed to start");
1932
+ if (startResult.error) {
1933
+ info(`Error: ${startResult.error}`);
1934
+ }
1935
+ }
1936
+ const docker = {
1937
+ compose_file: sharedComposeFile,
1938
+ stack_running: false,
1939
+ services: [],
1940
+ ports: DEFAULT_PORTS
1941
+ };
1942
+ return ok2({ docker, state });
1943
+ }
1944
+
1945
+ // src/lib/deps.ts
1946
+ import { execFileSync as execFileSync8 } from "child_process";
1947
+ var DEPENDENCY_REGISTRY = [
1948
+ {
1949
+ name: "showboat",
1950
+ displayName: "Showboat",
1951
+ installCommands: [
1952
+ { cmd: "pip", args: ["install", "showboat"] },
1953
+ { cmd: "pipx", args: ["install", "showboat"] }
1954
+ ],
1955
+ checkCommand: { cmd: "showboat", args: ["--version"] },
1956
+ critical: false
1957
+ },
1958
+ {
1959
+ name: "agent-browser",
1960
+ displayName: "agent-browser",
1961
+ installCommands: [
1962
+ { cmd: "npm", args: ["install", "-g", "@anthropic/agent-browser"] }
1963
+ ],
1964
+ checkCommand: { cmd: "agent-browser", args: ["--version"] },
1965
+ critical: false
1966
+ },
1967
+ {
1968
+ name: "beads",
1969
+ displayName: "beads",
1970
+ installCommands: [
1971
+ { cmd: "pip", args: ["install", "beads"] },
1972
+ { cmd: "pipx", args: ["install", "beads"] }
1973
+ ],
1974
+ checkCommand: { cmd: "bd", args: ["--version"] },
1975
+ critical: false
1976
+ },
1977
+ {
1978
+ name: "semgrep",
1979
+ displayName: "Semgrep",
1980
+ installCommands: [
1981
+ { cmd: "pipx", args: ["install", "semgrep"] },
1982
+ { cmd: "pip", args: ["install", "semgrep"] }
1983
+ ],
1984
+ checkCommand: { cmd: "semgrep", args: ["--version"] },
1985
+ critical: false
1986
+ },
1987
+ {
1988
+ name: "bats",
1989
+ displayName: "BATS",
1990
+ installCommands: [
1991
+ { cmd: "brew", args: ["install", "bats-core"] },
1992
+ { cmd: "npm", args: ["install", "-g", "bats"] }
1993
+ ],
1994
+ checkCommand: { cmd: "bats", args: ["--version"] },
1995
+ critical: false
1996
+ },
1997
+ {
1998
+ name: "cargo-tarpaulin",
1999
+ displayName: "cargo-tarpaulin",
2000
+ installCommands: [{ cmd: "cargo", args: ["install", "cargo-tarpaulin"] }],
2001
+ checkCommand: { cmd: "cargo", args: ["tarpaulin", "--version"] },
2002
+ critical: false
2003
+ }
2004
+ ];
2005
+ function checkInstalled(spec) {
2006
+ try {
2007
+ const output = execFileSync8(spec.checkCommand.cmd, spec.checkCommand.args, { stdio: "pipe", timeout: 15e3 }).toString().trim();
2008
+ const version = parseVersion(output);
2009
+ return { installed: true, version };
2010
+ } catch {
2011
+ return { installed: false, version: null };
2012
+ }
2013
+ }
2014
+ function parseVersion(output) {
2015
+ const match = /(\d+\.\d+[\w.-]*)/.exec(output);
2016
+ return match ? match[1] : null;
2017
+ }
2018
+ function installDependency(spec) {
2019
+ const check = checkInstalled(spec);
2020
+ if (check.installed) {
2021
+ return {
2022
+ name: spec.name,
2023
+ displayName: spec.displayName,
2024
+ status: "already-installed",
2025
+ version: check.version
2026
+ };
2027
+ }
2028
+ for (const installCmd of spec.installCommands) {
2029
+ try {
2030
+ execFileSync8(installCmd.cmd, installCmd.args, { stdio: "pipe", timeout: 3e5 });
2031
+ const postCheck = checkInstalled(spec);
2032
+ if (postCheck.installed) {
2033
+ return {
2034
+ name: spec.name,
2035
+ displayName: spec.displayName,
2036
+ status: "installed",
2037
+ version: postCheck.version
2038
+ };
2039
+ }
2040
+ } catch {
2041
+ continue;
2042
+ }
2043
+ }
2044
+ const remedy = spec.installCommands.map((c) => [c.cmd, ...c.args].join(" ")).join(" or ");
2045
+ return {
2046
+ name: spec.name,
2047
+ displayName: spec.displayName,
2048
+ status: "failed",
2049
+ version: null,
2050
+ error: `Install failed. Try: ${remedy}`
2051
+ };
2052
+ }
2053
+ function installAllDependencies(opts) {
2054
+ const results = [];
2055
+ for (const spec of DEPENDENCY_REGISTRY) {
2056
+ const result = installDependency(spec);
2057
+ results.push(result);
2058
+ if (!opts.json) {
2059
+ if (result.status === "installed") {
2060
+ const versionStr = result.version ? ` (v${result.version})` : "";
2061
+ ok(`${spec.displayName}: installed${versionStr}`);
2062
+ } else if (result.status === "already-installed") {
2063
+ const versionStr = result.version ? ` (v${result.version})` : "";
2064
+ ok(`${spec.displayName}: already installed${versionStr}`);
2065
+ } else if (result.status === "failed") {
2066
+ fail(`${spec.displayName}: install failed. ${result.error ?? ""}`);
2067
+ if (!spec.critical) {
2068
+ info(`${spec.displayName} is optional \u2014 continuing without it`);
2069
+ }
2070
+ }
2071
+ }
2072
+ if (result.status === "failed" && spec.critical) {
2073
+ throw new CriticalDependencyError(spec.displayName, result.error ?? "Install failed");
2074
+ }
2075
+ }
2076
+ return results;
2077
+ }
2078
+ var CriticalDependencyError = class extends Error {
2079
+ constructor(dependencyName, reason) {
2080
+ super(`Critical dependency '${dependencyName}' failed to install: ${reason}`);
2081
+ this.dependencyName = dependencyName;
2082
+ this.reason = reason;
2083
+ this.name = "CriticalDependencyError";
2084
+ }
2085
+ };
2086
+
2087
+ // src/modules/infra/deps-install.ts
2088
+ function installDeps(opts) {
2089
+ try {
2090
+ const depResults = installAllDependencies({ json: opts.isJson });
2091
+ return ok2(depResults);
2092
+ } catch (err) {
2093
+ if (err instanceof CriticalDependencyError) {
2094
+ return fail2(err.message);
2095
+ }
2096
+ const message = err instanceof Error ? err.message : String(err);
2097
+ return fail2(`Dependency install error: ${message}`);
2098
+ }
2099
+ }
2100
+ function verifyDeps(isJson) {
2101
+ const depResults = [];
2102
+ for (const spec of DEPENDENCY_REGISTRY) {
2103
+ const check = checkInstalled(spec);
2104
+ const depResult = {
2105
+ name: spec.name,
2106
+ displayName: spec.displayName,
2107
+ status: check.installed ? "already-installed" : "failed",
2108
+ version: check.version
2109
+ };
2110
+ depResults.push(depResult);
2111
+ if (!isJson) {
2112
+ if (check.installed) {
2113
+ const versionStr = check.version ? ` (v${check.version})` : "";
2114
+ ok(`${spec.displayName}: already installed${versionStr}`);
2115
+ } else {
2116
+ fail(`${spec.displayName}: not found`);
2117
+ }
2118
+ }
2119
+ }
2120
+ return depResults;
2121
+ }
2122
+
2123
+ // src/lib/bmad.ts
2124
+ import { execFileSync as execFileSync10 } from "child_process";
2125
+ import { existsSync as existsSync11, readFileSync as readFileSync11 } from "fs";
2126
+ import { join as join12 } from "path";
2127
+
2128
+ // src/lib/patch-engine.ts
2129
+ import { readFileSync as readFileSync9, writeFileSync as writeFileSync7 } from "fs";
2130
+ function validatePatchName(patchName) {
2131
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(patchName)) {
2132
+ throw new Error(
2133
+ `Invalid patch name '${patchName}': must be kebab-case (lowercase letters, digits, hyphens)`
2134
+ );
2135
+ }
2136
+ }
2137
+ function getPatchMarkers(patchName) {
2138
+ validatePatchName(patchName);
2139
+ return {
2140
+ start: `<!-- CODEHARNESS-PATCH-START:${patchName} -->`,
2141
+ end: `<!-- CODEHARNESS-PATCH-END:${patchName} -->`
2142
+ };
2143
+ }
2144
+ function applyPatch(filePath, patchName, patchContent) {
2145
+ let content = readFileSync9(filePath, "utf-8");
2146
+ const markers = getPatchMarkers(patchName);
2147
+ const markerBlock = `${markers.start}
2148
+ ${patchContent}
2149
+ ${markers.end}`;
2150
+ const startIdx = content.indexOf(markers.start);
2151
+ const endIdx = content.indexOf(markers.end);
2152
+ if (startIdx !== -1 !== (endIdx !== -1)) {
2153
+ throw new Error(
2154
+ `Corrupted patch markers for '${patchName}': only ${startIdx !== -1 ? "start" : "end"} marker found in ${filePath}`
2155
+ );
2156
+ }
2157
+ if (startIdx !== -1 && endIdx !== -1) {
2158
+ if (endIdx < startIdx) {
2159
+ throw new Error(
2160
+ `Corrupted patch markers for '${patchName}': end marker appears before start marker in ${filePath}`
2161
+ );
2162
+ }
2163
+ const before = content.slice(0, startIdx);
2164
+ const after = content.slice(endIdx + markers.end.length);
2165
+ content = before + markerBlock + after;
2166
+ writeFileSync7(filePath, content, "utf-8");
2167
+ return { applied: true, updated: true };
2168
+ }
2169
+ const trimmed = content.trimEnd();
2170
+ content = trimmed + "\n\n" + markerBlock + "\n";
2171
+ writeFileSync7(filePath, content, "utf-8");
2172
+ return { applied: true, updated: false };
2173
+ }
2174
+ function removePatch(filePath, patchName) {
2175
+ let content = readFileSync9(filePath, "utf-8");
2176
+ const markers = getPatchMarkers(patchName);
2177
+ const startIdx = content.indexOf(markers.start);
2178
+ const endIdx = content.indexOf(markers.end);
2179
+ if (startIdx === -1 || endIdx === -1) {
2180
+ return false;
2181
+ }
2182
+ if (endIdx < startIdx) {
2183
+ throw new Error(
2184
+ `Corrupted patch markers for '${patchName}': end marker appears before start marker in ${filePath}`
2185
+ );
2186
+ }
2187
+ const before = content.slice(0, startIdx);
2188
+ const after = content.slice(endIdx + markers.end.length);
2189
+ content = before.trimEnd() + "\n" + after.trimStart();
2190
+ writeFileSync7(filePath, content, "utf-8");
2191
+ return true;
2192
+ }
2193
+
2194
+ // src/templates/bmad-patches.ts
2195
+ import { readFileSync as readFileSync10, existsSync as existsSync9 } from "fs";
2196
+ import { join as join10, dirname as dirname2 } from "path";
2197
+ import { fileURLToPath as fileURLToPath2 } from "url";
2198
+ var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
2199
+ function readPatchFile(role, name) {
2200
+ const patchPath = join10(__dirname2, "..", "..", "patches", role, `${name}.md`);
2201
+ try {
2202
+ if (existsSync9(patchPath)) {
2203
+ return readFileSync10(patchPath, "utf-8").trim();
2204
+ }
2205
+ } catch {
2206
+ }
2207
+ return null;
2208
+ }
2209
+ function storyVerificationPatch() {
2210
+ return readPatchFile("verify", "story-verification") ?? `## Verification Requirements
2211
+
2212
+ - [ ] Showboat proof document created (verification/<story-key>-proof.md)
2213
+ - [ ] All acceptance criteria verified with real-world evidence via docker exec
2214
+ - [ ] Test coverage meets target (100%)
2215
+
2216
+ ## Documentation Requirements
2217
+
2218
+ - [ ] Relevant AGENTS.md files updated
2219
+ - [ ] Exec-plan created in docs/exec-plans/active/<story-key>.md
2220
+
2221
+ ## Testing Requirements
2222
+
2223
+ - [ ] Unit tests written for all new/changed code
2224
+ - [ ] Coverage target: 100%`;
2225
+ }
2226
+ function devEnforcementPatch() {
2227
+ return readPatchFile("dev", "enforcement") ?? `## Codeharness Enforcement
2228
+
2229
+ ### Observability Check
2230
+ - [ ] Query VictoriaLogs after test runs to verify telemetry flows
2231
+
2232
+ ### Documentation Update
2233
+ - [ ] AGENTS.md updated for all changed modules
2234
+
2235
+ ### Test Enforcement
2236
+ - [ ] All tests pass
2237
+ - [ ] Coverage gate: 100% of new/changed code`;
2238
+ }
2239
+ function reviewEnforcementPatch() {
2240
+ return readPatchFile("review", "enforcement") ?? `## Codeharness Review Gates
2241
+
2242
+ ### Verification
2243
+ - [ ] Proof document exists and passes codeharness verify
2244
+ - [ ] All acceptance criteria have evidence in proof document
2245
+
2246
+ ### Coverage
2247
+ - [ ] No coverage regression in changed files`;
2248
+ }
2249
+ function retroEnforcementPatch() {
2250
+ return readPatchFile("retro", "enforcement") ?? `## Codeharness Quality Metrics
2251
+
2252
+ ### Verification Effectiveness
2253
+ - [ ] How many ACs were caught by verification vs manual review?
2254
+ - [ ] Were there any false positives in proofs?
2255
+
2256
+ ### Test Quality
2257
+ - [ ] Coverage trend (improving, stable, declining)`;
2258
+ }
2259
+ function sprintBeadsPatch() {
2260
+ return readPatchFile("sprint", "planning") ?? `## Codeharness Sprint Planning
2261
+
2262
+ - [ ] Review unresolved retrospective action items
2263
+ - [ ] Import from all backlog sources before triage
2264
+ - [ ] Verify story ACs are testable via CLI + Docker`;
2265
+ }
2266
+ function sprintPlanningRetroPatch() {
2267
+ return sprintBeadsPatch();
2268
+ }
2269
+ var PATCH_TEMPLATES = {
2270
+ "story-verification": storyVerificationPatch,
2271
+ "dev-enforcement": devEnforcementPatch,
2272
+ "review-enforcement": reviewEnforcementPatch,
2273
+ "retro-enforcement": retroEnforcementPatch,
2274
+ "sprint-beads": sprintBeadsPatch,
2275
+ "sprint-retro": sprintPlanningRetroPatch
2276
+ };
2277
+
2278
+ // src/lib/beads.ts
2279
+ import { execFileSync as execFileSync9 } from "child_process";
2280
+ import { existsSync as existsSync10, readdirSync as readdirSync2 } from "fs";
2281
+ import { join as join11 } from "path";
2282
+ var BeadsError = class extends Error {
2283
+ constructor(command, originalMessage) {
2284
+ super(`Beads failed: ${originalMessage}. Command: ${command}`);
2285
+ this.command = command;
2286
+ this.originalMessage = originalMessage;
2287
+ this.name = "BeadsError";
2288
+ }
2289
+ };
2290
+ function isBeadsCLIInstalled() {
2291
+ try {
2292
+ execFileSync9("which", ["bd"], { stdio: "pipe", timeout: 5e3 });
2293
+ return true;
2294
+ } catch {
2295
+ return false;
2296
+ }
2297
+ }
2298
+ function bdCommand(args) {
2299
+ const cmdStr = `bd ${args.join(" ")}`;
2300
+ let text;
2301
+ try {
2302
+ const output = execFileSync9("bd", args, {
2303
+ stdio: "pipe",
2304
+ timeout: 3e4
2305
+ });
2306
+ text = output.toString().trim();
2307
+ } catch (err) {
2308
+ const message = err instanceof Error ? err.message : String(err);
2309
+ throw new BeadsError(cmdStr, message);
2310
+ }
2311
+ if (!text) return void 0;
2312
+ try {
2313
+ return JSON.parse(text);
2314
+ } catch {
2315
+ throw new BeadsError(cmdStr, `Invalid JSON output from bd: ${text}`);
2316
+ }
2317
+ }
2318
+ function createIssue(title, opts) {
2319
+ const args = ["create", title, "--json"];
2320
+ if (opts?.type) {
2321
+ args.push("--type", opts.type);
2322
+ }
2323
+ if (opts?.priority !== void 0) {
2324
+ args.push("--priority", String(opts.priority));
2325
+ }
2326
+ if (opts?.description) {
2327
+ args.push("--description", opts.description);
2328
+ }
2329
+ if (opts?.deps && opts.deps.length > 0) {
2330
+ for (const dep of opts.deps) {
2331
+ args.push("--dep", dep);
2332
+ }
2333
+ }
2334
+ return bdCommand(args);
2335
+ }
2336
+ function closeIssue(id) {
2337
+ bdCommand(["close", id, "--json"]);
2338
+ }
2339
+ function updateIssue(id, opts) {
2340
+ const args = ["update", id, "--json"];
2341
+ if (opts.status !== void 0) {
2342
+ args.push("--status", opts.status);
2343
+ }
2344
+ if (opts.priority !== void 0) {
2345
+ args.push("--priority", String(opts.priority));
2346
+ }
2347
+ bdCommand(args);
2348
+ }
2349
+ function listIssues() {
2350
+ return bdCommand(["list", "--json"]);
2351
+ }
2352
+ function isBeadsInitialized(dir) {
2353
+ const beadsDir = join11(dir ?? process.cwd(), ".beads");
2354
+ return existsSync10(beadsDir);
2355
+ }
2356
+ function initBeads(dir) {
2357
+ if (isBeadsInitialized(dir)) {
2358
+ return;
2359
+ }
2360
+ const cmdStr = "bd init";
2361
+ try {
2362
+ execFileSync9("bd", ["init"], {
2363
+ stdio: "pipe",
2364
+ timeout: 3e4,
2365
+ cwd: dir ?? process.cwd()
2366
+ });
2367
+ } catch (err) {
2368
+ const message = err instanceof Error ? err.message : String(err);
2369
+ throw new BeadsError(cmdStr, message);
2370
+ }
2371
+ }
2372
+ function detectBeadsHooks(dir) {
2373
+ const hooksDir = join11(dir ?? process.cwd(), ".beads", "hooks");
2374
+ if (!existsSync10(hooksDir)) {
2375
+ return { hasHooks: false, hookTypes: [] };
2376
+ }
2377
+ try {
2378
+ const entries = readdirSync2(hooksDir);
2379
+ const hookTypes = entries.filter((e) => !e.startsWith("."));
2380
+ return {
2381
+ hasHooks: hookTypes.length > 0,
2382
+ hookTypes
2383
+ };
2384
+ } catch {
2385
+ return { hasHooks: false, hookTypes: [] };
2386
+ }
2387
+ }
2388
+ function buildGapId(category, identifier) {
2389
+ return `[gap:${category}:${identifier}]`;
2390
+ }
2391
+ function findExistingByGapId(gapId, issues) {
2392
+ return issues.find(
2393
+ (issue) => issue.status !== "done" && issue.description?.includes(gapId)
2394
+ );
2395
+ }
2396
+ function appendGapId(existingDescription, gapId) {
2397
+ if (!existingDescription) {
2398
+ return gapId;
2399
+ }
2400
+ return `${existingDescription}
2401
+ ${gapId}`;
2402
+ }
2403
+ function createOrFindIssue(title, gapId, opts) {
2404
+ const issues = listIssues();
2405
+ const existing = findExistingByGapId(gapId, issues);
2406
+ if (existing) {
2407
+ return { issue: existing, created: false };
2408
+ }
2409
+ const issue = createIssue(title, {
2410
+ ...opts,
2411
+ description: appendGapId(opts?.description, gapId)
2412
+ });
2413
+ return { issue, created: true };
2414
+ }
2415
+ function configureHookCoexistence(dir) {
2416
+ const detection = detectBeadsHooks(dir);
2417
+ if (!detection.hasHooks) {
2418
+ return;
2419
+ }
2420
+ const gitHooksDir = join11(dir ?? process.cwd(), ".git", "hooks");
2421
+ if (!existsSync10(gitHooksDir)) {
2422
+ return;
2423
+ }
2424
+ }
2425
+
2426
+ // src/lib/bmad.ts
2427
+ var BmadError = class extends Error {
2428
+ constructor(command, originalMessage) {
2429
+ super(`BMAD failed: ${originalMessage}. Command: ${command}`);
2430
+ this.command = command;
2431
+ this.originalMessage = originalMessage;
2432
+ this.name = "BmadError";
2433
+ }
2434
+ };
2435
+ var PATCH_TARGETS = {
2436
+ "story-verification": "bmm/workflows/4-implementation/create-story/template.md",
2437
+ "dev-enforcement": "bmm/workflows/4-implementation/dev-story/instructions.xml",
2438
+ "review-enforcement": "bmm/workflows/4-implementation/code-review/instructions.xml",
2439
+ "retro-enforcement": "bmm/workflows/4-implementation/retrospective/instructions.md",
2440
+ "sprint-beads": "bmm/workflows/4-implementation/sprint-planning/checklist.md",
2441
+ "sprint-retro": "bmm/workflows/4-implementation/sprint-planning/instructions.md"
2442
+ };
2443
+ function isBmadInstalled(dir) {
2444
+ const bmadDir = join12(dir ?? process.cwd(), "_bmad");
2445
+ return existsSync11(bmadDir);
2446
+ }
2447
+ function detectBmadVersion(dir) {
2448
+ const root = dir ?? process.cwd();
2449
+ const moduleYamlPath = join12(root, "_bmad", "core", "module.yaml");
2450
+ if (existsSync11(moduleYamlPath)) {
2451
+ try {
2452
+ const content = readFileSync11(moduleYamlPath, "utf-8");
2453
+ const versionMatch = content.match(/version:\s*["']?([^\s"']+)["']?/);
2454
+ if (versionMatch) {
2455
+ return versionMatch[1];
2456
+ }
2457
+ } catch {
2458
+ }
2459
+ }
2460
+ const versionFilePath = join12(root, "_bmad", "VERSION");
2461
+ if (existsSync11(versionFilePath)) {
2462
+ try {
2463
+ return readFileSync11(versionFilePath, "utf-8").trim() || null;
2464
+ } catch {
2465
+ }
2466
+ }
2467
+ const packageJsonPath = join12(root, "_bmad", "package.json");
2468
+ if (existsSync11(packageJsonPath)) {
2469
+ try {
2470
+ const pkg = JSON.parse(readFileSync11(packageJsonPath, "utf-8"));
2471
+ return pkg.version ?? null;
2472
+ } catch {
2473
+ }
2474
+ }
2475
+ return null;
2476
+ }
2477
+ function installBmad(dir) {
2478
+ const root = dir ?? process.cwd();
2479
+ if (isBmadInstalled(root)) {
2480
+ const version2 = detectBmadVersion(root);
2481
+ return {
2482
+ status: "already-installed",
2483
+ version: version2,
2484
+ patches_applied: []
2485
+ };
2486
+ }
2487
+ const cmdStr = "npx bmad-method install --yes --tools claude-code";
2488
+ try {
2489
+ execFileSync10("npx", ["bmad-method", "install", "--yes", "--tools", "claude-code"], {
2490
+ stdio: "pipe",
2491
+ timeout: 12e4,
2492
+ // 2 min — npx may need to download the package first time
2493
+ cwd: root
2494
+ });
2495
+ } catch (err) {
2496
+ const message = err instanceof Error ? err.message : String(err);
2497
+ throw new BmadError(cmdStr, message);
2498
+ }
2499
+ if (!isBmadInstalled(root)) {
2500
+ throw new BmadError(cmdStr, "_bmad/ directory was not created after successful npx bmad-method install");
2501
+ }
2502
+ const version = detectBmadVersion(root);
2503
+ return {
2504
+ status: "installed",
2505
+ version,
2506
+ patches_applied: []
2507
+ };
2508
+ }
2509
+ function applyAllPatches(dir, options) {
2510
+ const root = dir ?? process.cwd();
2511
+ const silent = options?.silent ?? false;
2512
+ const results = [];
2513
+ for (const [patchName, relativePath] of Object.entries(PATCH_TARGETS)) {
2514
+ const targetFile = join12(root, "_bmad", relativePath);
2515
+ if (!existsSync11(targetFile)) {
2516
+ if (!silent) warn(`Patch target not found: ${relativePath}`);
2517
+ results.push({
2518
+ patchName,
2519
+ targetFile,
2520
+ applied: false,
2521
+ updated: false,
2522
+ error: `File not found: ${relativePath}`
2523
+ });
2524
+ continue;
2525
+ }
2526
+ const templateFn = PATCH_TEMPLATES[patchName];
2527
+ if (!templateFn) {
2528
+ results.push({
2529
+ patchName,
2530
+ targetFile,
2531
+ applied: false,
2532
+ updated: false,
2533
+ error: `No template function for patch: ${patchName}`
2534
+ });
2535
+ continue;
2536
+ }
2537
+ try {
2538
+ const patchContent = templateFn();
2539
+ const patchResult = applyPatch(targetFile, patchName, patchContent);
2540
+ results.push({
2541
+ patchName,
2542
+ targetFile,
2543
+ applied: patchResult.applied,
2544
+ updated: patchResult.updated
2545
+ });
2546
+ } catch (err) {
2547
+ const message = err instanceof Error ? err.message : String(err);
2548
+ results.push({
2549
+ patchName,
2550
+ targetFile,
2551
+ applied: false,
2552
+ updated: false,
2553
+ error: message
2554
+ });
2555
+ }
2556
+ }
2557
+ return results;
2558
+ }
2559
+ function detectBmalph(dir) {
2560
+ const root = dir ?? process.cwd();
2561
+ const files = [];
2562
+ const ralphRcPath = join12(root, ".ralph", ".ralphrc");
2563
+ if (existsSync11(ralphRcPath)) {
2564
+ files.push(".ralph/.ralphrc");
2565
+ }
2566
+ const dotRalphDir = join12(root, ".ralph");
2567
+ if (existsSync11(dotRalphDir)) {
2568
+ if (files.length === 0) {
2569
+ files.push(".ralph/");
2570
+ }
2571
+ }
2572
+ return { detected: files.length > 0, files };
2573
+ }
2574
+ function generateStoryKey(epicNumber, storyNumber, title) {
2575
+ const slug = title.toLowerCase().replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2576
+ return `${epicNumber}-${storyNumber}-${slug}`;
2577
+ }
2578
+ function getStoryFilePath(storyKey) {
2579
+ return `_bmad-output/implementation-artifacts/${storyKey}.md`;
2580
+ }
2581
+ function parseEpicsFile(filePath) {
2582
+ if (!existsSync11(filePath)) {
2583
+ return [];
2584
+ }
2585
+ const content = readFileSync11(filePath, "utf-8");
2586
+ if (!content.trim()) {
2587
+ return [];
2588
+ }
2589
+ const lines = content.split("\n");
2590
+ const epics = [];
2591
+ let currentEpic = null;
2592
+ let currentStory = null;
2593
+ for (let i = 0; i < lines.length; i++) {
2594
+ const line = lines[i];
2595
+ const epicMatch = line.match(/^#{2,3}\s+Epic\s+(\d+):\s*(.+)$/);
2596
+ if (epicMatch) {
2597
+ if (currentStory && currentEpic) {
2598
+ currentEpic.stories.push(finalizeStory(currentStory));
2599
+ currentStory = null;
2600
+ }
2601
+ const epicNum = parseInt(epicMatch[1], 10);
2602
+ const epicTitle = epicMatch[2].trim();
2603
+ const existingIdx = epics.findIndex((e) => e.number === epicNum);
2604
+ if (existingIdx !== -1) {
2605
+ currentEpic = {
2606
+ number: epicNum,
2607
+ title: epicTitle,
2608
+ stories: []
2609
+ };
2610
+ epics[existingIdx] = currentEpic;
2611
+ } else {
2612
+ currentEpic = {
2613
+ number: epicNum,
2614
+ title: epicTitle,
2615
+ stories: []
2616
+ };
2617
+ epics.push(currentEpic);
2618
+ }
2619
+ continue;
2620
+ }
2621
+ const storyMatch = line.match(/^###\s+Story\s+(\d+)\.(\d+):\s*(.+)$/);
2622
+ if (storyMatch) {
2623
+ if (currentStory && currentEpic) {
2624
+ currentEpic.stories.push(finalizeStory(currentStory));
2625
+ }
2626
+ currentStory = {
2627
+ epicNumber: parseInt(storyMatch[1], 10),
2628
+ storyNumber: parseInt(storyMatch[2], 10),
2629
+ title: storyMatch[3].trim(),
2630
+ lines: []
2631
+ };
2632
+ continue;
2633
+ }
2634
+ if (currentStory) {
2635
+ currentStory.lines.push(line);
2636
+ }
2637
+ }
2638
+ if (currentStory && currentEpic) {
2639
+ currentEpic.stories.push(finalizeStory(currentStory));
2640
+ }
2641
+ return epics;
2642
+ }
2643
+ function finalizeStory(raw) {
2644
+ const body = raw.lines.join("\n");
2645
+ const userStoryMatch = body.match(
2646
+ /As\s+a[n]?\s+.+?,\s*\n\s*I\s+want\s+.+?,\s*\n\s*So\s+that\s+.+?\./s
2647
+ );
2648
+ const userStory = userStoryMatch ? userStoryMatch[0].trim() : "";
2649
+ const acceptanceCriteria = [];
2650
+ const acBlockRegex = /\*\*Given\*\*\s+.+?(?=\n\n\*\*Given\*\*|\n\n\*\*Technical\s+notes|\n\n---|\n\n##|\n\n###|$)/gs;
2651
+ let acMatch;
2652
+ while ((acMatch = acBlockRegex.exec(body)) !== null) {
2653
+ acceptanceCriteria.push(acMatch[0].trim());
2654
+ }
2655
+ let technicalNotes = null;
2656
+ const techMatch = body.match(/\*\*Technical\s+notes:\*\*\s*\n([\s\S]*?)(?=\n\n---|\n\n##|\n\n###|$)/);
2657
+ if (techMatch) {
2658
+ technicalNotes = techMatch[1].trim() || null;
2659
+ }
2660
+ const key = generateStoryKey(raw.epicNumber, raw.storyNumber, raw.title);
2661
+ return {
2662
+ epicNumber: raw.epicNumber,
2663
+ storyNumber: raw.storyNumber,
2664
+ key,
2665
+ title: raw.title,
2666
+ userStory,
2667
+ acceptanceCriteria,
2668
+ technicalNotes
2669
+ };
2670
+ }
2671
+ function importStoriesToBeads(stories, opts, beadsFns) {
2672
+ const results = [];
2673
+ let existingIssues = [];
2674
+ try {
2675
+ existingIssues = beadsFns.listIssues();
2676
+ } catch {
2677
+ }
2678
+ const lastBeadsIdByEpic = /* @__PURE__ */ new Map();
2679
+ let priority = 1;
2680
+ for (const story of stories) {
2681
+ const storyFilePath = getStoryFilePath(story.key);
2682
+ const gapId = buildGapId("bridge", `${story.epicNumber}.${story.storyNumber}`);
2683
+ const existingIssue = findExistingByGapId(gapId, existingIssues);
2684
+ if (existingIssue) {
2685
+ lastBeadsIdByEpic.set(story.epicNumber, existingIssue.id);
2686
+ results.push({
2687
+ storyKey: story.key,
2688
+ title: story.title,
2689
+ beadsId: existingIssue.id,
2690
+ status: "exists",
2691
+ storyFilePath
2692
+ });
2693
+ priority++;
2694
+ continue;
2695
+ }
2696
+ if (opts.dryRun) {
2697
+ results.push({
2698
+ storyKey: story.key,
2699
+ title: story.title,
2700
+ beadsId: null,
2701
+ status: "skipped",
2702
+ storyFilePath
2703
+ });
2704
+ priority++;
2705
+ continue;
2706
+ }
2707
+ const deps = [];
2708
+ const prevId = lastBeadsIdByEpic.get(story.epicNumber);
2709
+ if (prevId) {
2710
+ deps.push(prevId);
2711
+ }
2712
+ try {
2713
+ const description = appendGapId(storyFilePath, gapId);
2714
+ const issue = beadsFns.createIssue(story.title, {
2715
+ type: "story",
2716
+ priority,
2717
+ description,
2718
+ deps: deps.length > 0 ? deps : void 0
2719
+ });
2720
+ lastBeadsIdByEpic.set(story.epicNumber, issue.id);
2721
+ results.push({
2722
+ storyKey: story.key,
2723
+ title: story.title,
2724
+ beadsId: issue.id,
2725
+ status: "created",
2726
+ storyFilePath
2727
+ });
2728
+ } catch (err) {
2729
+ const message = err instanceof Error ? err.message : String(err);
2730
+ results.push({
2731
+ storyKey: story.key,
2732
+ title: story.title,
2733
+ beadsId: null,
2734
+ status: "failed",
2735
+ storyFilePath,
2736
+ error: message
2737
+ });
2738
+ }
2739
+ priority++;
2740
+ }
2741
+ return results;
2742
+ }
2743
+
2744
+ // src/modules/infra/bmad-setup.ts
2745
+ function setupBmad(opts) {
2746
+ try {
2747
+ const bmadAlreadyInstalled = isBmadInstalled(opts.projectDir);
2748
+ let bmadResult;
2749
+ if (bmadAlreadyInstalled) {
2750
+ const version = detectBmadVersion(opts.projectDir);
2751
+ const patchResults = applyAllPatches(opts.projectDir, { silent: opts.isJson });
2752
+ const patchNames = patchResults.filter((r) => r.applied).map((r) => r.patchName);
2753
+ bmadResult = {
2754
+ status: "already-installed",
2755
+ version,
2756
+ patches_applied: patchNames,
2757
+ bmalph_detected: false
2758
+ };
2759
+ if (!opts.isJson) {
2760
+ info("BMAD: already installed, patches verified");
2761
+ }
2762
+ } else {
2763
+ const installResult = installBmad(opts.projectDir);
2764
+ const patchResults = applyAllPatches(opts.projectDir, { silent: opts.isJson });
2765
+ const patchNames = patchResults.filter((r) => r.applied).map((r) => r.patchName);
2766
+ bmadResult = {
2767
+ status: installResult.status,
2768
+ version: installResult.version,
2769
+ patches_applied: patchNames,
2770
+ bmalph_detected: false
2771
+ };
2772
+ if (!opts.isJson) {
2773
+ ok(`BMAD: installed (v${installResult.version ?? "unknown"}), harness patches applied`);
2774
+ }
2775
+ }
2776
+ const bmalpHDetection = detectBmalph(opts.projectDir);
2777
+ if (bmalpHDetection.detected) {
2778
+ bmadResult = { ...bmadResult, bmalph_detected: true };
2779
+ if (!opts.isJson) {
2780
+ warn("bmalph detected \u2014 superseded files noted for cleanup");
2781
+ }
2782
+ }
2783
+ return ok2(bmadResult);
2784
+ } catch (err) {
2785
+ const message = err instanceof Error ? err.message : String(err);
2786
+ const bmadResult = {
2787
+ status: "failed",
2788
+ version: null,
2789
+ patches_applied: [],
2790
+ bmalph_detected: false,
2791
+ error: message
2792
+ };
2793
+ if (!opts.isJson) {
2794
+ fail(`BMAD install failed: ${message}`);
2795
+ }
2796
+ return ok2(bmadResult);
2797
+ }
2798
+ }
2799
+ function verifyBmadOnRerun(projectDir, isJson) {
2800
+ if (!isBmadInstalled(projectDir)) {
2801
+ return void 0;
2802
+ }
2803
+ try {
2804
+ const patchResults = applyAllPatches(projectDir, { silent: isJson });
2805
+ const patchNames = patchResults.filter((r) => r.applied).map((r) => r.patchName);
2806
+ const version = detectBmadVersion(projectDir);
2807
+ const bmalpHDetection = detectBmalph(projectDir);
2808
+ const result = {
2809
+ status: "already-installed",
2810
+ version,
2811
+ patches_applied: patchNames,
2812
+ bmalph_detected: bmalpHDetection.detected
2813
+ };
2814
+ if (!isJson) {
2815
+ info("BMAD: already installed, patches verified");
2816
+ if (bmalpHDetection.detected) {
2817
+ warn("bmalph detected \u2014 superseded files noted for cleanup");
2818
+ }
2819
+ }
2820
+ return result;
2821
+ } catch {
2822
+ return void 0;
2823
+ }
2824
+ }
2825
+
2826
+ // src/modules/infra/beads-init.ts
2827
+ function initializeBeads(projectDir, isJson) {
2828
+ try {
2829
+ let beadsResult;
2830
+ if (isBeadsInitialized(projectDir)) {
2831
+ beadsResult = { status: "already-initialized", hooks_detected: false };
2832
+ if (!isJson) {
2833
+ info("Beads: .beads/ already exists");
2834
+ }
2835
+ } else {
2836
+ initBeads(projectDir);
2837
+ beadsResult = { status: "initialized", hooks_detected: false };
2838
+ if (!isJson) {
2839
+ ok("Beads: initialized (.beads/ created)");
2840
+ }
2841
+ }
2842
+ const hookDetection = detectBeadsHooks(projectDir);
2843
+ beadsResult = { ...beadsResult, hooks_detected: hookDetection.hasHooks };
2844
+ if (hookDetection.hasHooks) {
2845
+ configureHookCoexistence(projectDir);
2846
+ if (!isJson) {
2847
+ info("Beads hooks detected \u2014 coexistence configured");
2848
+ }
2849
+ }
2850
+ return beadsResult;
2851
+ } catch (err) {
2852
+ const message = err instanceof Error ? err.message : String(err);
2853
+ const beadsResult = {
2854
+ status: "failed",
2855
+ hooks_detected: false,
2856
+ error: message
2857
+ };
2858
+ if (!isJson) {
2859
+ warn(`Beads init failed: ${message}`);
2860
+ info("Beads is optional \u2014 continuing without it");
2861
+ }
2862
+ return beadsResult;
2863
+ }
2864
+ }
2865
+
2866
+ // src/modules/infra/docs-scaffold.ts
2867
+ import { existsSync as existsSync12, readFileSync as readFileSync12 } from "fs";
2868
+ import { join as join13, basename as basename2 } from "path";
2869
+
2870
+ // src/templates/readme.ts
2871
+ function readmeTemplate(config) {
2872
+ const { projectName, stack, cliHelpOutput } = config;
2873
+ const installCommand = getInstallCommand(stack);
2874
+ return renderTemplateFile("templates/docs/readme.md.tmpl", {
2875
+ PROJECT_NAME: projectName,
2876
+ INSTALL_COMMAND: installCommand,
2877
+ CLI_HELP_OUTPUT: cliHelpOutput.trimEnd()
2878
+ });
2879
+ }
2880
+ function getInstallCommand(stack) {
2881
+ if (Array.isArray(stack)) {
2882
+ const commands = stack.map((s) => getSingleInstallCommand(s));
2883
+ return [...new Set(commands)].join("\n");
2884
+ }
2885
+ return getSingleInstallCommand(stack);
2886
+ }
2887
+ var INSTALL_COMMANDS = {
2888
+ python: "pip install codeharness",
2889
+ rust: "cargo install codeharness"
2890
+ };
2891
+ function getSingleInstallCommand(stack) {
2892
+ if (stack && INSTALL_COMMANDS[stack]) return INSTALL_COMMANDS[stack];
2893
+ return "npm install -g codeharness";
2894
+ }
2895
+
2896
+ // src/modules/infra/docs-scaffold.ts
2897
+ var DO_NOT_EDIT_HEADER = "<!-- DO NOT EDIT MANUALLY -->\n";
2898
+ function getProjectName(projectDir) {
2899
+ try {
2900
+ const pkgPath = join13(projectDir, "package.json");
2901
+ if (existsSync12(pkgPath)) {
2902
+ const pkg = JSON.parse(readFileSync12(pkgPath, "utf-8"));
2903
+ if (pkg.name && typeof pkg.name === "string") {
2904
+ return pkg.name;
2905
+ }
2906
+ }
2907
+ } catch {
2908
+ }
2909
+ try {
2910
+ const cargoPath = join13(projectDir, "Cargo.toml");
2911
+ if (existsSync12(cargoPath)) {
2912
+ const content = readFileSync12(cargoPath, "utf-8");
2913
+ const packageMatch = content.match(/\[package\]([\s\S]*?)(?=\n\[|$)/s);
2914
+ if (packageMatch) {
2915
+ const nameMatch = packageMatch[1].match(/^\s*name\s*=\s*["']([^"']+)["']/m);
2916
+ if (nameMatch) {
2917
+ return nameMatch[1];
2918
+ }
2919
+ }
2920
+ }
2921
+ } catch {
2922
+ }
2923
+ return basename2(projectDir);
2924
+ }
2925
+ function getStackLabel(stack) {
2926
+ if (Array.isArray(stack)) {
2927
+ if (stack.length === 0) return "Unknown";
2928
+ return stack.map((s) => getStackLabel(s)).join(" + ");
2929
+ }
2930
+ if (!stack) return "Unknown";
2931
+ const provider = getStackProvider(stack);
2932
+ return provider ? provider.displayName : "Unknown";
2933
+ }
2934
+ var STATE_COVERAGE_TOOLS = {
2935
+ nodejs: "c8",
2936
+ python: "coverage.py",
2937
+ rust: "cargo-tarpaulin"
2938
+ };
2939
+ function getCoverageTool(stack) {
2940
+ if (!stack) return "c8";
2941
+ return STATE_COVERAGE_TOOLS[stack] ?? "c8";
2942
+ }
2943
+ function generateAgentsMdContent(projectDir, stack) {
2944
+ if (Array.isArray(stack) && stack.length > 1) {
2945
+ return generateMultiStackAgentsMd(projectDir, stack);
2946
+ }
2947
+ if (Array.isArray(stack)) {
2948
+ stack = stack.length === 1 ? stack[0].stack : null;
2949
+ }
2950
+ const projectName = basename2(projectDir);
2951
+ const provider = stack ? getStackProvider(stack) : void 0;
2952
+ const stackLabel = provider ? stackDisplayName(stack) : "Unknown";
2953
+ const lines = [
2954
+ `# ${projectName}`,
2955
+ "",
2956
+ "## Stack",
2957
+ "",
2958
+ `- **Language/Runtime:** ${stackLabel}`,
2959
+ "",
2960
+ "## Build & Test Commands",
2961
+ ""
2962
+ ];
2963
+ if (provider) {
2964
+ appendBuildTestCommands(lines, provider.name, "");
2965
+ } else {
2966
+ lines.push("```bash", "# No recognized stack \u2014 add build/test commands here", "```");
2967
+ }
2968
+ lines.push(
2969
+ "",
2970
+ "## Project Structure",
2971
+ "",
2972
+ "```",
2973
+ `${projectName}/`,
2974
+ "\u251C\u2500\u2500 src/ # Source code",
2975
+ "\u251C\u2500\u2500 tests/ # Test files",
2976
+ "\u251C\u2500\u2500 docs/ # Documentation",
2977
+ "\u2514\u2500\u2500 .claude/ # Codeharness state",
2978
+ "```",
2979
+ "",
2980
+ "## Conventions",
2981
+ "",
2982
+ "- All changes must pass tests before commit",
2983
+ "- Maintain test coverage targets",
2984
+ "- Follow existing code style and patterns",
2985
+ ""
2986
+ );
2987
+ return lines.join("\n");
2988
+ }
2989
+ function stackDisplayName(stack) {
2990
+ const provider = getStackProvider(stack);
2991
+ if (!provider) return "Unknown";
2992
+ return provider.displayName.replace(/ \(.*\)$/, "");
2993
+ }
2994
+ function appendBuildTestCommands(lines, stack, prefix) {
2995
+ const provider = getStackProvider(stack);
2996
+ if (!provider) return;
2997
+ const buildCmds = provider.getBuildCommands();
2998
+ const testCmds = provider.getTestCommands();
2999
+ lines.push("```bash");
3000
+ for (const cmd of buildCmds) {
3001
+ lines.push(`${prefix}${cmd}`);
3002
+ }
3003
+ for (const cmd of testCmds) {
3004
+ lines.push(`${prefix}${cmd}`);
3005
+ }
3006
+ lines.push("```");
3007
+ }
3008
+ function generateMultiStackAgentsMd(projectDir, stacks) {
3009
+ const projectName = basename2(projectDir);
3010
+ const stackNames = stacks.map((s) => stackDisplayName(s.stack));
3011
+ const lines = [
3012
+ `# ${projectName}`,
3013
+ "",
3014
+ "## Stack",
3015
+ "",
3016
+ `- **Language/Runtime:** ${stackNames.join(" + ")}`,
3017
+ "",
3018
+ "## Build & Test Commands",
3019
+ ""
3020
+ ];
3021
+ for (const detection of stacks) {
3022
+ const label = stackDisplayName(detection.stack);
3023
+ const heading = detection.dir === "." ? `### ${label}` : `### ${label} (${detection.dir}/)`;
3024
+ const prefix = detection.dir === "." ? "" : `cd ${detection.dir} && `;
3025
+ lines.push(heading, "");
3026
+ appendBuildTestCommands(lines, detection.stack, prefix);
3027
+ lines.push("");
3028
+ }
3029
+ lines.push(
3030
+ "## Project Structure",
3031
+ "",
3032
+ "```",
3033
+ `${projectName}/`,
3034
+ "\u251C\u2500\u2500 src/ # Source code",
3035
+ "\u251C\u2500\u2500 tests/ # Test files",
3036
+ "\u251C\u2500\u2500 docs/ # Documentation",
3037
+ "\u2514\u2500\u2500 .claude/ # Codeharness state",
3038
+ "```",
3039
+ "",
3040
+ "## Conventions",
3041
+ "",
3042
+ "- All changes must pass tests before commit",
3043
+ "- Maintain test coverage targets",
3044
+ "- Follow existing code style and patterns",
3045
+ ""
3046
+ );
3047
+ return lines.join("\n");
3048
+ }
3049
+ function generateDocsIndexContent() {
3050
+ return [
3051
+ "# Project Documentation",
3052
+ "",
3053
+ "## Planning Artifacts",
3054
+ "- [Product Requirements](../_bmad-output/planning-artifacts/prd.md)",
3055
+ "- [Architecture](../_bmad-output/planning-artifacts/architecture.md)",
3056
+ "- [Epics & Stories](../_bmad-output/planning-artifacts/epics.md)",
3057
+ "",
3058
+ "## Execution",
3059
+ "- [Active Exec Plans](exec-plans/active/)",
3060
+ "- [Completed Exec Plans](exec-plans/completed/)",
3061
+ "",
3062
+ "## Quality",
3063
+ "- [Quality Reports](quality/)",
3064
+ "- [Generated Reports](generated/)",
3065
+ ""
3066
+ ].join("\n");
3067
+ }
3068
+ async function scaffoldDocs(opts) {
3069
+ try {
3070
+ let agentsMd = "skipped";
3071
+ let docsScaffold = "skipped";
3072
+ let readme = "skipped";
3073
+ const agentsMdPath = join13(opts.projectDir, "AGENTS.md");
3074
+ if (!existsSync12(agentsMdPath)) {
3075
+ const stackArg = opts.stacks && opts.stacks.length > 1 ? opts.stacks : opts.stack;
3076
+ const content = generateAgentsMdContent(opts.projectDir, stackArg);
3077
+ generateFile(agentsMdPath, content);
3078
+ agentsMd = "created";
3079
+ } else {
3080
+ agentsMd = "exists";
3081
+ }
3082
+ const docsDir = join13(opts.projectDir, "docs");
3083
+ if (!existsSync12(docsDir)) {
3084
+ generateFile(join13(docsDir, "index.md"), generateDocsIndexContent());
3085
+ generateFile(join13(docsDir, "exec-plans", "active", ".gitkeep"), "");
3086
+ generateFile(join13(docsDir, "exec-plans", "completed", ".gitkeep"), "");
3087
+ generateFile(join13(docsDir, "quality", ".gitkeep"), DO_NOT_EDIT_HEADER);
3088
+ generateFile(join13(docsDir, "generated", ".gitkeep"), DO_NOT_EDIT_HEADER);
3089
+ docsScaffold = "created";
3090
+ } else {
3091
+ docsScaffold = "exists";
3092
+ }
3093
+ const readmePath = join13(opts.projectDir, "README.md");
3094
+ if (!existsSync12(readmePath)) {
3095
+ let cliHelpOutput = "";
3096
+ try {
3097
+ const { execFileSync: execFileSync13 } = await import("child_process");
3098
+ cliHelpOutput = execFileSync13(process.execPath, [process.argv[1], "--help"], {
3099
+ stdio: "pipe",
3100
+ timeout: 1e4
3101
+ }).toString();
3102
+ } catch {
3103
+ cliHelpOutput = "Run: codeharness --help";
3104
+ }
3105
+ const readmeStack = opts.stacks && opts.stacks.length > 1 ? opts.stacks.map((s) => s.stack) : opts.stack;
3106
+ const readmeContent = readmeTemplate({
3107
+ projectName: getProjectName(opts.projectDir),
3108
+ stack: readmeStack,
3109
+ cliHelpOutput
3110
+ });
3111
+ generateFile(readmePath, readmeContent);
3112
+ readme = "created";
3113
+ } else {
3114
+ readme = "exists";
3115
+ }
3116
+ const result = { agents_md: agentsMd, docs_scaffold: docsScaffold, readme };
3117
+ if (!opts.isJson) {
3118
+ if (result.agents_md === "created" || result.docs_scaffold === "created") {
3119
+ ok("Documentation: AGENTS.md + docs/ scaffold created");
3120
+ }
3121
+ if (result.readme === "created") {
3122
+ ok("Documentation: README.md created");
3123
+ }
3124
+ }
3125
+ return ok2(result);
3126
+ } catch (err) {
3127
+ const message = err instanceof Error ? err.message : String(err);
3128
+ return fail2(`Documentation scaffold failed: ${message}`);
3129
+ }
3130
+ }
3131
+
3132
+ // src/modules/infra/dockerfile-template.ts
3133
+ import { existsSync as existsSync13, writeFileSync as writeFileSync8 } from "fs";
3134
+ import { join as join14 } from "path";
3135
+ function genericTemplate() {
3136
+ return renderTemplateFile("templates/dockerfiles/Dockerfile.generic");
3137
+ }
3138
+ function runtimeCopyDirectives(stacks) {
3139
+ const lines = [];
3140
+ for (const stack of stacks) {
3141
+ const provider = getStackProvider(stack);
3142
+ if (provider) {
3143
+ lines.push(provider.getRuntimeCopyDirectives());
3144
+ }
3145
+ }
3146
+ return lines.join("\n");
3147
+ }
3148
+ function multiStageTemplate(detections) {
3149
+ const supported = detections.filter((d) => getStackProvider(d.stack) !== void 0);
3150
+ if (supported.length === 0) {
3151
+ return genericTemplate();
3152
+ }
3153
+ const stacks = supported.map((d) => d.stack);
3154
+ const buildStages = [];
3155
+ for (const stack of stacks) {
3156
+ const provider = getStackProvider(stack);
3157
+ if (provider) {
3158
+ buildStages.push(provider.getDockerBuildStage());
3159
+ }
3160
+ }
3161
+ const copyLines = runtimeCopyDirectives(stacks);
3162
+ return renderTemplateFile("templates/dockerfiles/Dockerfile.multi-stage.tmpl", {
3163
+ BUILD_STAGES: buildStages.join("\n"),
3164
+ COPY_DIRECTIVES: copyLines
3165
+ });
3166
+ }
3167
+ function generateDockerfileTemplate(projectDir, stackOrDetections) {
3168
+ if (!projectDir) {
3169
+ return fail2("projectDir is required");
3170
+ }
3171
+ const dockerfilePath = join14(projectDir, "Dockerfile");
3172
+ if (existsSync13(dockerfilePath)) {
3173
+ return fail2("Dockerfile already exists");
3174
+ }
3175
+ if (Array.isArray(stackOrDetections) && stackOrDetections.length > 1) {
3176
+ const content2 = multiStageTemplate(stackOrDetections);
3177
+ const stacks = stackOrDetections.map((d) => d.stack);
3178
+ try {
3179
+ writeFileSync8(dockerfilePath, content2, "utf-8");
3180
+ } catch (err) {
3181
+ const message = err instanceof Error ? err.message : String(err);
3182
+ return fail2(`Failed to write Dockerfile: ${message}`);
3183
+ }
3184
+ return ok2({ path: dockerfilePath, stack: stacks[0], stacks });
3185
+ }
3186
+ const stack = Array.isArray(stackOrDetections) ? stackOrDetections.length === 1 ? stackOrDetections[0].stack : null : stackOrDetections;
3187
+ let content;
3188
+ let resolvedStack;
3189
+ const provider = stack ? getStackProvider(stack) : void 0;
3190
+ if (provider) {
3191
+ content = provider.getDockerfileTemplate();
3192
+ resolvedStack = provider.name;
3193
+ } else {
3194
+ content = genericTemplate();
3195
+ resolvedStack = "generic";
3196
+ }
3197
+ try {
3198
+ writeFileSync8(dockerfilePath, content, "utf-8");
3199
+ } catch (err) {
3200
+ const message = err instanceof Error ? err.message : String(err);
3201
+ return fail2(`Failed to write Dockerfile: ${message}`);
3202
+ }
3203
+ return ok2({ path: dockerfilePath, stack: resolvedStack, stacks: [resolvedStack] });
3204
+ }
3205
+
3206
+ // src/modules/infra/init-project.ts
3207
+ var HARNESS_VERSION = true ? "0.25.2" : "0.0.0-dev";
3208
+ function failResult(opts, error) {
3209
+ return {
3210
+ status: "fail",
3211
+ stack: null,
3212
+ stacks: [],
3213
+ error,
3214
+ enforcement: { frontend: opts.frontend, database: opts.database, api: opts.api },
3215
+ documentation: { agents_md: "skipped", docs_scaffold: "skipped", readme: "skipped" }
3216
+ };
3217
+ }
3218
+ function emitError(error, isJson) {
3219
+ if (isJson) {
3220
+ jsonOutput({ status: "fail", error });
3221
+ } else {
3222
+ fail(error);
3223
+ }
3224
+ process.exitCode = 1;
3225
+ }
3226
+ async function initProject(opts) {
3227
+ try {
3228
+ return await initProjectInner(opts);
3229
+ } catch (err) {
3230
+ const message = err instanceof Error ? err.message : String(err);
3231
+ return fail2(`Unexpected init error: ${message}`);
3232
+ }
3233
+ }
3234
+ async function initProjectInner(opts) {
3235
+ const { projectDir, json: isJson = false } = opts;
3236
+ const result = {
3237
+ status: "ok",
3238
+ stack: null,
3239
+ stacks: [],
3240
+ enforcement: { frontend: opts.frontend, database: opts.database, api: opts.api },
3241
+ documentation: { agents_md: "skipped", docs_scaffold: "skipped", readme: "skipped" }
3242
+ };
3243
+ const rerunResult = handleRerun(opts, result);
3244
+ if (rerunResult !== null) return rerunResult;
3245
+ const validBackends = ["victoria", "elk", "none"];
3246
+ if (opts.observabilityBackend !== void 0 && !validBackends.includes(opts.observabilityBackend)) {
3247
+ const msg = `Invalid --observability-backend '${opts.observabilityBackend}'. Must be one of: ${validBackends.join(", ")}`;
3248
+ emitError(msg, isJson);
3249
+ return ok2(failResult(opts, msg));
3250
+ }
3251
+ const urlError = validateRemoteUrls(opts);
3252
+ if (urlError !== null) {
3253
+ emitError(urlError, isJson);
3254
+ return ok2(failResult(opts, urlError));
3255
+ }
3256
+ const allStacks = detectStacks(projectDir);
3257
+ const rootDetection = allStacks.find((s) => s.dir === ".");
3258
+ const stack = rootDetection ? rootDetection.stack : null;
3259
+ if (!stack && !isJson) warn("No recognized stack detected");
3260
+ result.stack = stack;
3261
+ result.stacks = [...new Set(allStacks.map((s) => s.stack))];
3262
+ const appType = detectAppType(projectDir, stack);
3263
+ result.app_type = appType;
3264
+ if (!isJson) {
3265
+ if (result.stacks.length > 0) {
3266
+ info(`Stack detected: ${getStackLabel(result.stacks)}`);
3267
+ } else if (stack) {
3268
+ info(`Stack detected: ${getStackLabel(stack)}`);
3269
+ }
3270
+ info(`App type: ${appType}`);
3271
+ }
3272
+ const dfResult = generateDockerfileTemplate(projectDir, allStacks);
3273
+ if (isOk(dfResult)) {
3274
+ result.dockerfile = { generated: true, stack: dfResult.data.stack, stacks: dfResult.data.stacks };
3275
+ if (!isJson) info(`Generated Dockerfile for ${dfResult.data.stacks.join("+") || dfResult.data.stack} project.`);
3276
+ } else {
3277
+ if (!isJson) info("Dockerfile already exists -- skipping template generation.");
3278
+ }
3279
+ const dockerCheck = checkDocker({ observability: opts.observability, otelEndpoint: opts.otelEndpoint, logsUrl: opts.logsUrl, opensearchUrl: opts.opensearchUrl, isJson });
3280
+ if (!isOk(dockerCheck)) return fail2(dockerCheck.error);
3281
+ const { available: dockerAvailable, criticalFailure, dockerResult: criticalDockerResult } = dockerCheck.data;
3282
+ if (criticalFailure) {
3283
+ result.status = "fail";
3284
+ result.error = "Docker not installed";
3285
+ result.docker = criticalDockerResult;
3286
+ if (isJson) {
3287
+ jsonOutput(result);
3288
+ } else {
3289
+ fail("Docker not installed");
3290
+ info("\u2192 Install Docker: https://docs.docker.com/engine/install/");
3291
+ info("\u2192 Or skip observability: codeharness init --no-observability");
3292
+ }
3293
+ process.exitCode = 1;
3294
+ return ok2(result);
3295
+ }
3296
+ const depResult = installDeps({ isJson });
3297
+ if (!isOk(depResult)) {
3298
+ result.status = "fail";
3299
+ result.error = depResult.error;
3300
+ if (isJson) {
3301
+ jsonOutput(result);
3302
+ } else {
3303
+ info("Critical dependency failed \u2014 aborting init");
3304
+ }
3305
+ process.exitCode = 1;
3306
+ return ok2(result);
3307
+ }
3308
+ result.dependencies = depResult.data;
3309
+ result.beads = initializeBeads(projectDir, isJson);
3310
+ const bmadResult = setupBmad({ projectDir, isJson });
3311
+ if (isOk(bmadResult)) result.bmad = bmadResult.data;
3312
+ let state = getDefaultState(stack);
3313
+ state.harness_version = HARNESS_VERSION;
3314
+ state.initialized = true;
3315
+ state.app_type = appType;
3316
+ state.enforcement = { frontend: opts.frontend, database: opts.database, api: opts.api };
3317
+ const coverageTools = {};
3318
+ for (const detection of allStacks) {
3319
+ coverageTools[detection.stack] = getCoverageTool(detection.stack);
3320
+ }
3321
+ state.coverage.tool = getCoverageTool(stack);
3322
+ state.coverage.tools = coverageTools;
3323
+ state.stacks = result.stacks;
3324
+ writeState(state, projectDir);
3325
+ if (!isJson) ok("State file: .claude/codeharness.local.md created");
3326
+ const docsResult = await scaffoldDocs({ projectDir, stack, stacks: allStacks, isJson });
3327
+ if (isOk(docsResult)) result.documentation = docsResult.data;
3328
+ const obsBackend = opts.observabilityBackend ?? "victoria";
3329
+ if (obsBackend === "none") {
3330
+ state.otlp = { enabled: false, endpoint: "", service_name: basename3(projectDir), mode: "local-shared", backend: "none" };
3331
+ writeState(state, projectDir);
3332
+ result.otlp = { status: "skipped", packages_installed: false, start_script_patched: false, env_vars_configured: false };
3333
+ if (!isJson) info("Observability: disabled (--observability-backend none)");
3334
+ if (!isJson) {
3335
+ const e = state.enforcement;
3336
+ const fmt = (v) => v ? "ON" : "OFF";
3337
+ ok(`Enforcement: frontend:${fmt(e.frontend)} database:${fmt(e.database)} api:${fmt(e.api)} observability:OFF`);
3338
+ info("Harness initialized. Run: codeharness bridge --epics <path>");
3339
+ }
3340
+ if (isJson) jsonOutput(result);
3341
+ return ok2(result);
3342
+ }
3343
+ if (!opts.observability) {
3344
+ result.otlp = { status: "skipped", packages_installed: false, start_script_patched: false, env_vars_configured: false };
3345
+ if (!isJson) info("OTLP: skipped (--no-observability)");
3346
+ } else {
3347
+ for (const detection of allStacks) {
3348
+ const stackDir = detection.dir === "." ? projectDir : join15(projectDir, detection.dir);
3349
+ const stackOtlp = instrumentProject(stackDir, detection.stack, { json: isJson, appType });
3350
+ if (detection.dir === "." && detection.stack === stack) {
3351
+ result.otlp = stackOtlp;
3352
+ }
3353
+ }
3354
+ if (!result.otlp) {
3355
+ result.otlp = instrumentProject(projectDir, stack, { json: isJson, appType });
3356
+ }
3357
+ }
3358
+ try {
3359
+ const u = readState(projectDir);
3360
+ if (u.otlp) state.otlp = u.otlp;
3361
+ } catch {
3362
+ }
3363
+ if (!state.otlp) {
3364
+ state.otlp = { enabled: true, endpoint: "http://localhost:4318", service_name: basename3(projectDir), mode: "local-shared" };
3365
+ }
3366
+ state.otlp.backend = obsBackend;
3367
+ writeState(state, projectDir);
3368
+ const dockerSetup = setupDocker({
3369
+ observability: opts.observability,
3370
+ otelEndpoint: opts.otelEndpoint,
3371
+ opensearchUrl: opts.opensearchUrl,
3372
+ logsUrl: opts.logsUrl,
3373
+ metricsUrl: opts.metricsUrl,
3374
+ tracesUrl: opts.tracesUrl,
3375
+ isJson,
3376
+ dockerAvailable,
3377
+ appType,
3378
+ state,
3379
+ projectDir
3380
+ });
3381
+ if (isOk(dockerSetup)) result.docker = dockerSetup.data.docker;
3382
+ if (!isJson) {
3383
+ const e = state.enforcement;
3384
+ const fmt = (v) => v ? "ON" : "OFF";
3385
+ ok(`Enforcement: frontend:${fmt(e.frontend)} database:${fmt(e.database)} api:${fmt(e.api)} observability:ON`);
3386
+ info("Harness initialized. Run: codeharness bridge --epics <path>");
3387
+ }
3388
+ if (isJson) jsonOutput(result);
3389
+ return ok2(result);
3390
+ }
3391
+ function handleRerun(opts, result) {
3392
+ const { projectDir, json: isJson = false } = opts;
3393
+ if (!existsSync14(getStatePath(projectDir))) return null;
3394
+ try {
3395
+ const existingState = readState(projectDir);
3396
+ const legacyObsDisabled = existingState.enforcement.observability === false;
3397
+ if (!existingState.initialized || legacyObsDisabled) {
3398
+ if (legacyObsDisabled && !isJson) info("Observability upgraded from disabled to enabled");
3399
+ return null;
3400
+ }
3401
+ result.stack = existingState.stack;
3402
+ result.stacks = existingState.stacks ?? [];
3403
+ result.enforcement = existingState.enforcement;
3404
+ result.documentation = { agents_md: "exists", docs_scaffold: "exists", readme: "exists" };
3405
+ result.dependencies = verifyDeps(isJson);
3406
+ result.docker = existingState.docker ? { compose_file: existingState.docker.compose_file, stack_running: existingState.docker.stack_running, services: [], ports: existingState.docker.ports } : null;
3407
+ const bmadResult = verifyBmadOnRerun(projectDir, isJson);
3408
+ if (bmadResult) result.bmad = bmadResult;
3409
+ if (isJson) {
3410
+ jsonOutput(result);
3411
+ } else {
3412
+ info("Harness already initialized \u2014 verifying configuration");
3413
+ ok("Configuration verified");
3414
+ }
3415
+ return ok2(result);
3416
+ } catch {
3417
+ return null;
3418
+ }
3419
+ }
3420
+ function validateRemoteUrls(opts) {
3421
+ const remoteUrls = [opts.otelEndpoint, opts.opensearchUrl, opts.logsUrl, opts.metricsUrl, opts.tracesUrl].filter(Boolean);
3422
+ for (const url of remoteUrls) {
3423
+ if (!/^https?:\/\//i.test(url)) return `Invalid URL: '${url}' \u2014 must start with http:// or https://`;
3424
+ }
3425
+ if (opts.otelEndpoint && (opts.logsUrl || opts.metricsUrl || opts.tracesUrl)) {
3426
+ return "Cannot combine --otel-endpoint with --logs-url/--metrics-url/--traces-url";
3427
+ }
3428
+ const hasAny = opts.logsUrl || opts.metricsUrl || opts.tracesUrl;
3429
+ if (hasAny && !(opts.logsUrl && opts.metricsUrl && opts.tracesUrl)) {
3430
+ return "When using remote backends, all three are required: --logs-url, --metrics-url, --traces-url";
3431
+ }
3432
+ return null;
3433
+ }
3434
+
3435
+ // src/modules/infra/stack-management.ts
3436
+ import { execFileSync as execFileSync11 } from "child_process";
3437
+
3438
+ // src/modules/infra/container-cleanup.ts
3439
+ import { execFileSync as execFileSync12 } from "child_process";
3440
+ var STALE_PATTERNS = ["codeharness-shared-", "codeharness-collector-", "codeharness-verify-"];
3441
+ function cleanupContainers() {
3442
+ try {
3443
+ if (!isDockerAvailable()) {
3444
+ return ok2({ containersRemoved: 0, names: [] });
3445
+ }
3446
+ const staleNames = [];
3447
+ for (const pattern of STALE_PATTERNS) {
3448
+ try {
3449
+ const output = execFileSync12(
3450
+ "docker",
3451
+ [
3452
+ "ps",
3453
+ "-a",
3454
+ "--filter",
3455
+ `name=${pattern}`,
3456
+ "--filter",
3457
+ "status=exited",
3458
+ "--filter",
3459
+ "status=dead",
3460
+ "--format",
3461
+ "{{.Names}}"
3462
+ ],
3463
+ { stdio: "pipe", timeout: 1e4 }
3464
+ );
3465
+ const names = output.toString().trim().split("\n").filter((n) => n.trim());
3466
+ staleNames.push(...names);
3467
+ } catch {
3468
+ }
3469
+ }
3470
+ if (staleNames.length === 0) {
3471
+ return ok2({ containersRemoved: 0, names: [] });
3472
+ }
3473
+ const removed = [];
3474
+ for (const name of staleNames) {
3475
+ try {
3476
+ execFileSync12("docker", ["rm", "-f", name], {
3477
+ stdio: "pipe",
3478
+ timeout: 1e4
3479
+ });
3480
+ removed.push(name);
3481
+ } catch {
3482
+ }
3483
+ }
3484
+ return ok2({ containersRemoved: removed.length, names: removed });
3485
+ } catch (err) {
3486
+ const message = err instanceof Error ? err.message : String(err);
3487
+ return fail2(`Container cleanup failed: ${message}`);
3488
+ }
3489
+ }
3490
+
3491
+ // src/modules/infra/victoria-backend.ts
3492
+ var DEFAULT_LOGS_URL = `http://localhost:${DEFAULT_PORTS.logs}`;
3493
+ var DEFAULT_METRICS_URL = `http://localhost:${DEFAULT_PORTS.metrics}`;
3494
+ var DEFAULT_TRACES_URL = `http://localhost:${DEFAULT_PORTS.traces}`;
3495
+
3496
+ // src/modules/infra/dockerfile-validator.ts
3497
+ import { existsSync as existsSync15, readFileSync as readFileSync13 } from "fs";
3498
+ import { join as join16 } from "path";
3499
+ var DEFAULT_RULES = {
3500
+ requirePinnedFrom: true,
3501
+ requireBinaryOnPath: true,
3502
+ verificationTools: ["curl", "jq"],
3503
+ forbidSourceCopy: true,
3504
+ requireNonRootUser: true,
3505
+ requireCacheCleanup: true
3506
+ };
3507
+ function dfGap(rule, description, suggestedFix, line) {
3508
+ const g = { rule, description, suggestedFix };
3509
+ if (line !== void 0) return { ...g, line };
3510
+ return g;
3511
+ }
3512
+ function loadRules(projectDir) {
3513
+ const rulesPath = join16(projectDir, "patches", "infra", "dockerfile-rules.md");
3514
+ if (!existsSync15(rulesPath)) {
3515
+ return {
3516
+ rules: DEFAULT_RULES,
3517
+ warnings: ["dockerfile-rules.md not found -- using defaults."]
3518
+ };
3519
+ }
3520
+ return { rules: DEFAULT_RULES, warnings: [] };
3521
+ }
3522
+ function checkPinnedFrom(lines) {
3523
+ const gaps = [];
3524
+ for (let i = 0; i < lines.length; i++) {
3525
+ if (!/^\s*FROM\s+/i.test(lines[i])) continue;
3526
+ const ref = lines[i].replace(/^\s*FROM\s+/i, "").split(/\s+/)[0];
3527
+ if (ref.endsWith(":latest")) {
3528
+ gaps.push(dfGap("pinned-from", `unpinned base image -- use specific version.`, `Pin ${ref} to a specific version tag`, i + 1));
3529
+ } else if (!ref.includes(":") && !ref.includes("@")) {
3530
+ gaps.push(dfGap("pinned-from", `unpinned base image -- use specific version.`, `Pin ${ref} to a specific version tag (e.g., ${ref}:22-slim)`, i + 1));
3531
+ }
3532
+ }
3533
+ return gaps;
3534
+ }
3535
+ function checkBinaryOnPath(lines) {
3536
+ const content = lines.join("\n");
3537
+ const hasBinary = /npm\s+install\s+(-g|--global)\b/i.test(content) || /pip\s+install\b/i.test(content) || /COPY\s+--from=/i.test(content);
3538
+ if (!hasBinary) {
3539
+ return [dfGap("binary-on-path", "project binary not installed.", "Add npm install -g, pip install, or COPY --from to install the project binary")];
3540
+ }
3541
+ return [];
3542
+ }
3543
+ function checkVerificationTools(lines, tools) {
3544
+ const gaps = [];
3545
+ for (const tool of tools) {
3546
+ let found = false;
3547
+ for (const line of lines) {
3548
+ const lower = line.toLowerCase();
3549
+ const isInstallLine = lower.includes("apt-get install") || lower.includes("apk add");
3550
+ if (isInstallLine && new RegExp(`\\b${tool.toLowerCase()}\\b`).test(lower)) {
3551
+ found = true;
3552
+ break;
3553
+ }
3554
+ }
3555
+ if (!found) {
3556
+ gaps.push(dfGap("verification-tools", `verification tool missing: ${tool}`, `Install ${tool} via apt-get install or apk add`));
3557
+ }
3558
+ }
3559
+ return gaps;
3560
+ }
3561
+ function checkNoSourceCopy(lines) {
3562
+ const gaps = [];
3563
+ const sourcePatterns = [/COPY\s+(?:--\S+\s+)*src\//i, /COPY\s+(?:--\S+\s+)*lib\//i, /COPY\s+(?:--\S+\s+)*test\//i];
3564
+ for (let i = 0; i < lines.length; i++) {
3565
+ for (const pattern of sourcePatterns) {
3566
+ if (pattern.test(lines[i])) {
3567
+ gaps.push(dfGap("no-source-copy", "source code copied into container -- use build artifact instead.", "Use COPY --from=builder or COPY dist/ instead of copying source", i + 1));
3568
+ }
3569
+ }
3570
+ }
3571
+ return gaps;
3572
+ }
3573
+ function checkNonRootUser(lines) {
3574
+ const userLines = lines.filter((l) => /^\s*USER\s+/i.test(l));
3575
+ if (userLines.length === 0) {
3576
+ return [dfGap("non-root-user", "no non-root USER instruction found.", "Add USER <non-root-user> instruction (e.g., USER node)")];
3577
+ }
3578
+ const hasNonRoot = userLines.some((l) => {
3579
+ const user = l.replace(/^\s*USER\s+/i, "").trim().split(/\s+/)[0];
3580
+ return user.toLowerCase() !== "root";
3581
+ });
3582
+ if (!hasNonRoot) {
3583
+ return [dfGap("non-root-user", "no non-root USER instruction found.", "Add USER <non-root-user> instruction (e.g., USER node)")];
3584
+ }
3585
+ return [];
3586
+ }
3587
+ function checkCacheCleanup(lines) {
3588
+ const content = lines.join("\n");
3589
+ const hasCleanup = /rm\s+-rf\s+\/var\/lib\/apt\/lists/i.test(content) || /rm\s+-rf\s+\/var\/cache\/apk/i.test(content) || /npm\s+cache\s+clean/i.test(content) || /pip\s+cache\s+purge/i.test(content);
3590
+ if (!hasCleanup) {
3591
+ return [dfGap("cache-cleanup", "no cache cleanup detected.", "Add cache cleanup: rm -rf /var/lib/apt/lists/*, npm cache clean --force, or pip cache purge")];
3592
+ }
3593
+ return [];
3594
+ }
3595
+ function validateDockerfile(projectDir) {
3596
+ const dfPath = join16(projectDir, "Dockerfile");
3597
+ if (!existsSync15(dfPath)) {
3598
+ return fail2("No Dockerfile found");
3599
+ }
3600
+ let content;
3601
+ try {
3602
+ content = readFileSync13(dfPath, "utf-8");
3603
+ } catch {
3604
+ return fail2("Dockerfile exists but could not be read");
3605
+ }
3606
+ const lines = content.split("\n");
3607
+ const fromLines = lines.filter((l) => /^\s*FROM\s+/i.test(l));
3608
+ if (fromLines.length === 0) {
3609
+ return fail2("Dockerfile has no FROM instruction");
3610
+ }
3611
+ const { rules, warnings } = loadRules(projectDir);
3612
+ const gaps = [];
3613
+ if (rules.requirePinnedFrom) gaps.push(...checkPinnedFrom(lines));
3614
+ if (rules.requireBinaryOnPath) gaps.push(...checkBinaryOnPath(lines));
3615
+ gaps.push(...checkVerificationTools(lines, rules.verificationTools));
3616
+ if (rules.forbidSourceCopy) gaps.push(...checkNoSourceCopy(lines));
3617
+ if (rules.requireNonRootUser) gaps.push(...checkNonRootUser(lines));
3618
+ if (rules.requireCacheCleanup) gaps.push(...checkCacheCleanup(lines));
3619
+ return ok2({
3620
+ passed: gaps.length === 0,
3621
+ gaps,
3622
+ warnings
3623
+ });
3624
+ }
3625
+
3626
+ // src/modules/infra/index.ts
3627
+ async function initProject2(opts) {
3628
+ return initProject(opts);
3629
+ }
3630
+ function cleanupContainers2() {
3631
+ return cleanupContainers();
3632
+ }
3633
+
3634
+ // src/lib/docker/cleanup.ts
3635
+ function cleanupOrphanedContainers() {
3636
+ const result = cleanupContainers2();
3637
+ return result.success ? result.data.containersRemoved : 0;
3638
+ }
3639
+ function cleanupVerifyEnv() {
3640
+ }
3641
+
3642
+ export {
3643
+ ok,
3644
+ fail,
3645
+ warn,
3646
+ info,
3647
+ jsonOutput,
3648
+ getStackProvider,
3649
+ detectStacks,
3650
+ detectStack,
3651
+ renderTemplateFile,
3652
+ getStatePath,
3653
+ writeState,
3654
+ readState,
3655
+ readStateWithBody,
3656
+ StateFileNotFoundError,
3657
+ getNestedValue,
3658
+ setNestedValue,
3659
+ parseValue,
3660
+ NODE_REQUIRE_FLAG2 as NODE_REQUIRE_FLAG,
3661
+ ok2,
3662
+ fail2,
3663
+ isOk,
3664
+ isDockerAvailable,
3665
+ isDockerComposeAvailable,
3666
+ getStackHealth,
3667
+ getCollectorHealth,
3668
+ checkRemoteEndpoint,
3669
+ getStackDir,
3670
+ getComposeFilePath,
3671
+ getElkComposeFilePath,
3672
+ isStackRunning,
3673
+ isSharedStackRunning,
3674
+ startStack,
3675
+ startSharedStack,
3676
+ stopStack,
3677
+ stopSharedStack,
3678
+ startCollectorOnly,
3679
+ isCollectorRunning,
3680
+ stopCollectorOnly,
3681
+ cleanupOrphanedContainers,
3682
+ cleanupVerifyEnv,
3683
+ removePatch,
3684
+ isBeadsCLIInstalled,
3685
+ createIssue,
3686
+ closeIssue,
3687
+ updateIssue,
3688
+ listIssues,
3689
+ isBeadsInitialized,
3690
+ buildGapId,
3691
+ createOrFindIssue,
3692
+ PATCH_TARGETS,
3693
+ isBmadInstalled,
3694
+ parseEpicsFile,
3695
+ importStoriesToBeads,
3696
+ validateDockerfile,
3697
+ initProject2 as initProject,
3698
+ cleanupContainers2 as cleanupContainers
3699
+ };