shipwright-cli 2.0.0 → 2.1.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 (113) hide show
  1. package/README.md +160 -72
  2. package/completions/_shipwright +59 -7
  3. package/completions/shipwright.bash +24 -4
  4. package/completions/shipwright.fish +80 -2
  5. package/dashboard/server.ts +208 -0
  6. package/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
  7. package/docs/tmux-research/TMUX-AUDIT.md +925 -0
  8. package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
  9. package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
  10. package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
  11. package/package.json +2 -2
  12. package/scripts/lib/helpers.sh +7 -0
  13. package/scripts/sw +116 -2
  14. package/scripts/sw-activity.sh +1 -1
  15. package/scripts/sw-adaptive.sh +1 -1
  16. package/scripts/sw-adversarial.sh +1 -1
  17. package/scripts/sw-architecture-enforcer.sh +1 -1
  18. package/scripts/sw-auth.sh +1 -1
  19. package/scripts/sw-autonomous.sh +128 -38
  20. package/scripts/sw-changelog.sh +1 -1
  21. package/scripts/sw-checkpoint.sh +1 -1
  22. package/scripts/sw-ci.sh +1 -1
  23. package/scripts/sw-cleanup.sh +1 -1
  24. package/scripts/sw-code-review.sh +62 -1
  25. package/scripts/sw-connect.sh +1 -1
  26. package/scripts/sw-context.sh +1 -1
  27. package/scripts/sw-cost.sh +44 -3
  28. package/scripts/sw-daemon.sh +155 -27
  29. package/scripts/sw-dashboard.sh +1 -1
  30. package/scripts/sw-db.sh +958 -118
  31. package/scripts/sw-decompose.sh +1 -1
  32. package/scripts/sw-deps.sh +1 -1
  33. package/scripts/sw-developer-simulation.sh +1 -1
  34. package/scripts/sw-discovery.sh +1 -1
  35. package/scripts/sw-docs-agent.sh +1 -1
  36. package/scripts/sw-docs.sh +1 -1
  37. package/scripts/sw-doctor.sh +49 -1
  38. package/scripts/sw-dora.sh +1 -1
  39. package/scripts/sw-durable.sh +1 -1
  40. package/scripts/sw-e2e-orchestrator.sh +1 -1
  41. package/scripts/sw-eventbus.sh +1 -1
  42. package/scripts/sw-feedback.sh +23 -15
  43. package/scripts/sw-fix.sh +1 -1
  44. package/scripts/sw-fleet-discover.sh +1 -1
  45. package/scripts/sw-fleet-viz.sh +1 -1
  46. package/scripts/sw-fleet.sh +1 -1
  47. package/scripts/sw-github-app.sh +1 -1
  48. package/scripts/sw-github-checks.sh +4 -4
  49. package/scripts/sw-github-deploy.sh +1 -1
  50. package/scripts/sw-github-graphql.sh +1 -1
  51. package/scripts/sw-guild.sh +1 -1
  52. package/scripts/sw-heartbeat.sh +1 -1
  53. package/scripts/sw-hygiene.sh +1 -1
  54. package/scripts/sw-incident.sh +45 -6
  55. package/scripts/sw-init.sh +150 -24
  56. package/scripts/sw-instrument.sh +1 -1
  57. package/scripts/sw-intelligence.sh +1 -1
  58. package/scripts/sw-jira.sh +1 -1
  59. package/scripts/sw-launchd.sh +1 -1
  60. package/scripts/sw-linear.sh +1 -1
  61. package/scripts/sw-logs.sh +1 -1
  62. package/scripts/sw-loop.sh +204 -19
  63. package/scripts/sw-memory.sh +18 -1
  64. package/scripts/sw-mission-control.sh +1 -1
  65. package/scripts/sw-model-router.sh +1 -1
  66. package/scripts/sw-otel.sh +1 -1
  67. package/scripts/sw-oversight.sh +76 -1
  68. package/scripts/sw-pipeline-composer.sh +1 -1
  69. package/scripts/sw-pipeline-vitals.sh +1 -1
  70. package/scripts/sw-pipeline.sh +261 -12
  71. package/scripts/sw-pm.sh +70 -5
  72. package/scripts/sw-pr-lifecycle.sh +1 -1
  73. package/scripts/sw-predictive.sh +8 -1
  74. package/scripts/sw-prep.sh +1 -1
  75. package/scripts/sw-ps.sh +1 -1
  76. package/scripts/sw-public-dashboard.sh +1 -1
  77. package/scripts/sw-quality.sh +1 -1
  78. package/scripts/sw-reaper.sh +1 -1
  79. package/scripts/sw-recruit.sh +1853 -178
  80. package/scripts/sw-regression.sh +1 -1
  81. package/scripts/sw-release-manager.sh +1 -1
  82. package/scripts/sw-release.sh +1 -1
  83. package/scripts/sw-remote.sh +1 -1
  84. package/scripts/sw-replay.sh +1 -1
  85. package/scripts/sw-retro.sh +1 -1
  86. package/scripts/sw-scale.sh +1 -1
  87. package/scripts/sw-security-audit.sh +1 -1
  88. package/scripts/sw-self-optimize.sh +1 -1
  89. package/scripts/sw-session.sh +1 -1
  90. package/scripts/sw-setup.sh +263 -127
  91. package/scripts/sw-standup.sh +1 -1
  92. package/scripts/sw-status.sh +44 -2
  93. package/scripts/sw-strategic.sh +189 -41
  94. package/scripts/sw-stream.sh +1 -1
  95. package/scripts/sw-swarm.sh +42 -5
  96. package/scripts/sw-team-stages.sh +1 -1
  97. package/scripts/sw-templates.sh +4 -4
  98. package/scripts/sw-testgen.sh +66 -15
  99. package/scripts/sw-tmux-pipeline.sh +1 -1
  100. package/scripts/sw-tmux-role-color.sh +58 -0
  101. package/scripts/sw-tmux-status.sh +128 -0
  102. package/scripts/sw-tmux.sh +1 -1
  103. package/scripts/sw-trace.sh +1 -1
  104. package/scripts/sw-tracker.sh +1 -1
  105. package/scripts/sw-triage.sh +61 -37
  106. package/scripts/sw-upgrade.sh +1 -1
  107. package/scripts/sw-ux.sh +1 -1
  108. package/scripts/sw-webhook.sh +1 -1
  109. package/scripts/sw-widgets.sh +1 -1
  110. package/scripts/sw-worktree.sh +1 -1
  111. package/templates/pipelines/autonomous.json +2 -2
  112. package/tmux/shipwright-overlay.conf +35 -17
  113. package/tmux/tmux.conf +23 -21
@@ -12,6 +12,7 @@ import {
12
12
  } from "fs";
13
13
  import { join, extname } from "path";
14
14
  import { execSync } from "child_process";
15
+ import { Database } from "bun:sqlite";
15
16
 
16
17
  // ─── Config ──────────────────────────────────────────────────────────
17
18
  const PORT = parseInt(
@@ -40,6 +41,107 @@ const SESSION_SECRET = process.env.SESSION_SECRET || crypto.randomUUID();
40
41
  const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
41
42
  const ALLOWED_PERMISSIONS = ["admin", "write"];
42
43
 
44
+ // ─── SQLite Database (optional) ──────────────────────────────────────
45
+ const DB_FILE = join(HOME, ".shipwright", "shipwright.db");
46
+ let db: Database | null = null;
47
+
48
+ function getDb(): Database | null {
49
+ if (db) return db;
50
+ try {
51
+ if (!existsSync(DB_FILE)) return null;
52
+ db = new Database(DB_FILE, { readonly: true });
53
+ db.exec("PRAGMA journal_mode=WAL;");
54
+ return db;
55
+ } catch {
56
+ return null;
57
+ }
58
+ }
59
+
60
+ function dbQueryEvents(since?: number, limit = 200): DaemonEvent[] {
61
+ const conn = getDb();
62
+ if (!conn) return [];
63
+ try {
64
+ const cutoff = since || 0;
65
+ const rows = conn
66
+ .query(
67
+ `SELECT ts, ts_epoch, type, job_id, stage, status, duration_secs, metadata
68
+ FROM events WHERE ts_epoch >= ? ORDER BY ts_epoch DESC LIMIT ?`,
69
+ )
70
+ .all(cutoff, limit) as Array<Record<string, unknown>>;
71
+ return rows.map((r) => {
72
+ const base: DaemonEvent = {
73
+ ts: r.ts as string,
74
+ ts_epoch: r.ts_epoch as number,
75
+ type: r.type as string,
76
+ };
77
+ if (r.job_id)
78
+ base.issue = parseInt(String(r.job_id).replace(/\D/g, "")) || undefined;
79
+ if (r.stage) base.stage = r.stage as string;
80
+ if (r.duration_secs) base.duration_s = r.duration_secs as number;
81
+ if (r.status) base.result = r.status as string;
82
+ if (r.metadata) {
83
+ try {
84
+ Object.assign(base, JSON.parse(r.metadata as string));
85
+ } catch {
86
+ /* ignore malformed metadata */
87
+ }
88
+ }
89
+ return base;
90
+ });
91
+ } catch {
92
+ return [];
93
+ }
94
+ }
95
+
96
+ function dbQueryJobs(status?: string): Array<Record<string, unknown>> {
97
+ const conn = getDb();
98
+ if (!conn) return [];
99
+ try {
100
+ if (status) {
101
+ return conn
102
+ .query(
103
+ "SELECT * FROM daemon_state WHERE status = ? ORDER BY started_at DESC",
104
+ )
105
+ .all(status) as Array<Record<string, unknown>>;
106
+ }
107
+ return conn
108
+ .query("SELECT * FROM daemon_state ORDER BY started_at DESC LIMIT 50")
109
+ .all() as Array<Record<string, unknown>>;
110
+ } catch {
111
+ return [];
112
+ }
113
+ }
114
+
115
+ function dbQueryCostsToday(): { total: number; count: number } {
116
+ const conn = getDb();
117
+ if (!conn) return { total: 0, count: 0 };
118
+ try {
119
+ const todayStart = new Date();
120
+ todayStart.setUTCHours(0, 0, 0, 0);
121
+ const epoch = Math.floor(todayStart.getTime() / 1000);
122
+ const row = conn
123
+ .query(
124
+ "SELECT COALESCE(SUM(cost_usd), 0) as total, COUNT(*) as count FROM cost_entries WHERE ts_epoch >= ?",
125
+ )
126
+ .get(epoch) as { total: number; count: number } | null;
127
+ return row || { total: 0, count: 0 };
128
+ } catch {
129
+ return { total: 0, count: 0 };
130
+ }
131
+ }
132
+
133
+ function dbQueryHeartbeats(): Array<Record<string, unknown>> {
134
+ const conn = getDb();
135
+ if (!conn) return [];
136
+ try {
137
+ return conn
138
+ .query("SELECT * FROM heartbeats ORDER BY updated_at DESC")
139
+ .all() as Array<Record<string, unknown>>;
140
+ } catch {
141
+ return [];
142
+ }
143
+ }
144
+
43
145
  // ─── ANSI helpers ────────────────────────────────────────────────────
44
146
  const CYAN = "\x1b[38;2;0;212;255m";
45
147
  const GREEN = "\x1b[38;2;74;222;128m";
@@ -602,6 +704,11 @@ function broadcastToClients(data: FleetState): void {
602
704
 
603
705
  // ─── Data Collection ─────────────────────────────────────────────────
604
706
  function readEvents(): DaemonEvent[] {
707
+ // Try SQLite first (faster for large event logs)
708
+ const dbEvents = dbQueryEvents(0, 10000);
709
+ if (dbEvents.length > 0) return dbEvents;
710
+
711
+ // Fallback to JSONL
605
712
  if (!existsSync(EVENTS_FILE)) return [];
606
713
  try {
607
714
  const content = readFileSync(EVENTS_FILE, "utf-8").trim();
@@ -2610,6 +2717,107 @@ const server = Bun.serve({
2610
2717
  });
2611
2718
  }
2612
2719
 
2720
+ // ── SQLite DB API endpoints ─────────────────────────────────────
2721
+
2722
+ // REST: Events from DB with since/limit params
2723
+ if (pathname === "/api/db/events") {
2724
+ const since = parseInt(url.searchParams.get("since") || "0");
2725
+ const limit = Math.min(
2726
+ parseInt(url.searchParams.get("limit") || "200"),
2727
+ 10000,
2728
+ );
2729
+ const events = dbQueryEvents(since, limit);
2730
+ return new Response(
2731
+ JSON.stringify({
2732
+ events,
2733
+ source: events.length > 0 ? "sqlite" : "none",
2734
+ }),
2735
+ {
2736
+ headers: { "Content-Type": "application/json", ...CORS_HEADERS },
2737
+ },
2738
+ );
2739
+ }
2740
+
2741
+ // REST: Active/completed jobs from DB
2742
+ if (pathname === "/api/db/jobs") {
2743
+ const status = url.searchParams.get("status") || undefined;
2744
+ const jobs = dbQueryJobs(status);
2745
+ return new Response(
2746
+ JSON.stringify({ jobs, source: jobs.length > 0 ? "sqlite" : "none" }),
2747
+ {
2748
+ headers: { "Content-Type": "application/json", ...CORS_HEADERS },
2749
+ },
2750
+ );
2751
+ }
2752
+
2753
+ // REST: Today's cost from DB
2754
+ if (pathname === "/api/db/costs/today") {
2755
+ const costs = dbQueryCostsToday();
2756
+ return new Response(JSON.stringify({ ...costs, source: "sqlite" }), {
2757
+ headers: { "Content-Type": "application/json", ...CORS_HEADERS },
2758
+ });
2759
+ }
2760
+
2761
+ // REST: Heartbeats from DB
2762
+ if (pathname === "/api/db/heartbeats") {
2763
+ const heartbeats = dbQueryHeartbeats();
2764
+ return new Response(
2765
+ JSON.stringify({
2766
+ heartbeats,
2767
+ source: heartbeats.length > 0 ? "sqlite" : "none",
2768
+ }),
2769
+ {
2770
+ headers: { "Content-Type": "application/json", ...CORS_HEADERS },
2771
+ },
2772
+ );
2773
+ }
2774
+
2775
+ // REST: DB health info
2776
+ if (pathname === "/api/db/health") {
2777
+ const conn = getDb();
2778
+ if (!conn) {
2779
+ return new Response(JSON.stringify({ available: false }), {
2780
+ headers: { "Content-Type": "application/json", ...CORS_HEADERS },
2781
+ });
2782
+ }
2783
+ try {
2784
+ const version = conn
2785
+ .query("SELECT MAX(version) as v FROM _schema")
2786
+ .get() as { v: number } | null;
2787
+ const walMode = conn.query("PRAGMA journal_mode").get() as {
2788
+ journal_mode: string;
2789
+ } | null;
2790
+ const eventCount = conn
2791
+ .query("SELECT COUNT(*) as c FROM events")
2792
+ .get() as { c: number } | null;
2793
+ const runCount = conn
2794
+ .query("SELECT COUNT(*) as c FROM pipeline_runs")
2795
+ .get() as { c: number } | null;
2796
+ const costCount = conn
2797
+ .query("SELECT COUNT(*) as c FROM cost_entries")
2798
+ .get() as { c: number } | null;
2799
+
2800
+ return new Response(
2801
+ JSON.stringify({
2802
+ available: true,
2803
+ schema_version: version?.v || 0,
2804
+ wal_mode: walMode?.journal_mode || "unknown",
2805
+ events: eventCount?.c || 0,
2806
+ runs: runCount?.c || 0,
2807
+ costs: costCount?.c || 0,
2808
+ }),
2809
+ { headers: { "Content-Type": "application/json", ...CORS_HEADERS } },
2810
+ );
2811
+ } catch {
2812
+ return new Response(
2813
+ JSON.stringify({ available: false, error: "query failed" }),
2814
+ {
2815
+ headers: { "Content-Type": "application/json", ...CORS_HEADERS },
2816
+ },
2817
+ );
2818
+ }
2819
+ }
2820
+
2613
2821
  // REST: Memory failure patterns for a specific issue
2614
2822
  if (pathname.startsWith("/api/memory/failures/")) {
2615
2823
  const issueNum = parseInt(pathname.split("/")[4] || "0");