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,362 @@
1
+ // Team tab - connected developers grid + team activity
2
+
3
+ import { store } from "../core/state";
4
+ import { escapeHtml, timeAgo } from "../core/helpers";
5
+ import { icon } from "../design/icons";
6
+ import * as api from "../core/api";
7
+ import type {
8
+ FleetState,
9
+ View,
10
+ TeamData,
11
+ TeamDeveloper,
12
+ TeamActivityEvent,
13
+ } from "../types/api";
14
+
15
+ let teamRefreshTimer: ReturnType<typeof setInterval> | null = null;
16
+
17
+ function fetchTeamData(): void {
18
+ api
19
+ .fetchTeam()
20
+ .then((data) => {
21
+ store.set("teamCache", data);
22
+ renderTeamGrid(data);
23
+ renderTeamStats(data);
24
+ })
25
+ .catch(() => {});
26
+
27
+ api
28
+ .fetchTeamActivity()
29
+ .then((events) => {
30
+ store.set("teamActivityCache", events);
31
+ renderTeamActivity(events);
32
+ })
33
+ .catch(() => {});
34
+ }
35
+
36
+ function renderTeamStats(data: TeamData): void {
37
+ const el1 = document.getElementById("team-stat-online");
38
+ if (el1) el1.textContent = String(data.total_online || 0);
39
+ const el2 = document.getElementById("team-stat-pipelines");
40
+ if (el2) el2.textContent = String(data.total_active_pipelines || 0);
41
+ const el3 = document.getElementById("team-stat-queued");
42
+ if (el3) el3.textContent = String(data.total_queued || 0);
43
+ }
44
+
45
+ function renderTeamGrid(data: TeamData): void {
46
+ const grid = document.getElementById("team-grid");
47
+ if (!grid) return;
48
+ const devs = data.developers || [];
49
+ if (devs.length === 0) {
50
+ grid.innerHTML = `<div class="empty-state">${icon("users-round", 32)}<p>No developers connected. Run <code>shipwright connect start</code> to join.</p></div>`;
51
+ return;
52
+ }
53
+
54
+ grid.innerHTML = devs
55
+ .map((dev) => {
56
+ const presence = dev._presence || "offline";
57
+ const initials = (dev.developer_id || "?").substring(0, 2).toUpperCase();
58
+ const pipelines = (dev.active_jobs || [])
59
+ .map(
60
+ (job) =>
61
+ `<div class="team-card-pipeline-item">` +
62
+ `<span class="team-card-pipeline-issue">#${escapeHtml(String(job.issue))}</span>` +
63
+ `<span class="team-card-pipeline-stage">${escapeHtml(job.stage || "\u2014")}</span></div>`,
64
+ )
65
+ .join("");
66
+ const pipelineSection = pipelines
67
+ ? `<div class="team-card-pipelines">${pipelines}</div>`
68
+ : "";
69
+
70
+ return (
71
+ `<div class="team-card"><div class="team-card-header">` +
72
+ `<div class="team-card-avatar">${escapeHtml(initials)}</div>` +
73
+ `<div class="team-card-info"><div class="team-card-name">${escapeHtml(dev.developer_id)}</div>` +
74
+ `<div class="team-card-machine">${escapeHtml(dev.machine_name)}</div></div>` +
75
+ `<div class="presence-dot ${presence}" title="${presence}"></div></div>` +
76
+ `<div class="team-card-body">` +
77
+ `<div class="team-card-row"><span class="team-card-row-label">Daemon</span>` +
78
+ `<span class="team-card-row-value">${dev.daemon_running ? "\u25cf Running" : "\u25cb Stopped"}</span></div>` +
79
+ `<div class="team-card-row"><span class="team-card-row-label">Active</span>` +
80
+ `<span class="team-card-row-value">${(dev.active_jobs || []).length} pipelines</span></div>` +
81
+ `<div class="team-card-row"><span class="team-card-row-label">Queued</span>` +
82
+ `<span class="team-card-row-value">${(dev.queued || []).length} issues</span></div>` +
83
+ pipelineSection +
84
+ "</div></div>"
85
+ );
86
+ })
87
+ .join("");
88
+ }
89
+
90
+ function renderTeamActivity(events: TeamActivityEvent[]): void {
91
+ const container = document.getElementById("team-activity");
92
+ if (!container) return;
93
+ const items = Array.isArray(events) ? events : [];
94
+ if (items.length === 0) {
95
+ container.innerHTML =
96
+ '<div class="empty-state">No team activity yet.</div>';
97
+ return;
98
+ }
99
+
100
+ container.innerHTML = items
101
+ .slice(0, 50)
102
+ .map((evt) => {
103
+ const isCI = evt.from_developer === "github-actions";
104
+ const badgeClass = isCI ? "ci" : "local";
105
+ const badgeText = isCI ? "CI" : evt.from_developer || "local";
106
+ const text = formatTeamEvent(evt);
107
+ const time = evt.ts ? timeAgo(new Date(evt.ts)) : "";
108
+ return (
109
+ `<div class="team-activity-item"><span class="source-badge ${badgeClass}">${escapeHtml(badgeText)}</span>` +
110
+ `<div class="team-activity-content"><div class="team-activity-text">${text}</div>` +
111
+ `<div class="team-activity-time">${time}</div></div></div>`
112
+ );
113
+ })
114
+ .join("");
115
+ }
116
+
117
+ function formatTeamEvent(evt: TeamActivityEvent): string {
118
+ const type = evt.type || "";
119
+ const issue = evt.issue ? " #" + evt.issue : "";
120
+ if (type.indexOf("pipeline.started") !== -1)
121
+ return "Pipeline started" + issue;
122
+ if (
123
+ type.indexOf("pipeline.completed") !== -1 ||
124
+ type.indexOf("pipeline_completed") !== -1
125
+ ) {
126
+ const result = evt.result === "success" ? "\u2713" : "\u2717";
127
+ return "Pipeline " + result + issue;
128
+ }
129
+ if (type.indexOf("stage.") !== -1) {
130
+ const stage = evt.stage || type.split(".").pop() || "";
131
+ return "Stage " + escapeHtml(stage) + issue;
132
+ }
133
+ if (type.indexOf("daemon.") !== -1)
134
+ return type.replace("daemon.", "Daemon: ");
135
+ if (type.indexOf("ci.") !== -1) return type.replace("ci.", "CI: ") + issue;
136
+ return escapeHtml(type) + issue;
137
+ }
138
+
139
+ function setupInviteButton(): void {
140
+ const btn = document.getElementById("btn-create-invite");
141
+ if (!btn) return;
142
+ btn.addEventListener("click", () => {
143
+ (btn as HTMLButtonElement).disabled = true;
144
+ btn.textContent = "Creating...";
145
+ api
146
+ .createTeamInvite({ expires_hours: 72 })
147
+ .then((data) => {
148
+ const result = document.getElementById("team-invite-result");
149
+ if (result) {
150
+ result.style.display = "";
151
+ result.innerHTML =
152
+ `<div class="invite-link-box">` +
153
+ `<span class="invite-label">Invite link (expires in 72h):</span>` +
154
+ `<code class="invite-url" id="invite-url">${escapeHtml(data.url || data.token)}</code>` +
155
+ `<button class="btn-sm" id="copy-invite">Copy</button></div>`;
156
+ const copyBtn = document.getElementById("copy-invite");
157
+ if (copyBtn) {
158
+ copyBtn.addEventListener("click", () => {
159
+ const url = data.url || data.token;
160
+ navigator.clipboard.writeText(url).then(() => {
161
+ copyBtn.textContent = "Copied!";
162
+ setTimeout(() => {
163
+ copyBtn.textContent = "Copy";
164
+ }, 2000);
165
+ });
166
+ });
167
+ }
168
+ }
169
+ })
170
+ .catch(() => {
171
+ const result = document.getElementById("team-invite-result");
172
+ if (result) {
173
+ result.style.display = "";
174
+ result.innerHTML =
175
+ '<div class="invite-error">Failed to create invite. Check server logs.</div>';
176
+ }
177
+ })
178
+ .finally(() => {
179
+ (btn as HTMLButtonElement).disabled = false;
180
+ btn.textContent = "Create Invite Link";
181
+ });
182
+ });
183
+ }
184
+
185
+ function loadIntegrationsStatus(): void {
186
+ const container = document.getElementById("integrations-status");
187
+ if (!container) return;
188
+
189
+ api
190
+ .fetchLinearStatus()
191
+ .then((data) => {
192
+ let html = '<div class="integrations-grid">';
193
+ const connected = data.connected || data.configured || false;
194
+ const statusCls = connected
195
+ ? "integration-active"
196
+ : "integration-inactive";
197
+ const statusText = connected ? "Connected" : "Not configured";
198
+ html +=
199
+ `<div class="integration-card ${statusCls}">` +
200
+ `<div class="integration-name">${icon("git-branch", 18)} Linear</div>` +
201
+ `<div class="integration-status">${statusText}</div>`;
202
+ if (data.workspace)
203
+ html += `<div class="integration-detail">Workspace: ${escapeHtml(String(data.workspace))}</div>`;
204
+ if (data.team_id)
205
+ html += `<div class="integration-detail">Team: ${escapeHtml(String(data.team_id))}</div>`;
206
+ html += "</div>";
207
+
208
+ // GitHub (always available if dashboard is running)
209
+ html +=
210
+ `<div class="integration-card integration-active">` +
211
+ `<div class="integration-name">${icon("git-branch", 18)} GitHub</div>` +
212
+ `<div class="integration-status">Connected</div></div>`;
213
+
214
+ html += "</div>";
215
+ container.innerHTML = html;
216
+ })
217
+ .catch(() => {
218
+ container.innerHTML =
219
+ '<div class="empty-state"><p>Could not load status</p></div>';
220
+ });
221
+ }
222
+
223
+ function setupAdminDebug(): void {
224
+ const output = document.getElementById("admin-debug-output");
225
+ if (!output) return;
226
+
227
+ const renderJson = (data: unknown) => {
228
+ output.innerHTML = `<pre class="admin-debug-pre">${escapeHtml(JSON.stringify(data, null, 2))}</pre>`;
229
+ };
230
+
231
+ document.getElementById("btn-db-health")?.addEventListener("click", () => {
232
+ output.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
233
+ api
234
+ .fetchDbHealth()
235
+ .then(renderJson)
236
+ .catch((e) => {
237
+ output.innerHTML = `<div class="empty-state"><p>Error: ${escapeHtml(String(e))}</p></div>`;
238
+ });
239
+ });
240
+ document.getElementById("btn-db-events")?.addEventListener("click", () => {
241
+ output.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
242
+ api
243
+ .fetchDbEvents(0, 50)
244
+ .then(renderJson)
245
+ .catch((e) => {
246
+ output.innerHTML = `<div class="empty-state"><p>Error: ${escapeHtml(String(e))}</p></div>`;
247
+ });
248
+ });
249
+ document.getElementById("btn-db-jobs")?.addEventListener("click", () => {
250
+ output.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
251
+ api
252
+ .fetchDbJobs()
253
+ .then(renderJson)
254
+ .catch((e) => {
255
+ output.innerHTML = `<div class="empty-state"><p>Error: ${escapeHtml(String(e))}</p></div>`;
256
+ });
257
+ });
258
+ document
259
+ .getElementById("btn-db-heartbeats")
260
+ ?.addEventListener("click", () => {
261
+ output.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
262
+ api
263
+ .fetchDbHeartbeats()
264
+ .then(renderJson)
265
+ .catch((e) => {
266
+ output.innerHTML = `<div class="empty-state"><p>Error: ${escapeHtml(String(e))}</p></div>`;
267
+ });
268
+ });
269
+ document.getElementById("btn-db-costs")?.addEventListener("click", () => {
270
+ output.innerHTML = '<div class="empty-state"><p>Loading...</p></div>';
271
+ api
272
+ .fetchDbCostsToday()
273
+ .then(renderJson)
274
+ .catch((e) => {
275
+ output.innerHTML = `<div class="empty-state"><p>Error: ${escapeHtml(String(e))}</p></div>`;
276
+ });
277
+ });
278
+ }
279
+
280
+ function setupClaimPanel(): void {
281
+ const resultEl = document.getElementById("claim-result");
282
+ const issueInput = document.getElementById("claim-issue") as HTMLInputElement;
283
+ const machineInput = document.getElementById(
284
+ "claim-machine",
285
+ ) as HTMLInputElement;
286
+
287
+ document.getElementById("btn-claim")?.addEventListener("click", () => {
288
+ const issue = parseInt(issueInput?.value || "0");
289
+ const machine = machineInput?.value.trim();
290
+ if (!issue || !machine) {
291
+ if (resultEl)
292
+ resultEl.innerHTML =
293
+ '<span style="color:var(--rose)">Issue # and machine name required</span>';
294
+ return;
295
+ }
296
+ api
297
+ .claimIssue(issue, machine)
298
+ .then((data) => {
299
+ if (resultEl) {
300
+ if (data.approved) {
301
+ resultEl.innerHTML = `<span style="color:var(--green)">Claimed #${issue} for ${escapeHtml(machine)}</span>`;
302
+ } else {
303
+ resultEl.innerHTML = `<span style="color:var(--amber)">Already claimed by ${escapeHtml(data.claimed_by || "unknown")}</span>`;
304
+ }
305
+ }
306
+ })
307
+ .catch((e) => {
308
+ if (resultEl)
309
+ resultEl.innerHTML = `<span style="color:var(--rose)">Error: ${escapeHtml(String(e))}</span>`;
310
+ });
311
+ });
312
+
313
+ document.getElementById("btn-release")?.addEventListener("click", () => {
314
+ const issue = parseInt(issueInput?.value || "0");
315
+ const machine = machineInput?.value.trim();
316
+ if (!issue) {
317
+ if (resultEl)
318
+ resultEl.innerHTML =
319
+ '<span style="color:var(--rose)">Issue # required</span>';
320
+ return;
321
+ }
322
+ api
323
+ .releaseIssue(issue, machine || undefined)
324
+ .then(() => {
325
+ if (resultEl)
326
+ resultEl.innerHTML = `<span style="color:var(--green)">Released claim on #${issue}</span>`;
327
+ })
328
+ .catch((e) => {
329
+ if (resultEl)
330
+ resultEl.innerHTML = `<span style="color:var(--rose)">Error: ${escapeHtml(String(e))}</span>`;
331
+ });
332
+ });
333
+ }
334
+
335
+ export const teamView: View = {
336
+ init() {
337
+ fetchTeamData();
338
+ setupInviteButton();
339
+ loadIntegrationsStatus();
340
+ setupAdminDebug();
341
+ setupClaimPanel();
342
+ teamRefreshTimer = setInterval(fetchTeamData, 10000);
343
+ },
344
+ render(data: FleetState) {
345
+ if (data.team) {
346
+ renderTeamGrid(data.team);
347
+ renderTeamStats(data.team);
348
+ } else {
349
+ const cache = store.get("teamCache");
350
+ if (cache) {
351
+ renderTeamGrid(cache);
352
+ renderTeamStats(cache);
353
+ }
354
+ }
355
+ },
356
+ destroy() {
357
+ if (teamRefreshTimer) {
358
+ clearInterval(teamRefreshTimer);
359
+ teamRefreshTimer = null;
360
+ }
361
+ },
362
+ };