codeharness 0.6.1

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,9 @@
1
+ #!/usr/bin/env bash
2
+ # Self-contained CLI entry point for the codeharness plugin.
3
+ # Used by hooks via ${CLAUDE_PLUGIN_ROOT}/bin/codeharness
4
+ # so the CLI works without global npm install.
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
7
+ PLUGIN_ROOT="$(dirname "$SCRIPT_DIR")"
8
+
9
+ exec node "$PLUGIN_ROOT/dist/index.js" "$@"
@@ -0,0 +1,540 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/lib/docker.ts
4
+ import { execFileSync } from "child_process";
5
+ import { writeFileSync } from "fs";
6
+
7
+ // src/lib/stack-path.ts
8
+ import { homedir } from "os";
9
+ import { join, isAbsolute } from "path";
10
+ import { mkdirSync } from "fs";
11
+ function getStackDir() {
12
+ const xdgDataHome = process.env["XDG_DATA_HOME"];
13
+ if (xdgDataHome && xdgDataHome.trim() !== "" && isAbsolute(xdgDataHome)) {
14
+ return join(xdgDataHome, "codeharness", "stack");
15
+ }
16
+ return join(homedir(), ".codeharness", "stack");
17
+ }
18
+ function getComposeFilePath() {
19
+ return join(getStackDir(), "docker-compose.harness.yml");
20
+ }
21
+ function getOtelConfigPath() {
22
+ return join(getStackDir(), "otel-collector-config.yaml");
23
+ }
24
+ function ensureStackDir() {
25
+ mkdirSync(getStackDir(), { recursive: true });
26
+ }
27
+
28
+ // src/templates/docker-compose.ts
29
+ function dockerComposeCollectorOnlyTemplate() {
30
+ return `# Generated by codeharness \u2014 do not edit manually
31
+ name: codeharness-collector
32
+
33
+ services:
34
+ otel-collector:
35
+ image: otel/opentelemetry-collector-contrib:0.96.0
36
+ labels:
37
+ com.codeharness.stack: collector
38
+ ports:
39
+ - "4317:4317"
40
+ - "4318:4318"
41
+ volumes:
42
+ - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:ro
43
+ restart: unless-stopped
44
+
45
+ networks:
46
+ default:
47
+ name: codeharness-collector-net
48
+ `;
49
+ }
50
+ function dockerComposeTemplate(config) {
51
+ return `# Generated by codeharness \u2014 do not edit manually
52
+ name: codeharness-shared
53
+
54
+ services:
55
+ victoria-logs:
56
+ image: victoriametrics/victoria-logs:v1.15.0
57
+ labels:
58
+ com.codeharness.stack: shared
59
+ ports:
60
+ - "9428:9428"
61
+ volumes:
62
+ - victoria-logs-data:/vlogs
63
+ restart: unless-stopped
64
+
65
+ victoria-metrics:
66
+ image: victoriametrics/victoria-metrics:v1.106.1
67
+ labels:
68
+ com.codeharness.stack: shared
69
+ ports:
70
+ - "8428:8428"
71
+ volumes:
72
+ - victoria-metrics-data:/victoria-metrics-data
73
+ restart: unless-stopped
74
+
75
+ victoria-traces:
76
+ image: jaegertracing/all-in-one:1.56
77
+ labels:
78
+ com.codeharness.stack: shared
79
+ ports:
80
+ - "14268:14268"
81
+ - "16686:16686"
82
+ environment:
83
+ - COLLECTOR_OTLP_ENABLED=true
84
+ restart: unless-stopped
85
+
86
+ otel-collector:
87
+ image: otel/opentelemetry-collector-contrib:0.96.0
88
+ labels:
89
+ com.codeharness.stack: shared
90
+ ports:
91
+ - "4317:4317"
92
+ - "4318:4318"
93
+ volumes:
94
+ - ./otel-collector-config.yaml:/etc/otelcol-contrib/config.yaml:ro
95
+ depends_on:
96
+ - victoria-logs
97
+ - victoria-metrics
98
+ - victoria-traces
99
+ restart: unless-stopped
100
+
101
+ volumes:
102
+ victoria-logs-data:
103
+ victoria-metrics-data:
104
+
105
+ networks:
106
+ default:
107
+ name: codeharness-shared-net
108
+ `;
109
+ }
110
+
111
+ // src/templates/otel-config.ts
112
+ function otelCollectorRemoteTemplate(config) {
113
+ const logsUrl = config.logsUrl.replace(/\/+$/, "");
114
+ const metricsUrl = config.metricsUrl.replace(/\/+$/, "");
115
+ const tracesUrl = config.tracesUrl.replace(/\/+$/, "");
116
+ return `# Generated by codeharness \u2014 do not edit manually
117
+ receivers:
118
+ otlp:
119
+ protocols:
120
+ grpc:
121
+ endpoint: 0.0.0.0:4317
122
+ http:
123
+ endpoint: 0.0.0.0:4318
124
+
125
+ processors:
126
+ resource/default:
127
+ attributes:
128
+ - key: service.name
129
+ value: "unknown"
130
+ action: insert
131
+
132
+ exporters:
133
+ otlphttp/logs:
134
+ endpoint: ${logsUrl}/insert/opentelemetry
135
+ tls:
136
+ insecure: true
137
+
138
+ prometheusremotewrite:
139
+ endpoint: ${metricsUrl}/api/v1/write
140
+ tls:
141
+ insecure: true
142
+
143
+ otlp/traces:
144
+ endpoint: ${tracesUrl}
145
+ tls:
146
+ insecure: true
147
+
148
+ service:
149
+ pipelines:
150
+ logs:
151
+ receivers:
152
+ - otlp
153
+ processors:
154
+ - resource/default
155
+ exporters:
156
+ - otlphttp/logs
157
+ metrics:
158
+ receivers:
159
+ - otlp
160
+ processors:
161
+ - resource/default
162
+ exporters:
163
+ - prometheusremotewrite
164
+ traces:
165
+ receivers:
166
+ - otlp
167
+ processors:
168
+ - resource/default
169
+ exporters:
170
+ - otlp/traces
171
+ `;
172
+ }
173
+ function otelCollectorConfigTemplate() {
174
+ return `# Generated by codeharness \u2014 do not edit manually
175
+ receivers:
176
+ otlp:
177
+ protocols:
178
+ grpc:
179
+ endpoint: 0.0.0.0:4317
180
+ http:
181
+ endpoint: 0.0.0.0:4318
182
+
183
+ processors:
184
+ resource/default:
185
+ attributes:
186
+ - key: service.name
187
+ value: "unknown"
188
+ action: insert
189
+
190
+ exporters:
191
+ otlphttp/logs:
192
+ endpoint: http://victoria-logs:9428/insert/opentelemetry
193
+ tls:
194
+ insecure: true
195
+
196
+ prometheusremotewrite:
197
+ endpoint: http://victoria-metrics:8428/api/v1/write
198
+ tls:
199
+ insecure: true
200
+
201
+ otlp/traces:
202
+ endpoint: http://victoria-traces:14268
203
+ tls:
204
+ insecure: true
205
+
206
+ service:
207
+ pipelines:
208
+ logs:
209
+ receivers:
210
+ - otlp
211
+ processors:
212
+ - resource/default
213
+ exporters:
214
+ - otlphttp/logs
215
+ metrics:
216
+ receivers:
217
+ - otlp
218
+ processors:
219
+ - resource/default
220
+ exporters:
221
+ - prometheusremotewrite
222
+ traces:
223
+ receivers:
224
+ - otlp
225
+ processors:
226
+ - resource/default
227
+ exporters:
228
+ - otlp/traces
229
+ `;
230
+ }
231
+
232
+ // src/lib/docker.ts
233
+ function isDockerAvailable() {
234
+ try {
235
+ execFileSync("docker", ["--version"], { stdio: "pipe", timeout: 1e4 });
236
+ return true;
237
+ } catch {
238
+ return false;
239
+ }
240
+ }
241
+ function isDockerComposeAvailable() {
242
+ try {
243
+ execFileSync("docker", ["compose", "version"], { stdio: "pipe", timeout: 1e4 });
244
+ return true;
245
+ } catch {
246
+ return false;
247
+ }
248
+ }
249
+ function isStackRunning(composeFile) {
250
+ try {
251
+ const output = execFileSync("docker", ["compose", "-f", composeFile, "ps", "--format", "json"], {
252
+ stdio: "pipe",
253
+ timeout: 15e3
254
+ });
255
+ const text = output.toString().trim();
256
+ if (!text) return false;
257
+ const lines = text.split("\n").filter((l) => l.trim());
258
+ for (const line of lines) {
259
+ const svc = JSON.parse(line);
260
+ if (svc.State !== "running") return false;
261
+ }
262
+ return lines.length > 0;
263
+ } catch {
264
+ return false;
265
+ }
266
+ }
267
+ function isSharedStackRunning() {
268
+ try {
269
+ const composeFile = getComposeFilePath();
270
+ const output = execFileSync("docker", ["compose", "-p", "codeharness-shared", "-f", composeFile, "ps", "--format", "json"], {
271
+ stdio: "pipe",
272
+ timeout: 15e3
273
+ });
274
+ const text = output.toString().trim();
275
+ if (!text) return false;
276
+ const lines = text.split("\n").filter((l) => l.trim());
277
+ for (const line of lines) {
278
+ const svc = JSON.parse(line);
279
+ if (svc.State !== "running") return false;
280
+ }
281
+ return lines.length > 0;
282
+ } catch {
283
+ return false;
284
+ }
285
+ }
286
+ function startStack(composeFile) {
287
+ try {
288
+ execFileSync("docker", ["compose", "-f", composeFile, "up", "-d"], {
289
+ stdio: "pipe",
290
+ timeout: 3e4
291
+ });
292
+ const services = getRunningServices(composeFile);
293
+ return {
294
+ started: true,
295
+ services
296
+ };
297
+ } catch (err) {
298
+ const message = err instanceof Error ? err.message : String(err);
299
+ return {
300
+ started: false,
301
+ services: [],
302
+ error: message
303
+ };
304
+ }
305
+ }
306
+ function startSharedStack() {
307
+ try {
308
+ ensureStackDir();
309
+ const composeFile = getComposeFilePath();
310
+ const otelConfigFile = getOtelConfigPath();
311
+ const composeContent = dockerComposeTemplate({ shared: true });
312
+ writeFileSync(composeFile, composeContent, "utf-8");
313
+ const otelContent = otelCollectorConfigTemplate();
314
+ writeFileSync(otelConfigFile, otelContent, "utf-8");
315
+ execFileSync("docker", ["compose", "-p", "codeharness-shared", "-f", composeFile, "up", "-d"], {
316
+ stdio: "pipe",
317
+ timeout: 3e4
318
+ });
319
+ const services = getRunningServices(composeFile, "codeharness-shared");
320
+ return {
321
+ started: true,
322
+ services
323
+ };
324
+ } catch (err) {
325
+ const message = err instanceof Error ? err.message : String(err);
326
+ return {
327
+ started: false,
328
+ services: [],
329
+ error: message
330
+ };
331
+ }
332
+ }
333
+ function stopStack(composeFile) {
334
+ execFileSync("docker", ["compose", "-f", composeFile, "down", "-v"], {
335
+ stdio: "pipe",
336
+ timeout: 3e4
337
+ });
338
+ }
339
+ function stopSharedStack() {
340
+ const composeFile = getComposeFilePath();
341
+ execFileSync("docker", ["compose", "-p", "codeharness-shared", "-f", composeFile, "down"], {
342
+ stdio: "pipe",
343
+ timeout: 3e4
344
+ });
345
+ }
346
+ function startCollectorOnly(logsUrl, metricsUrl, tracesUrl) {
347
+ try {
348
+ ensureStackDir();
349
+ const composeFile = getComposeFilePath();
350
+ const otelConfigFile = getOtelConfigPath();
351
+ const composeContent = dockerComposeCollectorOnlyTemplate();
352
+ writeFileSync(composeFile, composeContent, "utf-8");
353
+ const otelContent = otelCollectorRemoteTemplate({ logsUrl, metricsUrl, tracesUrl });
354
+ writeFileSync(otelConfigFile, otelContent, "utf-8");
355
+ execFileSync("docker", ["compose", "-p", "codeharness-collector", "-f", composeFile, "up", "-d"], {
356
+ stdio: "pipe",
357
+ timeout: 3e4
358
+ });
359
+ const services = getRunningServices(composeFile, "codeharness-collector");
360
+ return {
361
+ started: true,
362
+ services
363
+ };
364
+ } catch (err) {
365
+ const message = err instanceof Error ? err.message : String(err);
366
+ return {
367
+ started: false,
368
+ services: [],
369
+ error: message
370
+ };
371
+ }
372
+ }
373
+ function isCollectorRunning() {
374
+ try {
375
+ const composeFile = getComposeFilePath();
376
+ const output = execFileSync("docker", ["compose", "-p", "codeharness-collector", "-f", composeFile, "ps", "--format", "json"], {
377
+ stdio: "pipe",
378
+ timeout: 15e3
379
+ });
380
+ const text = output.toString().trim();
381
+ if (!text) return false;
382
+ const lines = text.split("\n").filter((l) => l.trim());
383
+ for (const line of lines) {
384
+ const svc = JSON.parse(line);
385
+ if (svc.State !== "running") return false;
386
+ }
387
+ return lines.length > 0;
388
+ } catch {
389
+ return false;
390
+ }
391
+ }
392
+ function stopCollectorOnly() {
393
+ const composeFile = getComposeFilePath();
394
+ execFileSync("docker", ["compose", "-p", "codeharness-collector", "-f", composeFile, "down"], {
395
+ stdio: "pipe",
396
+ timeout: 3e4
397
+ });
398
+ }
399
+ function getCollectorHealth(composeFile) {
400
+ const expectedServices = ["otel-collector"];
401
+ try {
402
+ const output = execFileSync("docker", ["compose", "-p", "codeharness-collector", "-f", composeFile, "ps", "--format", "json"], {
403
+ stdio: "pipe",
404
+ timeout: 15e3
405
+ });
406
+ const text = output.toString().trim();
407
+ const runningNames = /* @__PURE__ */ new Set();
408
+ if (text) {
409
+ const lines = text.split("\n").filter((l) => l.trim());
410
+ for (const line of lines) {
411
+ const svc = JSON.parse(line);
412
+ if (svc.State === "running" && svc.Service) {
413
+ runningNames.add(svc.Service);
414
+ }
415
+ }
416
+ }
417
+ const services = expectedServices.map((name) => ({
418
+ name,
419
+ running: runningNames.has(name)
420
+ }));
421
+ const healthy = services.every((s) => s.running);
422
+ return {
423
+ healthy,
424
+ services,
425
+ remedy: healthy ? void 0 : `Restart: docker compose -p codeharness-collector -f ${composeFile} up -d`
426
+ };
427
+ } catch {
428
+ const services = expectedServices.map((name) => ({
429
+ name,
430
+ running: false
431
+ }));
432
+ return {
433
+ healthy: false,
434
+ services,
435
+ remedy: `Restart: docker compose -p codeharness-collector -f ${composeFile} up -d`
436
+ };
437
+ }
438
+ }
439
+ async function checkRemoteEndpoint(url) {
440
+ try {
441
+ const controller = new AbortController();
442
+ const timeout = setTimeout(() => controller.abort(), 5e3);
443
+ try {
444
+ await fetch(url, { signal: controller.signal });
445
+ return { reachable: true };
446
+ } finally {
447
+ clearTimeout(timeout);
448
+ }
449
+ } catch (err) {
450
+ const message = err instanceof Error ? err.message : String(err);
451
+ return { reachable: false, error: message };
452
+ }
453
+ }
454
+ function getStackHealth(composeFile, projectName) {
455
+ const expectedServices = ["victoria-logs", "victoria-metrics", "victoria-traces", "otel-collector"];
456
+ try {
457
+ const args = projectName ? ["compose", "-p", projectName, "-f", composeFile, "ps", "--format", "json"] : ["compose", "-f", composeFile, "ps", "--format", "json"];
458
+ const output = execFileSync("docker", args, {
459
+ stdio: "pipe",
460
+ timeout: 15e3
461
+ });
462
+ const text = output.toString().trim();
463
+ const runningNames = /* @__PURE__ */ new Set();
464
+ if (text) {
465
+ const lines = text.split("\n").filter((l) => l.trim());
466
+ for (const line of lines) {
467
+ const svc = JSON.parse(line);
468
+ if (svc.State === "running" && svc.Service) {
469
+ runningNames.add(svc.Service);
470
+ }
471
+ }
472
+ }
473
+ const services = expectedServices.map((name) => ({
474
+ name,
475
+ running: runningNames.has(name)
476
+ }));
477
+ const healthy = services.every((s) => s.running);
478
+ const remedyCmd = projectName ? `docker compose -p ${projectName} -f ${composeFile} up -d` : `docker compose -f ${composeFile} up -d`;
479
+ return {
480
+ healthy,
481
+ services,
482
+ remedy: healthy ? void 0 : `Restart: ${remedyCmd}`
483
+ };
484
+ } catch {
485
+ const remedyCmd = projectName ? `docker compose -p ${projectName} -f ${composeFile} up -d` : `docker compose -f ${composeFile} up -d`;
486
+ const services = expectedServices.map((name) => ({
487
+ name,
488
+ running: false
489
+ }));
490
+ return {
491
+ healthy: false,
492
+ services,
493
+ remedy: `Restart: ${remedyCmd}`
494
+ };
495
+ }
496
+ }
497
+ function getRunningServices(composeFile, projectName) {
498
+ try {
499
+ const args = projectName ? ["compose", "-p", projectName, "-f", composeFile, "ps", "--format", "json"] : ["compose", "-f", composeFile, "ps", "--format", "json"];
500
+ const output = execFileSync("docker", args, {
501
+ stdio: "pipe",
502
+ timeout: 15e3
503
+ });
504
+ const text = output.toString().trim();
505
+ if (!text) return [];
506
+ const lines = text.split("\n").filter((l) => l.trim());
507
+ const services = [];
508
+ for (const line of lines) {
509
+ const svc = JSON.parse(line);
510
+ const ports = (svc.Publishers ?? []).filter((p) => p.PublishedPort && p.PublishedPort > 0).map((p) => String(p.PublishedPort));
511
+ services.push({
512
+ name: svc.Service ?? "unknown",
513
+ status: svc.State ?? "unknown",
514
+ port: ports.join(",")
515
+ });
516
+ }
517
+ return services;
518
+ } catch {
519
+ return [];
520
+ }
521
+ }
522
+
523
+ export {
524
+ getStackDir,
525
+ getComposeFilePath,
526
+ isDockerAvailable,
527
+ isDockerComposeAvailable,
528
+ isStackRunning,
529
+ isSharedStackRunning,
530
+ startStack,
531
+ startSharedStack,
532
+ stopStack,
533
+ stopSharedStack,
534
+ startCollectorOnly,
535
+ isCollectorRunning,
536
+ stopCollectorOnly,
537
+ getCollectorHealth,
538
+ checkRemoteEndpoint,
539
+ getStackHealth
540
+ };
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ checkRemoteEndpoint,
4
+ getCollectorHealth,
5
+ getStackHealth,
6
+ isCollectorRunning,
7
+ isDockerAvailable,
8
+ isDockerComposeAvailable,
9
+ isSharedStackRunning,
10
+ isStackRunning,
11
+ startCollectorOnly,
12
+ startSharedStack,
13
+ startStack,
14
+ stopCollectorOnly,
15
+ stopSharedStack,
16
+ stopStack
17
+ } from "./chunk-7ZD2ZNDU.js";
18
+ export {
19
+ checkRemoteEndpoint,
20
+ getCollectorHealth,
21
+ getStackHealth,
22
+ isCollectorRunning,
23
+ isDockerAvailable,
24
+ isDockerComposeAvailable,
25
+ isSharedStackRunning,
26
+ isStackRunning,
27
+ startCollectorOnly,
28
+ startSharedStack,
29
+ startStack,
30
+ stopCollectorOnly,
31
+ stopSharedStack,
32
+ stopStack
33
+ };