codeharness 0.25.1 → 0.25.3

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