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.
- package/README.md +160 -72
- package/completions/_shipwright +59 -7
- package/completions/shipwright.bash +24 -4
- package/completions/shipwright.fish +80 -2
- package/dashboard/server.ts +208 -0
- package/docs/tmux-research/TMUX-ARCHITECTURE.md +567 -0
- package/docs/tmux-research/TMUX-AUDIT.md +925 -0
- package/docs/tmux-research/TMUX-BEST-PRACTICES-2025-2026.md +829 -0
- package/docs/tmux-research/TMUX-QUICK-REFERENCE.md +543 -0
- package/docs/tmux-research/TMUX-RESEARCH-INDEX.md +438 -0
- package/package.json +2 -2
- package/scripts/lib/helpers.sh +7 -0
- package/scripts/sw +116 -2
- package/scripts/sw-activity.sh +1 -1
- package/scripts/sw-adaptive.sh +1 -1
- package/scripts/sw-adversarial.sh +1 -1
- package/scripts/sw-architecture-enforcer.sh +1 -1
- package/scripts/sw-auth.sh +1 -1
- package/scripts/sw-autonomous.sh +128 -38
- package/scripts/sw-changelog.sh +1 -1
- package/scripts/sw-checkpoint.sh +1 -1
- package/scripts/sw-ci.sh +1 -1
- package/scripts/sw-cleanup.sh +1 -1
- package/scripts/sw-code-review.sh +62 -1
- package/scripts/sw-connect.sh +1 -1
- package/scripts/sw-context.sh +1 -1
- package/scripts/sw-cost.sh +44 -3
- package/scripts/sw-daemon.sh +155 -27
- package/scripts/sw-dashboard.sh +1 -1
- package/scripts/sw-db.sh +958 -118
- package/scripts/sw-decompose.sh +1 -1
- package/scripts/sw-deps.sh +1 -1
- package/scripts/sw-developer-simulation.sh +1 -1
- package/scripts/sw-discovery.sh +1 -1
- package/scripts/sw-docs-agent.sh +1 -1
- package/scripts/sw-docs.sh +1 -1
- package/scripts/sw-doctor.sh +49 -1
- package/scripts/sw-dora.sh +1 -1
- package/scripts/sw-durable.sh +1 -1
- package/scripts/sw-e2e-orchestrator.sh +1 -1
- package/scripts/sw-eventbus.sh +1 -1
- package/scripts/sw-feedback.sh +23 -15
- package/scripts/sw-fix.sh +1 -1
- package/scripts/sw-fleet-discover.sh +1 -1
- package/scripts/sw-fleet-viz.sh +1 -1
- package/scripts/sw-fleet.sh +1 -1
- package/scripts/sw-github-app.sh +1 -1
- package/scripts/sw-github-checks.sh +4 -4
- package/scripts/sw-github-deploy.sh +1 -1
- package/scripts/sw-github-graphql.sh +1 -1
- package/scripts/sw-guild.sh +1 -1
- package/scripts/sw-heartbeat.sh +1 -1
- package/scripts/sw-hygiene.sh +1 -1
- package/scripts/sw-incident.sh +45 -6
- package/scripts/sw-init.sh +150 -24
- package/scripts/sw-instrument.sh +1 -1
- package/scripts/sw-intelligence.sh +1 -1
- package/scripts/sw-jira.sh +1 -1
- package/scripts/sw-launchd.sh +1 -1
- package/scripts/sw-linear.sh +1 -1
- package/scripts/sw-logs.sh +1 -1
- package/scripts/sw-loop.sh +204 -19
- package/scripts/sw-memory.sh +18 -1
- package/scripts/sw-mission-control.sh +1 -1
- package/scripts/sw-model-router.sh +1 -1
- package/scripts/sw-otel.sh +1 -1
- package/scripts/sw-oversight.sh +76 -1
- package/scripts/sw-pipeline-composer.sh +1 -1
- package/scripts/sw-pipeline-vitals.sh +1 -1
- package/scripts/sw-pipeline.sh +261 -12
- package/scripts/sw-pm.sh +70 -5
- package/scripts/sw-pr-lifecycle.sh +1 -1
- package/scripts/sw-predictive.sh +8 -1
- package/scripts/sw-prep.sh +1 -1
- package/scripts/sw-ps.sh +1 -1
- package/scripts/sw-public-dashboard.sh +1 -1
- package/scripts/sw-quality.sh +1 -1
- package/scripts/sw-reaper.sh +1 -1
- package/scripts/sw-recruit.sh +1853 -178
- package/scripts/sw-regression.sh +1 -1
- package/scripts/sw-release-manager.sh +1 -1
- package/scripts/sw-release.sh +1 -1
- package/scripts/sw-remote.sh +1 -1
- package/scripts/sw-replay.sh +1 -1
- package/scripts/sw-retro.sh +1 -1
- package/scripts/sw-scale.sh +1 -1
- package/scripts/sw-security-audit.sh +1 -1
- package/scripts/sw-self-optimize.sh +1 -1
- package/scripts/sw-session.sh +1 -1
- package/scripts/sw-setup.sh +263 -127
- package/scripts/sw-standup.sh +1 -1
- package/scripts/sw-status.sh +44 -2
- package/scripts/sw-strategic.sh +189 -41
- package/scripts/sw-stream.sh +1 -1
- package/scripts/sw-swarm.sh +42 -5
- package/scripts/sw-team-stages.sh +1 -1
- package/scripts/sw-templates.sh +4 -4
- package/scripts/sw-testgen.sh +66 -15
- package/scripts/sw-tmux-pipeline.sh +1 -1
- package/scripts/sw-tmux-role-color.sh +58 -0
- package/scripts/sw-tmux-status.sh +128 -0
- package/scripts/sw-tmux.sh +1 -1
- package/scripts/sw-trace.sh +1 -1
- package/scripts/sw-tracker.sh +1 -1
- package/scripts/sw-triage.sh +61 -37
- package/scripts/sw-upgrade.sh +1 -1
- package/scripts/sw-ux.sh +1 -1
- package/scripts/sw-webhook.sh +1 -1
- package/scripts/sw-widgets.sh +1 -1
- package/scripts/sw-worktree.sh +1 -1
- package/templates/pipelines/autonomous.json +2 -2
- package/tmux/shipwright-overlay.conf +35 -17
- package/tmux/tmux.conf +23 -21
package/dashboard/server.ts
CHANGED
|
@@ -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");
|