opencode-dashboard 0.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/LICENSE +21 -0
- package/README.md +329 -0
- package/agents/orchestrator.md +99 -0
- package/agents/pipeline-builder.md +53 -0
- package/agents/pipeline-committer.md +78 -0
- package/agents/pipeline-refactor.md +58 -0
- package/agents/pipeline-reviewer.md +68 -0
- package/bin/cli.ts +332 -0
- package/commands/dashboard-start.md +5 -0
- package/commands/dashboard-status.md +5 -0
- package/commands/dashboard-stop.md +5 -0
- package/dist/assets/index-W-qyIr7d.js +134 -0
- package/dist/assets/index-mMdK5PVd.css +1 -0
- package/dist/index.html +13 -0
- package/package.json +82 -0
- package/plugin/index.ts +1441 -0
- package/server/PLUGIN_EVENTS.md +410 -0
- package/server/index.ts +55 -0
- package/server/pid.ts +140 -0
- package/server/routes.ts +520 -0
- package/server/sse.ts +196 -0
- package/server/state.ts +936 -0
- package/shared/types.ts +402 -0
package/bin/cli.ts
ADDED
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* CLI for the OpenCode Dashboard
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx opencode-dashboard <command> [options]
|
|
7
|
+
*
|
|
8
|
+
* Commands:
|
|
9
|
+
* setup [--global|--project] Install agents and commands
|
|
10
|
+
* start [--port 3333] Start the dashboard server
|
|
11
|
+
* stop Stop a running dashboard server
|
|
12
|
+
* status Check if the dashboard server is running
|
|
13
|
+
*
|
|
14
|
+
* Options:
|
|
15
|
+
* --port, -p Server port (default: 3333, env: DASHBOARD_PORT)
|
|
16
|
+
* --help, -h Show help
|
|
17
|
+
* --version, -v Show version
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { join } from "path";
|
|
21
|
+
import { readdir, copyFile, mkdir, stat } from "fs/promises";
|
|
22
|
+
import { createInterface } from "readline";
|
|
23
|
+
import { readPid, removePid, isServerRunning } from "../server/pid";
|
|
24
|
+
|
|
25
|
+
// ─── Constants ─────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const SERVER_ENTRY = join(import.meta.dir, "..", "server", "index.ts");
|
|
28
|
+
const DEFAULT_PORT = 3333;
|
|
29
|
+
const SPAWN_TIMEOUT_MS = 10_000;
|
|
30
|
+
const SPAWN_POLL_INTERVAL_MS = 250;
|
|
31
|
+
|
|
32
|
+
// ─── Helpers ───────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
function printHelp(): void {
|
|
35
|
+
console.log(`
|
|
36
|
+
opencode-dashboard - Real-time Kanban dashboard for OpenCode agent activity
|
|
37
|
+
|
|
38
|
+
Usage:
|
|
39
|
+
opencode-dashboard <command> [options]
|
|
40
|
+
|
|
41
|
+
Commands:
|
|
42
|
+
setup Install agents and commands into your OpenCode config
|
|
43
|
+
start Start the dashboard server
|
|
44
|
+
stop Stop a running dashboard server
|
|
45
|
+
status Check if the dashboard server is running
|
|
46
|
+
|
|
47
|
+
Setup Options:
|
|
48
|
+
--global Install to ~/.config/opencode/ (shared across projects)
|
|
49
|
+
--project Install to .opencode/ (current project only)
|
|
50
|
+
|
|
51
|
+
Server Options:
|
|
52
|
+
--port, -p <port> Server port (default: ${DEFAULT_PORT}, env: DASHBOARD_PORT)
|
|
53
|
+
|
|
54
|
+
General:
|
|
55
|
+
--help, -h Show this help message
|
|
56
|
+
--version, -v Show version
|
|
57
|
+
`.trim());
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printVersion(): void {
|
|
61
|
+
try {
|
|
62
|
+
const pkg = require("../package.json");
|
|
63
|
+
console.log(pkg.version);
|
|
64
|
+
} catch {
|
|
65
|
+
console.log("unknown");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parsePort(args: string[]): number {
|
|
70
|
+
const portIdx = args.findIndex((a) => a === "--port" || a === "-p");
|
|
71
|
+
if (portIdx !== -1 && args[portIdx + 1]) {
|
|
72
|
+
const port = Number(args[portIdx + 1]);
|
|
73
|
+
if (Number.isNaN(port) || port < 1 || port > 65535) {
|
|
74
|
+
console.error(`Error: Invalid port "${args[portIdx + 1]}". Must be 1-65535.`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
return port;
|
|
78
|
+
}
|
|
79
|
+
return Number(process.env.DASHBOARD_PORT) || DEFAULT_PORT;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function checkHealth(port: number): Promise<boolean> {
|
|
83
|
+
try {
|
|
84
|
+
const res = await fetch(`http://localhost:${port}/api/health`, {
|
|
85
|
+
signal: AbortSignal.timeout(3000),
|
|
86
|
+
});
|
|
87
|
+
return res.ok;
|
|
88
|
+
} catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Commands ──────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
async function cmdStart(args: string[]): Promise<void> {
|
|
96
|
+
const port = parsePort(args);
|
|
97
|
+
|
|
98
|
+
// Check if already running
|
|
99
|
+
const existing = await isServerRunning(true);
|
|
100
|
+
if (existing) {
|
|
101
|
+
console.log(`Dashboard server is already running.`);
|
|
102
|
+
console.log(` URL: http://localhost:${existing.port}`);
|
|
103
|
+
console.log(` PID: ${existing.pid}`);
|
|
104
|
+
console.log(` Started: ${existing.startedAt}`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
console.log(`Starting dashboard server on port ${port}...`);
|
|
109
|
+
|
|
110
|
+
// Use Bun.which to find bun reliably (process.execPath may not always be bun)
|
|
111
|
+
const bunPath = Bun.which("bun") ?? process.execPath;
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const proc = Bun.spawn([bunPath, "run", SERVER_ENTRY], {
|
|
115
|
+
detached: true,
|
|
116
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
117
|
+
env: { ...process.env, DASHBOARD_PORT: String(port) },
|
|
118
|
+
});
|
|
119
|
+
proc.unref();
|
|
120
|
+
} catch (err: any) {
|
|
121
|
+
console.error(`Failed to start server: ${err?.message ?? err}`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Poll for readiness
|
|
126
|
+
const maxAttempts = Math.ceil(SPAWN_TIMEOUT_MS / SPAWN_POLL_INTERVAL_MS);
|
|
127
|
+
for (let i = 0; i < maxAttempts; i++) {
|
|
128
|
+
await Bun.sleep(SPAWN_POLL_INTERVAL_MS);
|
|
129
|
+
if (await checkHealth(port)) {
|
|
130
|
+
console.log(`Dashboard running at http://localhost:${port}`);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.error(`Server failed to start within ${SPAWN_TIMEOUT_MS / 1000}s.`);
|
|
136
|
+
console.error(`Check if port ${port} is already in use.`);
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function cmdStop(): Promise<void> {
|
|
141
|
+
const pidData = readPid();
|
|
142
|
+
if (!pidData) {
|
|
143
|
+
console.log("No dashboard server found (no PID file).");
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
process.kill(pidData.pid, "SIGTERM");
|
|
149
|
+
removePid();
|
|
150
|
+
console.log(`Dashboard server stopped (PID: ${pidData.pid}).`);
|
|
151
|
+
} catch (err: any) {
|
|
152
|
+
if (err?.code === "ESRCH") {
|
|
153
|
+
removePid();
|
|
154
|
+
console.log("Dashboard server was not running (stale PID file cleaned up).");
|
|
155
|
+
} else {
|
|
156
|
+
console.error(`Failed to stop server: ${err?.message ?? err}`);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function cmdStatus(): Promise<void> {
|
|
163
|
+
const pidData = await isServerRunning(false);
|
|
164
|
+
if (!pidData) {
|
|
165
|
+
console.log("Dashboard server is not running.");
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Process is alive, try health endpoint for more detail
|
|
170
|
+
try {
|
|
171
|
+
const res = await fetch(`http://localhost:${pidData.port}/api/health`, {
|
|
172
|
+
signal: AbortSignal.timeout(3000),
|
|
173
|
+
});
|
|
174
|
+
if (res.ok) {
|
|
175
|
+
const health = (await res.json()) as {
|
|
176
|
+
uptime?: number;
|
|
177
|
+
plugins?: number;
|
|
178
|
+
sseClients?: number;
|
|
179
|
+
};
|
|
180
|
+
console.log(`Dashboard server is running.`);
|
|
181
|
+
console.log(` URL: http://localhost:${pidData.port}`);
|
|
182
|
+
console.log(` PID: ${pidData.pid}`);
|
|
183
|
+
console.log(` Uptime: ${health.uptime ?? "?"}s`);
|
|
184
|
+
console.log(` Plugins: ${health.plugins ?? "?"}`);
|
|
185
|
+
console.log(` SSE: ${health.sseClients ?? "?"} clients`);
|
|
186
|
+
console.log(` Started: ${pidData.startedAt}`);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
console.log(`Dashboard server process exists (PID: ${pidData.pid}) but health check failed.`);
|
|
190
|
+
} catch {
|
|
191
|
+
console.log(`Dashboard server process exists (PID: ${pidData.pid}) but is not responding.`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ─── Setup Command ─────────────────────────────────────────────
|
|
196
|
+
|
|
197
|
+
const SHIPPED_AGENTS_DIR = join(import.meta.dir, "..", "agents");
|
|
198
|
+
const SHIPPED_COMMANDS_DIR = join(import.meta.dir, "..", "commands");
|
|
199
|
+
|
|
200
|
+
function getGlobalConfigDir(): string {
|
|
201
|
+
return join(
|
|
202
|
+
process.env.HOME ?? process.env.USERPROFILE ?? "",
|
|
203
|
+
".config",
|
|
204
|
+
"opencode",
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function getProjectConfigDir(): string {
|
|
209
|
+
return join(process.cwd(), ".opencode");
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async function prompt(question: string): Promise<string> {
|
|
213
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
214
|
+
return new Promise((resolve) => {
|
|
215
|
+
rl.question(question, (answer) => {
|
|
216
|
+
rl.close();
|
|
217
|
+
resolve(answer.trim().toLowerCase());
|
|
218
|
+
});
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async function copyDir(
|
|
223
|
+
srcDir: string,
|
|
224
|
+
destDir: string,
|
|
225
|
+
label: string,
|
|
226
|
+
): Promise<string[]> {
|
|
227
|
+
const installed: string[] = [];
|
|
228
|
+
try {
|
|
229
|
+
await mkdir(destDir, { recursive: true });
|
|
230
|
+
const entries = await readdir(srcDir);
|
|
231
|
+
for (const entry of entries) {
|
|
232
|
+
if (!entry.endsWith(".md")) continue;
|
|
233
|
+
const destPath = join(destDir, entry);
|
|
234
|
+
// Skip files that already exist (don't overwrite user customizations)
|
|
235
|
+
try {
|
|
236
|
+
await stat(destPath);
|
|
237
|
+
console.log(` skip ${entry} (already exists)`);
|
|
238
|
+
continue;
|
|
239
|
+
} catch {
|
|
240
|
+
// File doesn't exist — proceed
|
|
241
|
+
}
|
|
242
|
+
await copyFile(join(srcDir, entry), destPath);
|
|
243
|
+
installed.push(entry);
|
|
244
|
+
console.log(` add ${entry}`);
|
|
245
|
+
}
|
|
246
|
+
} catch (err: any) {
|
|
247
|
+
console.error(`Failed to install ${label}: ${err?.message ?? err}`);
|
|
248
|
+
}
|
|
249
|
+
return installed;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function cmdSetup(args: string[]): Promise<void> {
|
|
253
|
+
let scope: "global" | "project" | null = null;
|
|
254
|
+
|
|
255
|
+
if (args.includes("--global")) {
|
|
256
|
+
scope = "global";
|
|
257
|
+
} else if (args.includes("--project")) {
|
|
258
|
+
scope = "project";
|
|
259
|
+
} else {
|
|
260
|
+
// Interactive prompt
|
|
261
|
+
console.log("\nWhere would you like to install agents and commands?\n");
|
|
262
|
+
console.log(" g) Global (~/.config/opencode/) - shared across all projects");
|
|
263
|
+
console.log(" p) Project (.opencode/) - current project only\n");
|
|
264
|
+
const answer = await prompt("Choose [g/p]: ");
|
|
265
|
+
if (answer === "g" || answer === "global") {
|
|
266
|
+
scope = "global";
|
|
267
|
+
} else if (answer === "p" || answer === "project") {
|
|
268
|
+
scope = "project";
|
|
269
|
+
} else {
|
|
270
|
+
console.error(`Invalid choice: "${answer}". Use "g" for global or "p" for project.`);
|
|
271
|
+
process.exit(1);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const baseDir = scope === "global" ? getGlobalConfigDir() : getProjectConfigDir();
|
|
276
|
+
const agentsDir = join(baseDir, "agents");
|
|
277
|
+
const commandsDir = join(baseDir, "commands");
|
|
278
|
+
|
|
279
|
+
console.log(`\nInstalling to ${baseDir}/\n`);
|
|
280
|
+
|
|
281
|
+
// Install agents
|
|
282
|
+
console.log("Agents:");
|
|
283
|
+
const agents = await copyDir(SHIPPED_AGENTS_DIR, agentsDir, "agents");
|
|
284
|
+
|
|
285
|
+
// Install commands
|
|
286
|
+
console.log("\nCommands:");
|
|
287
|
+
const commands = await copyDir(SHIPPED_COMMANDS_DIR, commandsDir, "commands");
|
|
288
|
+
|
|
289
|
+
const total = agents.length + commands.length;
|
|
290
|
+
if (total > 0) {
|
|
291
|
+
console.log(`\nInstalled ${agents.length} agent(s) and ${commands.length} command(s).`);
|
|
292
|
+
} else {
|
|
293
|
+
console.log(`\nNothing new to install — all files already exist.`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
console.log(`\nRestart OpenCode to pick up the changes.`);
|
|
297
|
+
console.log(`Then use /dashboard-start in the TUI to start the dashboard.`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ─── Main ──────────────────────────────────────────────────────
|
|
301
|
+
|
|
302
|
+
const args = process.argv.slice(2);
|
|
303
|
+
const command = args[0];
|
|
304
|
+
|
|
305
|
+
if (args.includes("--help") || args.includes("-h") || !command) {
|
|
306
|
+
printHelp();
|
|
307
|
+
process.exit(command ? 0 : 1);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
311
|
+
printVersion();
|
|
312
|
+
process.exit(0);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
switch (command) {
|
|
316
|
+
case "setup":
|
|
317
|
+
await cmdSetup(args.slice(1));
|
|
318
|
+
break;
|
|
319
|
+
case "start":
|
|
320
|
+
await cmdStart(args.slice(1));
|
|
321
|
+
break;
|
|
322
|
+
case "stop":
|
|
323
|
+
await cmdStop();
|
|
324
|
+
break;
|
|
325
|
+
case "status":
|
|
326
|
+
await cmdStatus();
|
|
327
|
+
break;
|
|
328
|
+
default:
|
|
329
|
+
console.error(`Unknown command: ${command}`);
|
|
330
|
+
console.error(`Run "opencode-dashboard --help" for usage.`);
|
|
331
|
+
process.exit(1);
|
|
332
|
+
}
|