@paretools/docker 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,9 @@
1
1
  /** Formats structured Docker container data into a human-readable listing with state and ports. */
2
2
  export function formatPs(data) {
3
- const lines = [`${data.total} containers (${data.running} running, ${data.stopped} stopped)`];
3
+ const total = data.containers.length;
4
+ const running = data.containers.filter((c) => c.state === "running").length;
5
+ const stopped = total - running;
6
+ const lines = [`${total} containers (${running} running, ${stopped} stopped)`];
4
7
  for (const c of data.containers) {
5
8
  const portsArr = c.ports ?? [];
6
9
  const ports = portsArr.length
@@ -16,20 +19,24 @@ export function formatPs(data) {
16
19
  /** Formats structured Docker build results into a human-readable success/failure summary.
17
20
  * #97: Includes structured error details with line numbers. */
18
21
  export function formatBuild(data) {
22
+ const cacheByStep = data.cacheByStep ?? [];
23
+ const cacheHits = cacheByStep.filter((s) => s.cached).length;
24
+ const cacheMisses = cacheByStep.filter((s) => !s.cached).length;
25
+ const steps = cacheByStep.length;
19
26
  if (data.success) {
20
- const parts = [`Build succeeded in ${data.duration}s`];
27
+ const parts = ["Build succeeded"];
21
28
  if (data.imageId)
22
29
  parts[0] += ` → ${data.imageId}`;
23
- if (data.steps)
24
- parts.push(`${data.steps} steps`);
25
- if (data.cacheHits != null || data.cacheMisses != null) {
26
- parts.push(`cache hits=${data.cacheHits ?? 0}, misses=${data.cacheMisses ?? 0}`);
30
+ if (steps > 0)
31
+ parts.push(`${steps} steps`);
32
+ if (cacheByStep.length > 0) {
33
+ parts.push(`cache hits=${cacheHits}, misses=${cacheMisses}`);
27
34
  }
28
35
  return parts.join(", ");
29
36
  }
30
- const lines = [`Build failed (${data.duration}s)`];
31
- if (data.cacheHits != null || data.cacheMisses != null) {
32
- lines.push(` cache hits=${data.cacheHits ?? 0}, misses=${data.cacheMisses ?? 0}`);
37
+ const lines = ["Build failed"];
38
+ if (cacheByStep.length > 0) {
39
+ lines.push(` cache hits=${cacheHits}, misses=${cacheMisses}`);
33
40
  }
34
41
  for (const err of data.errors ?? []) {
35
42
  if (typeof err === "string") {
@@ -48,12 +55,11 @@ export function formatBuild(data) {
48
55
  }
49
56
  return lines.join("\n");
50
57
  }
51
- /** Formats structured Docker logs data into a human-readable output with container name and line count.
58
+ /** Formats structured Docker logs data into a human-readable output with line count.
52
59
  * #113: Notes when separate stdout/stderr streams are available. */
53
60
  export function formatLogs(data) {
54
- const header = data.isTruncated
55
- ? `${data.container} (${data.total} of ${data.totalLines} lines, truncated)`
56
- : `${data.container} (${data.total} lines)`;
61
+ const total = (data.lines ?? []).length;
62
+ const header = data.isTruncated ? `${total} lines (truncated)` : `${total} lines`;
57
63
  const streamInfo = data.stdoutLines || data.stderrLines
58
64
  ? ` [stdout=${(data.stdoutLines ?? []).length}, stderr=${(data.stderrLines ?? []).length}]`
59
65
  : "";
@@ -65,9 +71,10 @@ export function formatLogs(data) {
65
71
  /** Formats structured Docker image data into a human-readable listing with repository, tag, and size.
66
72
  * #110: Shows ISO creation timestamp when available. */
67
73
  export function formatImages(data) {
68
- if (data.total === 0)
74
+ const total = data.images.length;
75
+ if (total === 0)
69
76
  return "No images found.";
70
- const lines = [`${data.total} images:`];
77
+ const lines = [`${total} images:`];
71
78
  for (const img of data.images) {
72
79
  const tag = img.tag && img.tag !== "<none>" ? `:${img.tag}` : "";
73
80
  const digest = img.digest ? ` [${img.digest.slice(0, 19)}...]` : "";
@@ -85,7 +92,7 @@ export function formatImages(data) {
85
92
  export function formatRun(data) {
86
93
  const name = data.name ? ` (${data.name})` : "";
87
94
  const mode = data.detached ? "detached" : "attached";
88
- const lines = [`Container ${data.containerId}${name} started from ${data.image} [${mode}]`];
95
+ const lines = [`Container ${data.containerId}${name} started [${mode}]`];
89
96
  if (data.exitCode != null && data.exitCode !== 0) {
90
97
  lines.push(` Exit code: ${data.exitCode}`);
91
98
  if (data.errorCategory)
@@ -104,14 +111,14 @@ export function formatRun(data) {
104
111
  /** Formats structured Docker exec output into a human-readable summary.
105
112
  * #108: Shows truncation indicator. */
106
113
  export function formatExec(data) {
114
+ const success = data.exitCode === 0;
107
115
  const status = data.timedOut
108
116
  ? "timed out"
109
- : data.success
117
+ : success
110
118
  ? "succeeded"
111
119
  : `failed (exit code ${data.exitCode})`;
112
- const dur = data.duration != null ? ` in ${data.duration}s` : "";
113
120
  const trunc = data.isTruncated ? " [truncated]" : "";
114
- const lines = [`Exec ${status}${dur}${trunc}`];
121
+ const lines = [`Exec ${status}${trunc}`];
115
122
  if (data.stdout)
116
123
  lines.push(data.stdout);
117
124
  if (data.stderr)
@@ -127,11 +134,10 @@ export function formatExec(data) {
127
134
  export function formatComposeUp(data) {
128
135
  if (!data.success)
129
136
  return "Compose up failed";
130
- if (data.started === 0)
137
+ const started = (data.services ?? []).length;
138
+ if (started === 0)
131
139
  return "Compose up succeeded (no new services started)";
132
- const lines = [
133
- `Compose up: ${data.started} services started (${(data.services ?? []).join(", ")})`,
134
- ];
140
+ const lines = [`Compose up: ${started} services started (${(data.services ?? []).join(", ")})`];
135
141
  if (data.serviceStates && data.serviceStates.length > 0) {
136
142
  for (const ss of data.serviceStates) {
137
143
  lines.push(` ${ss.name}: ${ss.action}`);
@@ -149,7 +155,9 @@ export function formatComposeUp(data) {
149
155
  export function formatComposeDown(data) {
150
156
  if (!data.success)
151
157
  return "Compose down failed";
152
- const parts = [`Compose down: ${data.stopped} stopped, ${data.removed} removed`];
158
+ const removedContainers = (data.containers ?? []).filter((c) => c.action === "Removed").length;
159
+ const removed = removedContainers + (data.volumesRemoved ?? 0) + (data.networksRemoved ?? 0);
160
+ const parts = [`Compose down: ${data.stopped} stopped, ${removed} removed`];
153
161
  if (data.volumesRemoved)
154
162
  parts[0] += `, ${data.volumesRemoved} volumes removed`;
155
163
  if (data.networksRemoved)
@@ -167,13 +175,13 @@ export function formatPull(data) {
167
175
  if (!data.success) {
168
176
  const errType = data.errorType ? ` (${data.errorType})` : "";
169
177
  const msg = data.errorMessage ? `: ${data.errorMessage}` : "";
170
- return `Pull failed for ${data.image}:${data.tag}${errType}${msg}`;
178
+ return `Pull failed${errType}${msg}`;
171
179
  }
172
180
  const digest = data.digest ? ` (${data.digest.slice(0, 19)}...)` : "";
173
181
  const size = data.size ? ` [${data.size}]` : "";
174
182
  if (data.status === "up-to-date")
175
- return `${data.image}:${data.tag} is up to date${digest}`;
176
- return `Pulled ${data.image}:${data.tag}${digest}${size}`;
183
+ return `Image is up to date${digest}`;
184
+ return `Pulled${digest}${size}`;
177
185
  }
178
186
  export function compactPsMap(data) {
179
187
  return {
@@ -183,13 +191,12 @@ export function compactPsMap(data) {
183
191
  image: c.image,
184
192
  status: c.status,
185
193
  })),
186
- total: data.total,
187
- running: data.running,
188
- stopped: data.stopped,
189
194
  };
190
195
  }
191
196
  export function formatPsCompact(data) {
192
- const lines = [`${data.total} containers (${data.running} running)`];
197
+ const total = data.containers.length;
198
+ const running = data.containers.filter((c) => c.status.toLowerCase().startsWith("up")).length;
199
+ const lines = [`${total} containers (${running} running)`];
193
200
  for (const c of data.containers) {
194
201
  lines.push(` ${c.id.slice(0, 12)} ${c.name} (${c.image}) ${c.status}`);
195
202
  }
@@ -203,13 +210,13 @@ export function compactImagesMap(data) {
203
210
  tag: img.tag,
204
211
  size: img.size,
205
212
  })),
206
- total: data.total,
207
213
  };
208
214
  }
209
215
  export function formatImagesCompact(data) {
210
- if (data.total === 0)
216
+ const total = data.images.length;
217
+ if (total === 0)
211
218
  return "No images found.";
212
- const lines = [`${data.total} images:`];
219
+ const lines = [`${total} images:`];
213
220
  for (const img of data.images) {
214
221
  const tag = img.tag && img.tag !== "<none>" ? `:${img.tag}` : "";
215
222
  lines.push(` ${img.repository}${tag} (${img.size})`);
@@ -220,48 +227,35 @@ export function compactBuildMap(data) {
220
227
  return {
221
228
  success: data.success,
222
229
  ...(data.imageId ? { imageId: data.imageId } : {}),
223
- duration: data.duration,
224
- errorCount: (data.errors ?? []).length,
225
- ...(data.cacheHits != null ? { cacheHits: data.cacheHits } : {}),
226
- ...(data.cacheMisses != null ? { cacheMisses: data.cacheMisses } : {}),
227
230
  };
228
231
  }
229
232
  export function formatBuildCompact(data) {
230
233
  if (data.success) {
231
234
  const id = data.imageId ? ` → ${data.imageId}` : "";
232
- const cache = data.cacheHits != null || data.cacheMisses != null
233
- ? ` [cache h=${data.cacheHits ?? 0} m=${data.cacheMisses ?? 0}]`
234
- : "";
235
- return `Build succeeded in ${data.duration}s${id}${cache}`;
235
+ return `Build succeeded${id}`;
236
236
  }
237
- const cache = data.cacheHits != null || data.cacheMisses != null
238
- ? `, cache h=${data.cacheHits ?? 0} m=${data.cacheMisses ?? 0}`
239
- : "";
240
- return `Build failed (${data.duration}s, ${data.errorCount} errors${cache})`;
237
+ return "Build failed";
241
238
  }
242
239
  export function compactLogsMap(data) {
243
240
  const HEAD_SIZE = 5;
244
241
  const TAIL_SIZE = 5;
245
242
  const lines = data.lines ?? [];
243
+ const total = lines.length;
246
244
  return {
247
- container: data.container,
248
- total: data.total,
249
245
  head: lines.slice(0, HEAD_SIZE),
250
- tail: data.total > HEAD_SIZE + TAIL_SIZE ? lines.slice(-TAIL_SIZE) : [],
246
+ tail: total > HEAD_SIZE + TAIL_SIZE ? lines.slice(-TAIL_SIZE) : [],
251
247
  };
252
248
  }
253
249
  export function formatLogsCompact(data) {
254
- const parts = [`${data.container} (${data.total} lines)`];
250
+ const parts = [`${data.head.length + data.tail.length} lines`];
255
251
  if (data.head.length)
256
252
  parts.push(data.head.join("\n"));
257
253
  if (data.tail.length)
258
- parts.push(` ... ${data.total - data.head.length - data.tail.length} lines omitted ...`, data.tail.join("\n"));
254
+ parts.push(" ... lines omitted ...", data.tail.join("\n"));
259
255
  return parts.join("\n");
260
256
  }
261
257
  export function compactPullMap(data) {
262
258
  return {
263
- image: data.image,
264
- tag: data.tag,
265
259
  ...(data.digest ? { digest: data.digest } : {}),
266
260
  status: data.status,
267
261
  success: data.success,
@@ -271,29 +265,26 @@ export function compactPullMap(data) {
271
265
  export function formatPullCompact(data) {
272
266
  if (!data.success) {
273
267
  const err = data.errorType ? ` (${data.errorType})` : "";
274
- return `Pull failed for ${data.image}:${data.tag}${err}`;
268
+ return `Pull failed${err}`;
275
269
  }
276
270
  const digest = data.digest ? ` (${data.digest.slice(0, 19)}...)` : "";
277
271
  if (data.status === "up-to-date")
278
- return `${data.image}:${data.tag} is up to date${digest}`;
279
- return `Pulled ${data.image}:${data.tag}${digest}`;
272
+ return `Image is up to date${digest}`;
273
+ return `Pulled${digest}`;
280
274
  }
281
275
  export function compactRunMap(data) {
282
276
  return {
283
277
  containerId: data.containerId,
284
- image: data.image,
285
278
  detached: data.detached,
286
279
  };
287
280
  }
288
281
  export function formatRunCompact(data) {
289
282
  const mode = data.detached ? "detached" : "attached";
290
- return `Container ${data.containerId} from ${data.image} [${mode}]`;
283
+ return `Container ${data.containerId} [${mode}]`;
291
284
  }
292
285
  export function compactExecMap(data) {
293
286
  return {
294
287
  exitCode: data.exitCode,
295
- success: data.success,
296
- ...(data.duration != null ? { duration: data.duration } : {}),
297
288
  ...(data.timedOut ? { timedOut: true } : {}),
298
289
  ...(data.isTruncated ? { isTruncated: true } : {}),
299
290
  ...(data.stdout ? { stdoutPreview: data.stdout.slice(0, 200) } : {}),
@@ -301,14 +292,14 @@ export function compactExecMap(data) {
301
292
  };
302
293
  }
303
294
  export function formatExecCompact(data) {
304
- const dur = data.duration != null ? ` in ${data.duration}s` : "";
295
+ const success = data.exitCode === 0;
305
296
  const status = data.timedOut
306
297
  ? "timed out"
307
- : data.success
298
+ : success
308
299
  ? "succeeded"
309
300
  : `failed (exit code ${data.exitCode})`;
310
301
  const trunc = data.isTruncated ? " [truncated]" : "";
311
- const lines = [`Exec ${status}${dur}${trunc}`];
302
+ const lines = [`Exec ${status}${trunc}`];
312
303
  if (data.stdoutPreview)
313
304
  lines.push(data.stdoutPreview);
314
305
  if (data.stderrPreview)
@@ -318,7 +309,7 @@ export function formatExecCompact(data) {
318
309
  export function compactComposeUpMap(data) {
319
310
  return {
320
311
  success: data.success,
321
- started: data.started,
312
+ ...(data.services ? { services: data.services } : {}),
322
313
  ...(data.networksCreated != null ? { networksCreated: data.networksCreated } : {}),
323
314
  ...(data.volumesCreated != null ? { volumesCreated: data.volumesCreated } : {}),
324
315
  };
@@ -326,22 +317,22 @@ export function compactComposeUpMap(data) {
326
317
  export function formatComposeUpCompact(data) {
327
318
  if (!data.success)
328
319
  return "Compose up failed";
320
+ const started = (data.services ?? []).length;
329
321
  const infra = data.networksCreated != null || data.volumesCreated != null
330
322
  ? ` (networks=${data.networksCreated ?? 0}, volumes=${data.volumesCreated ?? 0})`
331
323
  : "";
332
- return `Compose up: ${data.started} services started${infra}`;
324
+ return `Compose up: ${started} services started${infra}`;
333
325
  }
334
326
  export function compactComposeDownMap(data) {
335
327
  return {
336
328
  success: data.success,
337
329
  stopped: data.stopped,
338
- removed: data.removed,
339
330
  };
340
331
  }
341
332
  export function formatComposeDownCompact(data) {
342
333
  if (!data.success)
343
334
  return "Compose down failed";
344
- return `Compose down: ${data.stopped} stopped, ${data.removed} removed`;
335
+ return `Compose down: ${data.stopped} stopped`;
345
336
  }
346
337
  // ── Inspect ──────────────────────────────────────────────────────────
347
338
  /** Formats structured Docker inspect data into a human-readable summary.
@@ -508,9 +499,10 @@ export function formatInspectCompact(data) {
508
499
  * #115: Shows labels when present.
509
500
  * #116: Shows boolean flags. */
510
501
  export function formatNetworkLs(data) {
511
- if (data.total === 0)
502
+ const total = data.networks.length;
503
+ if (total === 0)
512
504
  return "No networks found.";
513
- const lines = [`${data.total} networks:`];
505
+ const lines = [`${total} networks:`];
514
506
  for (const n of data.networks) {
515
507
  const created = n.createdAt ? ` (${n.createdAt})` : "";
516
508
  const flags = [];
@@ -533,13 +525,13 @@ export function compactNetworkLsMap(data) {
533
525
  name: n.name,
534
526
  driver: n.driver,
535
527
  })),
536
- total: data.total,
537
528
  };
538
529
  }
539
530
  export function formatNetworkLsCompact(data) {
540
- if (data.total === 0)
531
+ const total = data.networks.length;
532
+ if (total === 0)
541
533
  return "No networks found.";
542
- const lines = [`${data.total} networks:`];
534
+ const lines = [`${total} networks:`];
543
535
  for (const n of data.networks) {
544
536
  const id = n.id ? ` [${n.id}]` : "";
545
537
  lines.push(` ${n.name} (${n.driver})${id}`);
@@ -550,9 +542,10 @@ export function formatNetworkLsCompact(data) {
550
542
  /** Formats structured Docker volume list into a human-readable listing.
551
543
  * #125: Shows labels when present. */
552
544
  export function formatVolumeLs(data) {
553
- if (data.total === 0)
545
+ const total = data.volumes.length;
546
+ if (total === 0)
554
547
  return "No volumes found.";
555
- const lines = [`${data.total} volumes:`];
548
+ const lines = [`${total} volumes:`];
556
549
  for (const v of data.volumes) {
557
550
  const created = v.createdAt ? ` (${v.createdAt})` : "";
558
551
  const status = v.status ? ` status=${v.status}` : "";
@@ -568,13 +561,13 @@ export function compactVolumeLsMap(data) {
568
561
  driver: v.driver,
569
562
  ...(v.mountpoint ? { mountpoint: v.mountpoint } : {}),
570
563
  })),
571
- total: data.total,
572
564
  };
573
565
  }
574
566
  export function formatVolumeLsCompact(data) {
575
- if (data.total === 0)
567
+ const total = data.volumes.length;
568
+ if (total === 0)
576
569
  return "No volumes found.";
577
- const lines = [`${data.total} volumes:`];
570
+ const lines = [`${total} volumes:`];
578
571
  for (const v of data.volumes) {
579
572
  const mp = v.mountpoint ? ` @ ${v.mountpoint}` : "";
580
573
  lines.push(` ${v.name} (${v.driver})${mp}`);
@@ -585,15 +578,18 @@ export function formatVolumeLsCompact(data) {
585
578
  /** Formats structured Docker Compose build output into a human-readable summary.
586
579
  * #99: Shows per-service duration. */
587
580
  export function formatComposeBuild(data) {
588
- if (!data.success && data.built === 0) {
589
- const lines = [`Compose build failed (${data.duration}s)`];
590
- for (const s of data.services ?? []) {
581
+ const services = data.services ?? [];
582
+ const built = services.filter((s) => s.success).length;
583
+ const failed = services.filter((s) => !s.success).length;
584
+ if (!data.success && built === 0) {
585
+ const lines = ["Compose build failed"];
586
+ for (const s of services) {
591
587
  if (s.error)
592
588
  lines.push(` ${s.service}: ${s.error}`);
593
589
  }
594
590
  return lines.join("\n");
595
591
  }
596
- const lines = [`Compose build: ${data.built} built, ${data.failed} failed (${data.duration}s)`];
592
+ const lines = [`Compose build: ${built} built, ${failed} failed`];
597
593
  for (const s of data.services ?? []) {
598
594
  const status = s.success ? "built" : "failed";
599
595
  const error = s.error ? ` — ${s.error}` : "";
@@ -608,9 +604,10 @@ export function formatComposeBuild(data) {
608
604
  /** Formats structured Docker stats data into a human-readable listing with CPU, memory, and I/O.
609
605
  * #123/#124: Shows numeric memory and I/O values. */
610
606
  export function formatStats(data) {
611
- if (data.total === 0)
607
+ const total = data.containers.length;
608
+ if (total === 0)
612
609
  return "No container stats available.";
613
- const lines = [`${data.total} containers:`];
610
+ const lines = [`${total} containers:`];
614
611
  for (const c of data.containers) {
615
612
  const state = c.state ? ` [${c.state}]` : "";
616
613
  lines.push(` ${c.name}${state} (${c.id}) CPU: ${c.cpuPercent.toFixed(2)}% Mem: ${c.memoryUsage}/${c.memoryLimit} (${c.memoryPercent.toFixed(2)}%) Net: ${c.netIO} Block: ${c.blockIO} PIDs: ${c.pids}`);
@@ -620,16 +617,24 @@ export function formatStats(data) {
620
617
  export function compactComposeBuildMap(data) {
621
618
  return {
622
619
  success: data.success,
623
- built: data.built,
624
- failed: data.failed,
625
- duration: data.duration,
620
+ ...(data.services
621
+ ? {
622
+ services: data.services.map((s) => ({
623
+ service: s.service,
624
+ success: s.success,
625
+ })),
626
+ }
627
+ : {}),
626
628
  };
627
629
  }
628
630
  export function formatComposeBuildCompact(data) {
629
- if (!data.success && data.built === 0) {
630
- return `Compose build failed (${data.duration}s)`;
631
+ const services = data.services ?? [];
632
+ const built = services.filter((s) => s.success).length;
633
+ const failed = services.filter((s) => !s.success).length;
634
+ if (!data.success && built === 0) {
635
+ return "Compose build failed";
631
636
  }
632
- return `Compose build: ${data.built} built, ${data.failed} failed (${data.duration}s)`;
637
+ return `Compose build: ${built} built, ${failed} failed`;
633
638
  }
634
639
  export function compactStatsMap(data) {
635
640
  return {
@@ -642,13 +647,13 @@ export function compactStatsMap(data) {
642
647
  memoryPercent: c.memoryPercent,
643
648
  pids: c.pids,
644
649
  })),
645
- total: data.total,
646
650
  };
647
651
  }
648
652
  export function formatStatsCompact(data) {
649
- if (data.total === 0)
653
+ const total = data.containers.length;
654
+ if (total === 0)
650
655
  return "No container stats available.";
651
- const lines = [`${data.total} containers:`];
656
+ const lines = [`${total} containers:`];
652
657
  for (const c of data.containers) {
653
658
  const mem = c.memoryUsage ? ` ${c.memoryUsage}` : "";
654
659
  const state = c.state ? ` [${c.state}]` : "";
@@ -661,12 +666,13 @@ export function formatStatsCompact(data) {
661
666
  * #105: Shows health status.
662
667
  * #106: Shows running/stopped counts. */
663
668
  export function formatComposePs(data) {
664
- if (data.total === 0)
669
+ const total = data.services.length;
670
+ if (total === 0)
665
671
  return "No compose services found.";
666
- const countInfo = data.running != null && data.stopped != null
667
- ? ` (${data.running} running, ${data.stopped} stopped)`
668
- : "";
669
- const lines = [`${data.total} services${countInfo}:`];
672
+ const running = data.services.filter((s) => s.state === "running").length;
673
+ const stopped = data.services.filter((s) => s.state === "exited" || s.state === "dead" || s.state === "removing").length;
674
+ const countInfo = ` (${running} running, ${stopped} stopped)`;
675
+ const lines = [`${total} services${countInfo}:`];
670
676
  for (const s of data.services) {
671
677
  const portsArr = s.ports ?? [];
672
678
  const ports = portsArr.length
@@ -686,13 +692,13 @@ export function compactComposePsMap(data) {
686
692
  state: s.state,
687
693
  ...(s.exitCode != null ? { exitCode: s.exitCode } : {}),
688
694
  })),
689
- total: data.total,
690
695
  };
691
696
  }
692
697
  export function formatComposePsCompact(data) {
693
- if (data.total === 0)
698
+ const total = data.services.length;
699
+ if (total === 0)
694
700
  return "No compose services found.";
695
- const lines = [`${data.total} services:`];
701
+ const lines = [`${total} services:`];
696
702
  for (const s of data.services) {
697
703
  const exit = s.exitCode != null ? ` exit=${s.exitCode}` : "";
698
704
  lines.push(` ${s.state.padEnd(10)} ${s.name} (${s.service})${exit}`);
@@ -703,9 +709,13 @@ export function formatComposePsCompact(data) {
703
709
  /** Formats structured Docker Compose logs into a human-readable output grouped by service.
704
710
  * #104: Shows log level when available. */
705
711
  export function formatComposeLogs(data) {
712
+ const entries = data.entries ?? [];
713
+ const total = entries.length;
714
+ const serviceSet = new Set(entries.map((e) => e.service));
715
+ const serviceCount = serviceSet.size;
706
716
  const header = data.isTruncated
707
- ? `Compose logs: ${data.services.length} services, ${data.total} of ${data.totalEntries} entries (truncated)`
708
- : `Compose logs: ${data.services.length} services, ${data.total} entries`;
717
+ ? `Compose logs: ${serviceCount} services, ${total} entries (truncated)`
718
+ : `Compose logs: ${serviceCount} services, ${total} entries`;
709
719
  const lines = [header];
710
720
  for (const entry of data.entries ?? []) {
711
721
  const ts = entry.timestamp ? `${entry.timestamp} ` : "";
@@ -718,15 +728,14 @@ export function compactComposeLogsMap(data) {
718
728
  const HEAD_SIZE = 5;
719
729
  const TAIL_SIZE = 5;
720
730
  const entries = data.entries ?? [];
731
+ const total = entries.length;
721
732
  return {
722
- services: data.services,
723
- total: data.total,
724
733
  head: entries.slice(0, HEAD_SIZE).map((e) => ({
725
734
  service: e.service,
726
735
  message: e.message,
727
736
  ...(e.timestamp ? { timestamp: e.timestamp } : {}),
728
737
  })),
729
- tail: data.total > HEAD_SIZE + TAIL_SIZE
738
+ tail: total > HEAD_SIZE + TAIL_SIZE
730
739
  ? entries.slice(-TAIL_SIZE).map((e) => ({
731
740
  service: e.service,
732
741
  message: e.message,
@@ -736,13 +745,15 @@ export function compactComposeLogsMap(data) {
736
745
  };
737
746
  }
738
747
  export function formatComposeLogsCompact(data) {
739
- const parts = [`Compose logs: ${data.services.length} services, ${data.total} entries`];
748
+ const total = data.head.length + data.tail.length;
749
+ const serviceSet = new Set([...data.head, ...data.tail].map((e) => e.service));
750
+ const parts = [`Compose logs: ${serviceSet.size} services, ${total} entries`];
740
751
  for (const e of data.head) {
741
752
  const ts = e.timestamp ? `${e.timestamp} ` : "";
742
753
  parts.push(` ${e.service} | ${ts}${e.message}`);
743
754
  }
744
755
  if (data.tail.length) {
745
- parts.push(` ... ${data.total - data.head.length - data.tail.length} entries omitted ...`);
756
+ parts.push(" ... entries omitted ...");
746
757
  for (const e of data.tail) {
747
758
  const ts = e.timestamp ? `${e.timestamp} ` : "";
748
759
  parts.push(` ${e.service} | ${ts}${e.message}`);