shipwright-cli 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (143) hide show
  1. package/README.md +12 -11
  2. package/dashboard/public/index.html +224 -8
  3. package/dashboard/public/styles.css +1078 -4
  4. package/dashboard/server.ts +1100 -15
  5. package/dashboard/src/canvas/interactions.ts +74 -0
  6. package/dashboard/src/canvas/layout.ts +85 -0
  7. package/dashboard/src/canvas/overlays.ts +117 -0
  8. package/dashboard/src/canvas/particles.ts +105 -0
  9. package/dashboard/src/canvas/renderer.ts +191 -0
  10. package/dashboard/src/components/charts/bar.ts +54 -0
  11. package/dashboard/src/components/charts/donut.ts +25 -0
  12. package/dashboard/src/components/charts/pipeline-rail.ts +105 -0
  13. package/dashboard/src/components/charts/sparkline.ts +82 -0
  14. package/dashboard/src/components/header.ts +616 -0
  15. package/dashboard/src/components/modal.ts +413 -0
  16. package/dashboard/src/components/terminal.ts +144 -0
  17. package/dashboard/src/core/api.ts +381 -0
  18. package/dashboard/src/core/helpers.ts +118 -0
  19. package/dashboard/src/core/router.ts +190 -0
  20. package/dashboard/src/core/sse.ts +38 -0
  21. package/dashboard/src/core/state.ts +150 -0
  22. package/dashboard/src/core/ws.ts +143 -0
  23. package/dashboard/src/design/icons.ts +131 -0
  24. package/dashboard/src/design/tokens.ts +160 -0
  25. package/dashboard/src/main.ts +68 -0
  26. package/dashboard/src/types/api.ts +337 -0
  27. package/dashboard/src/views/activity.ts +185 -0
  28. package/dashboard/src/views/agent-cockpit.ts +236 -0
  29. package/dashboard/src/views/agents.ts +72 -0
  30. package/dashboard/src/views/fleet-map.ts +299 -0
  31. package/dashboard/src/views/insights.ts +298 -0
  32. package/dashboard/src/views/machines.ts +162 -0
  33. package/dashboard/src/views/metrics.ts +420 -0
  34. package/dashboard/src/views/overview.ts +409 -0
  35. package/dashboard/src/views/pipeline-theater.ts +219 -0
  36. package/dashboard/src/views/pipelines.ts +595 -0
  37. package/dashboard/src/views/team.ts +362 -0
  38. package/dashboard/src/views/timeline.ts +389 -0
  39. package/dashboard/tsconfig.json +21 -0
  40. package/docs/AGI-WHATS-NEXT.md +15 -15
  41. package/package.json +8 -1
  42. package/scripts/lib/helpers.sh +30 -0
  43. package/scripts/lib/pipeline-quality-checks.sh +1 -1
  44. package/scripts/sw +86 -167
  45. package/scripts/sw-activity.sh +1 -1
  46. package/scripts/sw-adaptive.sh +1 -1
  47. package/scripts/sw-adversarial.sh +1 -1
  48. package/scripts/sw-architecture-enforcer.sh +1 -1
  49. package/scripts/sw-auth.sh +14 -6
  50. package/scripts/sw-autonomous.sh +1 -1
  51. package/scripts/sw-changelog.sh +2 -2
  52. package/scripts/sw-checkpoint.sh +1 -1
  53. package/scripts/sw-ci.sh +1 -1
  54. package/scripts/sw-cleanup.sh +1 -1
  55. package/scripts/sw-code-review.sh +1 -1
  56. package/scripts/sw-connect.sh +1 -1
  57. package/scripts/sw-context.sh +1 -1
  58. package/scripts/sw-cost.sh +1 -1
  59. package/scripts/sw-daemon.sh +2 -2
  60. package/scripts/sw-dashboard.sh +1 -1
  61. package/scripts/sw-db.sh +1 -1
  62. package/scripts/sw-decompose.sh +1 -1
  63. package/scripts/sw-deps.sh +1 -1
  64. package/scripts/sw-developer-simulation.sh +1 -1
  65. package/scripts/sw-discovery.sh +1 -1
  66. package/scripts/sw-doc-fleet.sh +1 -1
  67. package/scripts/sw-docs-agent.sh +1 -1
  68. package/scripts/sw-docs.sh +1 -1
  69. package/scripts/sw-doctor.sh +8 -1
  70. package/scripts/sw-dora.sh +1 -1
  71. package/scripts/sw-durable.sh +1 -1
  72. package/scripts/sw-e2e-orchestrator.sh +1 -1
  73. package/scripts/sw-eventbus.sh +1 -1
  74. package/scripts/sw-feedback.sh +1 -1
  75. package/scripts/sw-fix.sh +6 -5
  76. package/scripts/sw-fleet-discover.sh +1 -1
  77. package/scripts/sw-fleet-viz.sh +1 -1
  78. package/scripts/sw-fleet.sh +1 -1
  79. package/scripts/sw-github-app.sh +5 -2
  80. package/scripts/sw-github-checks.sh +1 -1
  81. package/scripts/sw-github-deploy.sh +1 -1
  82. package/scripts/sw-github-graphql.sh +1 -1
  83. package/scripts/sw-guild.sh +1 -1
  84. package/scripts/sw-heartbeat.sh +1 -1
  85. package/scripts/sw-hygiene.sh +1 -1
  86. package/scripts/sw-incident.sh +1 -1
  87. package/scripts/sw-init.sh +112 -9
  88. package/scripts/sw-instrument.sh +6 -1
  89. package/scripts/sw-intelligence.sh +5 -1
  90. package/scripts/sw-jira.sh +1 -1
  91. package/scripts/sw-launchd.sh +1 -1
  92. package/scripts/sw-linear.sh +20 -9
  93. package/scripts/sw-logs.sh +1 -1
  94. package/scripts/sw-loop.sh +2 -1
  95. package/scripts/sw-memory.sh +10 -1
  96. package/scripts/sw-mission-control.sh +1 -1
  97. package/scripts/sw-model-router.sh +4 -1
  98. package/scripts/sw-otel.sh +1 -1
  99. package/scripts/sw-oversight.sh +1 -1
  100. package/scripts/sw-pipeline-composer.sh +3 -1
  101. package/scripts/sw-pipeline-vitals.sh +4 -6
  102. package/scripts/sw-pipeline.sh +4 -1
  103. package/scripts/sw-pm.sh +5 -2
  104. package/scripts/sw-pr-lifecycle.sh +1 -1
  105. package/scripts/sw-predictive.sh +4 -1
  106. package/scripts/sw-prep.sh +3 -2
  107. package/scripts/sw-ps.sh +1 -1
  108. package/scripts/sw-public-dashboard.sh +10 -4
  109. package/scripts/sw-quality.sh +1 -1
  110. package/scripts/sw-reaper.sh +1 -1
  111. package/scripts/sw-recruit.sh +16 -0
  112. package/scripts/sw-regression.sh +2 -1
  113. package/scripts/sw-release-manager.sh +1 -1
  114. package/scripts/sw-release.sh +7 -5
  115. package/scripts/sw-remote.sh +1 -1
  116. package/scripts/sw-replay.sh +1 -1
  117. package/scripts/sw-retro.sh +1 -1
  118. package/scripts/sw-scale.sh +4 -1
  119. package/scripts/sw-security-audit.sh +1 -1
  120. package/scripts/sw-self-optimize.sh +15 -1
  121. package/scripts/sw-session.sh +1 -1
  122. package/scripts/sw-setup.sh +1 -1
  123. package/scripts/sw-standup.sh +2 -1
  124. package/scripts/sw-status.sh +1 -1
  125. package/scripts/sw-strategic.sh +2 -1
  126. package/scripts/sw-stream.sh +1 -1
  127. package/scripts/sw-swarm.sh +6 -1
  128. package/scripts/sw-team-stages.sh +1 -1
  129. package/scripts/sw-templates.sh +1 -1
  130. package/scripts/sw-testgen.sh +3 -2
  131. package/scripts/sw-tmux-pipeline.sh +2 -1
  132. package/scripts/sw-tmux.sh +1 -1
  133. package/scripts/sw-trace.sh +1 -1
  134. package/scripts/sw-tracker-jira.sh +1 -0
  135. package/scripts/sw-tracker-linear.sh +1 -0
  136. package/scripts/sw-tracker.sh +1 -1
  137. package/scripts/sw-triage.sh +1 -1
  138. package/scripts/sw-upgrade.sh +1 -1
  139. package/scripts/sw-ux.sh +1 -1
  140. package/scripts/sw-webhook.sh +1 -1
  141. package/scripts/sw-widgets.sh +2 -2
  142. package/scripts/sw-worktree.sh +1 -1
  143. package/dashboard/public/app.js +0 -4422
@@ -0,0 +1,413 @@
1
+ // Modal system for intervention, machine management, and join links
2
+
3
+ import * as api from "../core/api";
4
+ import { store } from "../core/state";
5
+ import { escapeHtml } from "../core/helpers";
6
+
7
+ let interventionTarget: number | null = null;
8
+ let removeMachineTarget: string | null = null;
9
+ let workerUpdateTimer: ReturnType<typeof setTimeout> | null = null;
10
+
11
+ // Intervention Modal
12
+ export function setupInterventionModal(): void {
13
+ const modal = document.getElementById("intervention-modal");
14
+ const closeBtn = document.getElementById("modal-close");
15
+ const cancelBtn = document.getElementById("modal-cancel");
16
+ const sendBtn = document.getElementById("modal-send");
17
+ const msgEl = document.getElementById(
18
+ "modal-message",
19
+ ) as HTMLTextAreaElement | null;
20
+
21
+ function closeModal() {
22
+ if (modal) modal.style.display = "none";
23
+ interventionTarget = null;
24
+ }
25
+
26
+ if (closeBtn) closeBtn.addEventListener("click", closeModal);
27
+ if (cancelBtn) cancelBtn.addEventListener("click", closeModal);
28
+ if (modal)
29
+ modal.addEventListener("click", (e) => {
30
+ if (e.target === modal) closeModal();
31
+ });
32
+
33
+ if (sendBtn) {
34
+ sendBtn.addEventListener("click", () => {
35
+ if (interventionTarget && msgEl?.value.trim()) {
36
+ api.sendIntervention(interventionTarget, "message", {
37
+ message: msgEl.value.trim(),
38
+ });
39
+ closeModal();
40
+ }
41
+ });
42
+ }
43
+ }
44
+
45
+ export function openInterventionModal(issue: number): void {
46
+ interventionTarget = issue;
47
+ const modal = document.getElementById("intervention-modal");
48
+ const title = document.getElementById("modal-title");
49
+ const msg = document.getElementById(
50
+ "modal-message",
51
+ ) as HTMLTextAreaElement | null;
52
+ if (modal) modal.style.display = "";
53
+ if (title) title.textContent = "Send Message to #" + issue;
54
+ if (msg) msg.value = "";
55
+ }
56
+
57
+ export function confirmAbort(issue: number): void {
58
+ if (
59
+ confirm("Abort pipeline for issue #" + issue + "? This cannot be undone.")
60
+ ) {
61
+ api.sendIntervention(issue, "abort");
62
+ }
63
+ }
64
+
65
+ // Bulk Actions
66
+ export function setupBulkActions(): void {
67
+ const pauseBtn = document.getElementById("bulk-pause");
68
+ const resumeBtn = document.getElementById("bulk-resume");
69
+ const abortBtn = document.getElementById("bulk-abort");
70
+
71
+ if (pauseBtn) {
72
+ pauseBtn.addEventListener("click", () => {
73
+ const issues = Object.keys(store.get("selectedIssues"));
74
+ issues.forEach((i) => api.sendIntervention(i, "pause"));
75
+ });
76
+ }
77
+
78
+ if (resumeBtn) {
79
+ resumeBtn.addEventListener("click", () => {
80
+ const issues = Object.keys(store.get("selectedIssues"));
81
+ issues.forEach((i) => api.sendIntervention(i, "resume"));
82
+ });
83
+ }
84
+
85
+ if (abortBtn) {
86
+ abortBtn.addEventListener("click", () => {
87
+ const issues = Object.keys(store.get("selectedIssues"));
88
+ if (issues.length === 0) return;
89
+ if (
90
+ confirm(
91
+ "Abort " + issues.length + " pipeline(s)? This cannot be undone.",
92
+ )
93
+ ) {
94
+ issues.forEach((i) => api.sendIntervention(i, "abort"));
95
+ store.set("selectedIssues", {});
96
+ updateBulkToolbar();
97
+ }
98
+ });
99
+ }
100
+ }
101
+
102
+ export function updateBulkToolbar(): void {
103
+ const toolbar = document.getElementById("bulk-actions");
104
+ if (!toolbar) return;
105
+ const count = Object.keys(store.get("selectedIssues")).length;
106
+ if (count === 0) {
107
+ toolbar.style.display = "none";
108
+ return;
109
+ }
110
+ toolbar.style.display = "";
111
+ const countEl = document.getElementById("bulk-count");
112
+ if (countEl) countEl.textContent = count + " selected";
113
+ }
114
+
115
+ // Machine Modals
116
+ export function setupMachinesModals(): void {
117
+ const addBtn = document.getElementById("btn-add-machine");
118
+ if (addBtn) addBtn.addEventListener("click", openAddMachineModal);
119
+
120
+ const joinBtn = document.getElementById("btn-join-link");
121
+ if (joinBtn) joinBtn.addEventListener("click", openJoinLinkModal);
122
+
123
+ // Add machine modal
124
+ bindClick("machine-modal-close", closeAddMachineModal);
125
+ bindClick("machine-modal-cancel", closeAddMachineModal);
126
+ bindClick("machine-modal-submit", submitAddMachine);
127
+
128
+ // Join link modal
129
+ bindClick("join-modal-close", closeJoinLinkModal);
130
+ bindClick("join-modal-cancel", closeJoinLinkModal);
131
+ bindClick("join-modal-generate", generateJoinLink);
132
+ bindClick("join-copy-btn", copyJoinCommand);
133
+
134
+ // Remove machine modal
135
+ bindClick("remove-modal-close", () => {
136
+ const el = document.getElementById("remove-machine-modal");
137
+ if (el) el.style.display = "none";
138
+ });
139
+ bindClick("remove-modal-cancel", () => {
140
+ const el = document.getElementById("remove-machine-modal");
141
+ if (el) el.style.display = "none";
142
+ });
143
+ bindClick("remove-modal-confirm", executeRemoveMachine);
144
+ }
145
+
146
+ function bindClick(id: string, handler: () => void): void {
147
+ const el = document.getElementById(id);
148
+ if (el) el.addEventListener("click", handler);
149
+ }
150
+
151
+ function openAddMachineModal(): void {
152
+ const modal = document.getElementById("add-machine-modal");
153
+ if (modal) modal.style.display = "flex";
154
+ setVal("machine-name", "");
155
+ setVal("machine-host", "");
156
+ setVal("machine-ssh-user", "");
157
+ setVal("machine-path", "");
158
+ setVal("machine-workers", "4");
159
+ setVal("machine-role", "worker");
160
+ const err = document.getElementById("machine-modal-error");
161
+ if (err) err.style.display = "none";
162
+ }
163
+
164
+ function closeAddMachineModal(): void {
165
+ const modal = document.getElementById("add-machine-modal");
166
+ if (modal) modal.style.display = "none";
167
+ }
168
+
169
+ function submitAddMachine(): void {
170
+ const name = getVal("machine-name").trim();
171
+ const host = getVal("machine-host").trim();
172
+ const sshUser = getVal("machine-ssh-user").trim();
173
+ const swPath = getVal("machine-path").trim();
174
+ const maxWorkers = parseInt(getVal("machine-workers"), 10) || 4;
175
+ const role = getVal("machine-role");
176
+ const errEl = document.getElementById("machine-modal-error");
177
+
178
+ if (!name || !host) {
179
+ if (errEl) {
180
+ errEl.textContent = "Name and host are required";
181
+ errEl.style.display = "";
182
+ }
183
+ return;
184
+ }
185
+
186
+ const body: Record<string, unknown> = {
187
+ name,
188
+ host,
189
+ role,
190
+ max_workers: maxWorkers,
191
+ };
192
+ if (sshUser) body.ssh_user = sshUser;
193
+ if (swPath) body.shipwright_path = swPath;
194
+
195
+ api
196
+ .addMachine(body)
197
+ .then(() => {
198
+ closeAddMachineModal();
199
+ refreshMachines();
200
+ })
201
+ .catch((err) => {
202
+ if (errEl) {
203
+ errEl.textContent = err.message || "Failed to register machine";
204
+ errEl.style.display = "";
205
+ }
206
+ });
207
+ }
208
+
209
+ export function updateWorkerCount(name: string, value: string): void {
210
+ if (workerUpdateTimer) clearTimeout(workerUpdateTimer);
211
+ workerUpdateTimer = setTimeout(() => {
212
+ api
213
+ .updateMachine(name, { max_workers: parseInt(value, 10) })
214
+ .then((updated) => {
215
+ const card = document.getElementById("machine-card-" + name);
216
+ if (card) {
217
+ const countEl = card.querySelector(".workers-count");
218
+ if (countEl)
219
+ countEl.textContent =
220
+ (updated.active_workers || 0) +
221
+ " / " +
222
+ (updated.max_workers || value);
223
+ }
224
+ })
225
+ .catch((err) => console.error("Worker update failed:", err));
226
+ }, 500);
227
+ }
228
+
229
+ export function machineHealthCheckAction(name: string): void {
230
+ const card = document.getElementById("machine-card-" + name);
231
+ if (card) {
232
+ const checkBtn = card.querySelector(
233
+ ".machine-action-btn",
234
+ ) as HTMLButtonElement;
235
+ if (checkBtn) {
236
+ checkBtn.textContent = "Checking\u2026";
237
+ checkBtn.disabled = true;
238
+ }
239
+ }
240
+
241
+ api
242
+ .machineHealthCheck(name)
243
+ .then((result) => {
244
+ if (result.machine && card) {
245
+ const m = result.machine;
246
+ const health = m.health || {};
247
+ const healthRows = card.querySelectorAll(".machine-health-row");
248
+ if (healthRows.length >= 3) {
249
+ const statusEl = healthRows[0].querySelector(".health-status");
250
+ if (statusEl) {
251
+ statusEl.className =
252
+ "health-status " +
253
+ (health.daemon_running ? "running" : "stopped");
254
+ statusEl.textContent = health.daemon_running
255
+ ? "Running"
256
+ : "Stopped";
257
+ }
258
+ const hbEl = healthRows[1].querySelector(".health-value");
259
+ if (hbEl) hbEl.textContent = String(health.heartbeat_count || 0);
260
+ const lastEl = healthRows[2].querySelector(".health-value");
261
+ if (lastEl)
262
+ lastEl.textContent = formatHbAge(health.last_heartbeat_s_ago);
263
+ }
264
+ const dot = card.querySelector(".presence-dot");
265
+ if (dot) dot.className = "presence-dot " + (m.status || "offline");
266
+ }
267
+ resetCheckBtn(card);
268
+ })
269
+ .catch((err) => {
270
+ console.error("Health check failed:", err);
271
+ resetCheckBtn(card);
272
+ });
273
+ }
274
+
275
+ function resetCheckBtn(card: HTMLElement | null): void {
276
+ if (card) {
277
+ const btn = card.querySelector(".machine-action-btn") as HTMLButtonElement;
278
+ if (btn) {
279
+ btn.textContent = "Check";
280
+ btn.disabled = false;
281
+ }
282
+ }
283
+ }
284
+
285
+ function formatHbAge(age: number | undefined): string {
286
+ if (typeof age !== "number" || age >= 9999) return "\u2014";
287
+ if (age < 60) return age + "s ago";
288
+ if (age < 3600) return Math.floor(age / 60) + "m ago";
289
+ return Math.floor(age / 3600) + "h ago";
290
+ }
291
+
292
+ export function confirmMachineRemove(name: string): void {
293
+ removeMachineTarget = name;
294
+ const el = document.getElementById("remove-machine-name");
295
+ if (el) el.textContent = name;
296
+ const cb = document.getElementById("remove-stop-daemon") as HTMLInputElement;
297
+ if (cb) cb.checked = false;
298
+ const modal = document.getElementById("remove-machine-modal");
299
+ if (modal) modal.style.display = "flex";
300
+ }
301
+
302
+ function executeRemoveMachine(): void {
303
+ if (!removeMachineTarget) return;
304
+ api
305
+ .removeMachine(removeMachineTarget)
306
+ .then(() => {
307
+ const modal = document.getElementById("remove-machine-modal");
308
+ if (modal) modal.style.display = "none";
309
+ removeMachineTarget = null;
310
+ refreshMachines();
311
+ })
312
+ .catch((err) => {
313
+ console.error("Remove machine failed:", err);
314
+ const modal = document.getElementById("remove-machine-modal");
315
+ if (modal) modal.style.display = "none";
316
+ removeMachineTarget = null;
317
+ });
318
+ }
319
+
320
+ function openJoinLinkModal(): void {
321
+ const modal = document.getElementById("join-link-modal");
322
+ if (modal) modal.style.display = "flex";
323
+ setVal("join-label", "");
324
+ setVal("join-workers", "4");
325
+ const cmdDisplay = document.getElementById("join-command-display");
326
+ if (cmdDisplay) cmdDisplay.style.display = "none";
327
+ const cmdText = document.getElementById("join-command-text");
328
+ if (cmdText) cmdText.textContent = "";
329
+ }
330
+
331
+ function closeJoinLinkModal(): void {
332
+ const modal = document.getElementById("join-link-modal");
333
+ if (modal) modal.style.display = "none";
334
+ }
335
+
336
+ function generateJoinLink(): void {
337
+ const label = getVal("join-label").trim();
338
+ const maxWorkers = parseInt(getVal("join-workers"), 10) || 4;
339
+ const btn = document.getElementById(
340
+ "join-modal-generate",
341
+ ) as HTMLButtonElement;
342
+ if (btn) {
343
+ btn.textContent = "Generating\u2026";
344
+ btn.disabled = true;
345
+ }
346
+
347
+ api
348
+ .generateJoinToken({ label, max_workers: maxWorkers })
349
+ .then((data) => {
350
+ const cmdText = document.getElementById("join-command-text");
351
+ if (cmdText) cmdText.textContent = data.join_cmd || "";
352
+ const cmdDisplay = document.getElementById("join-command-display");
353
+ if (cmdDisplay) cmdDisplay.style.display = "";
354
+ if (btn) {
355
+ btn.textContent = "Generate";
356
+ btn.disabled = false;
357
+ }
358
+ refreshJoinTokens();
359
+ })
360
+ .catch((err) => {
361
+ console.error("Generate join link failed:", err);
362
+ if (btn) {
363
+ btn.textContent = "Generate";
364
+ btn.disabled = false;
365
+ }
366
+ });
367
+ }
368
+
369
+ function copyJoinCommand(): void {
370
+ const text = document.getElementById("join-command-text")?.textContent;
371
+ if (text && navigator.clipboard) {
372
+ navigator.clipboard.writeText(text).then(() => {
373
+ const btn = document.getElementById("join-copy-btn");
374
+ if (btn) {
375
+ btn.textContent = "Copied!";
376
+ setTimeout(() => {
377
+ btn.textContent = "Copy";
378
+ }, 2000);
379
+ }
380
+ });
381
+ }
382
+ }
383
+
384
+ function getVal(id: string): string {
385
+ return (document.getElementById(id) as HTMLInputElement)?.value || "";
386
+ }
387
+
388
+ function setVal(id: string, value: string): void {
389
+ const el = document.getElementById(id) as HTMLInputElement;
390
+ if (el) el.value = value;
391
+ }
392
+
393
+ function refreshMachines(): void {
394
+ const tab = store.get("activeTab");
395
+ if (tab === "machines") {
396
+ api.fetchMachines().then((data) => {
397
+ const machines = Array.isArray(data)
398
+ ? data
399
+ : ((data as any).machines ?? []);
400
+ store.set("machinesCache", machines);
401
+ });
402
+ api
403
+ .fetchJoinTokens()
404
+ .then(({ tokens }) => store.set("joinTokensCache", tokens));
405
+ }
406
+ }
407
+
408
+ function refreshJoinTokens(): void {
409
+ api
410
+ .fetchJoinTokens()
411
+ .then(({ tokens }) => store.set("joinTokensCache", tokens))
412
+ .catch(() => {});
413
+ }
@@ -0,0 +1,144 @@
1
+ // ANSI terminal renderer for log streaming and live output
2
+
3
+ import { escapeHtml } from "../core/helpers";
4
+
5
+ const ANSI_COLORS: Record<string, string> = {
6
+ "30": "#060a14",
7
+ "31": "#f43f5e",
8
+ "32": "#4ade80",
9
+ "33": "#fbbf24",
10
+ "34": "#0066ff",
11
+ "35": "#7c3aed",
12
+ "36": "#00d4ff",
13
+ "37": "#e8ecf4",
14
+ "90": "#5a6d8a",
15
+ "91": "#f43f5e",
16
+ "92": "#4ade80",
17
+ "93": "#fbbf24",
18
+ "94": "#0066ff",
19
+ "95": "#7c3aed",
20
+ "96": "#00d4ff",
21
+ "97": "#e8ecf4",
22
+ };
23
+
24
+ export function renderAnsiToHtml(text: string): string {
25
+ let result = "";
26
+ let openSpans = 0;
27
+ let i = 0;
28
+
29
+ while (i < text.length) {
30
+ if (text[i] === "\x1b" && text[i + 1] === "[") {
31
+ const end = text.indexOf("m", i + 2);
32
+ if (end !== -1) {
33
+ const codes = text.substring(i + 2, end).split(";");
34
+ for (const code of codes) {
35
+ if (code === "0" || code === "") {
36
+ while (openSpans > 0) {
37
+ result += "</span>";
38
+ openSpans--;
39
+ }
40
+ } else if (code === "1") {
41
+ result += '<span style="font-weight:bold">';
42
+ openSpans++;
43
+ } else if (code === "2") {
44
+ result += '<span style="opacity:0.7">';
45
+ openSpans++;
46
+ } else if (code === "3") {
47
+ result += '<span style="font-style:italic">';
48
+ openSpans++;
49
+ } else if (code === "4") {
50
+ result += '<span style="text-decoration:underline">';
51
+ openSpans++;
52
+ } else if (ANSI_COLORS[code]) {
53
+ result += `<span style="color:${ANSI_COLORS[code]}">`;
54
+ openSpans++;
55
+ }
56
+ }
57
+ i = end + 1;
58
+ continue;
59
+ }
60
+ }
61
+ result += escapeHtml(text[i]);
62
+ i++;
63
+ }
64
+ while (openSpans > 0) {
65
+ result += "</span>";
66
+ openSpans--;
67
+ }
68
+ return result;
69
+ }
70
+
71
+ export function stripAnsi(text: string): string {
72
+ return text.replace(/\x1b\[[0-9;]*m/g, "");
73
+ }
74
+
75
+ export function renderLogViewer(content: string): string {
76
+ if (!content)
77
+ return '<div class="empty-state"><p>No logs available</p></div>';
78
+ const clean = stripAnsi(content);
79
+ const lines = clean.split("\n");
80
+ let html = '<div class="log-viewer">';
81
+ for (let i = 0; i < lines.length; i++) {
82
+ const lineNum = i + 1;
83
+ const lower = lines[i].toLowerCase();
84
+ const lineClass =
85
+ lower.indexOf("error") !== -1 || lower.indexOf("fail") !== -1
86
+ ? " log-line-error"
87
+ : "";
88
+ html +=
89
+ `<div class="log-line${lineClass}">` +
90
+ `<span class="log-line-num">${lineNum}</span>` +
91
+ `<span class="log-line-text">${escapeHtml(lines[i])}</span></div>`;
92
+ }
93
+ html += "</div>";
94
+ return html;
95
+ }
96
+
97
+ export class LiveTerminal {
98
+ private container: HTMLElement;
99
+ private lines: string[] = [];
100
+ private autoScroll = true;
101
+ private maxLines = 5000;
102
+
103
+ constructor(container: HTMLElement) {
104
+ this.container = container;
105
+ this.container.classList.add("live-terminal");
106
+ }
107
+
108
+ append(text: string): void {
109
+ const newLines = text.split("\n");
110
+ this.lines.push(...newLines);
111
+ if (this.lines.length > this.maxLines) {
112
+ this.lines = this.lines.slice(-this.maxLines);
113
+ }
114
+ this.render();
115
+ }
116
+
117
+ clear(): void {
118
+ this.lines = [];
119
+ this.render();
120
+ }
121
+
122
+ private render(): void {
123
+ const html = this.lines
124
+ .map((line, i) => {
125
+ const rendered = renderAnsiToHtml(line);
126
+ return `<div class="terminal-line"><span class="terminal-line-num">${i + 1}</span><span class="terminal-line-text">${rendered}</span></div>`;
127
+ })
128
+ .join("");
129
+ this.container.innerHTML = html;
130
+
131
+ if (this.autoScroll) {
132
+ this.container.scrollTop = this.container.scrollHeight;
133
+ }
134
+ }
135
+
136
+ setAutoScroll(enabled: boolean): void {
137
+ this.autoScroll = enabled;
138
+ }
139
+
140
+ destroy(): void {
141
+ this.container.innerHTML = "";
142
+ this.lines = [];
143
+ }
144
+ }