oh-my-opencode-dashboard 0.0.1

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/src/cli/dev.ts ADDED
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env bun
2
+ import { spawn } from 'node:child_process';
3
+ import { cwd, exit } from 'node:process';
4
+
5
+ import { findAvailablePort } from './ports';
6
+
7
+ interface CliArgs {
8
+ project: string;
9
+ port: number;
10
+ }
11
+
12
+ function parseArgs(): CliArgs {
13
+ const args = process.argv.slice(2);
14
+ let project: string | undefined;
15
+ let port = 51234; // Default port
16
+
17
+ for (let i = 0; i < args.length; i++) {
18
+ const arg = args[i];
19
+
20
+ if (arg === '--project' && i + 1 < args.length) {
21
+ project = args[i + 1];
22
+ i++; // Skip next argument
23
+ } else if (arg === '--port' && i + 1 < args.length) {
24
+ const portStr = args[i + 1];
25
+ const parsedPort = parseInt(portStr, 10);
26
+ if (isNaN(parsedPort) || parsedPort <= 0) {
27
+ console.error('Error: --port must be a positive integer');
28
+ exit(1);
29
+ }
30
+ port = parsedPort;
31
+ i++; // Skip next argument
32
+ }
33
+ }
34
+
35
+ return { project: project ?? cwd(), port };
36
+ }
37
+
38
+ async function main() {
39
+ const { project, port } = parseArgs();
40
+
41
+ const host = '127.0.0.1';
42
+ const resolvedPort = await findAvailablePort({ host, preferredPort: port });
43
+ if (resolvedPort !== port) {
44
+ console.log(`Port ${port} is busy; using ${resolvedPort} instead`);
45
+ }
46
+
47
+ console.log(`Starting dev servers for project: ${project}`);
48
+ console.log(`API port: ${resolvedPort}`);
49
+
50
+ const apiArgs = ['run', 'src/server/dev.ts', '--', '--project', project, '--port', resolvedPort.toString()];
51
+ const uiArgs = ['run', 'dev:ui'];
52
+
53
+ const apiServer = spawn('bun', apiArgs, {
54
+ stdio: 'inherit',
55
+ shell: true
56
+ });
57
+
58
+ const uiServer = spawn('bun', uiArgs, {
59
+ stdio: 'inherit',
60
+ shell: true,
61
+ env: {
62
+ ...process.env,
63
+ OMO_DASHBOARD_API_PORT: resolvedPort.toString(),
64
+ },
65
+ });
66
+
67
+ // Handle signals and clean up both processes
68
+ const cleanup = () => {
69
+ console.log('\nShutting down servers...');
70
+ apiServer.kill('SIGTERM');
71
+ uiServer.kill('SIGTERM');
72
+
73
+ // Force kill if they don't terminate gracefully
74
+ setTimeout(() => {
75
+ apiServer.kill('SIGKILL');
76
+ uiServer.kill('SIGKILL');
77
+ exit(0);
78
+ }, 5000);
79
+ };
80
+
81
+ process.on('SIGINT', cleanup);
82
+ process.on('SIGTERM', cleanup);
83
+
84
+ // Wait for either process to exit
85
+ let apiExited = false;
86
+ let uiExited = false;
87
+
88
+ const handleExit = (processName: string) => {
89
+ if (processName === 'API') {
90
+ apiExited = true;
91
+ } else {
92
+ uiExited = true;
93
+ }
94
+
95
+ if (apiExited || uiExited) {
96
+ console.log(`${processName} server exited, shutting down the other server...`);
97
+ cleanup();
98
+ }
99
+ };
100
+
101
+ apiServer.on('exit', (code, signal) => {
102
+ if (signal) {
103
+ console.log(`API server killed by signal: ${signal}`);
104
+ } else if (code !== 0) {
105
+ console.log(`API server exited with code: ${code}`);
106
+ }
107
+ handleExit('API');
108
+ });
109
+
110
+ uiServer.on('exit', (code, signal) => {
111
+ if (signal) {
112
+ console.log(`UI server killed by signal: ${signal}`);
113
+ } else if (code !== 0) {
114
+ console.log(`UI server exited with code: ${code}`);
115
+ }
116
+ handleExit('UI');
117
+ });
118
+
119
+ apiServer.on('error', (error) => {
120
+ console.error('Failed to start API server:', error.message);
121
+ cleanup();
122
+ exit(1);
123
+ });
124
+
125
+ uiServer.on('error', (error) => {
126
+ console.error('Failed to start UI server:', error.message);
127
+ cleanup();
128
+ exit(1);
129
+ });
130
+
131
+ console.log('Both servers started successfully');
132
+ console.log(`API server: http://127.0.0.1:${resolvedPort}`);
133
+ console.log('UI server: check Vite output for URL');
134
+ }
135
+
136
+ main().catch((error) => {
137
+ console.error('Fatal error:', error);
138
+ exit(1);
139
+ });
@@ -0,0 +1,40 @@
1
+ import net from "node:net"
2
+ import { describe, expect, it } from "vitest"
3
+
4
+ import { findAvailablePort } from "./ports"
5
+
6
+ describe("findAvailablePort", () => {
7
+ it("returns a bindable port", async () => {
8
+ const port = await findAvailablePort({ host: "127.0.0.1", preferredPort: 51234 })
9
+
10
+ const server = net.createServer()
11
+ await new Promise<void>((resolve, reject) => {
12
+ server.once("error", reject)
13
+ server.listen(port, "127.0.0.1", () => resolve())
14
+ })
15
+
16
+ await new Promise<void>((resolve) => server.close(() => resolve()))
17
+ expect(port).toBeGreaterThan(0)
18
+ })
19
+
20
+ it("skips a port that is already in use", async () => {
21
+ const blocker = net.createServer()
22
+ await new Promise<void>((resolve, reject) => {
23
+ blocker.once("error", reject)
24
+ blocker.listen(0, "127.0.0.1", () => resolve())
25
+ })
26
+
27
+ const address = blocker.address()
28
+ if (!address || typeof address === "string") throw new Error("Unexpected address")
29
+ const usedPort = address.port
30
+
31
+ const port = await findAvailablePort({
32
+ host: "127.0.0.1",
33
+ preferredPort: usedPort,
34
+ maxTries: 50,
35
+ })
36
+
37
+ expect(port).not.toBe(usedPort)
38
+ await new Promise<void>((resolve) => blocker.close(() => resolve()))
39
+ })
40
+ })
@@ -0,0 +1,43 @@
1
+ import net from "node:net"
2
+
3
+ export interface FindAvailablePortOptions {
4
+ host: string
5
+ preferredPort: number
6
+ maxTries?: number
7
+ }
8
+
9
+ export async function findAvailablePort({
10
+ host,
11
+ preferredPort,
12
+ maxTries = 20,
13
+ }: FindAvailablePortOptions): Promise<number> {
14
+ if (!Number.isInteger(preferredPort) || preferredPort <= 0) {
15
+ throw new Error("preferredPort must be a positive integer")
16
+ }
17
+
18
+ for (let offset = 0; offset < maxTries; offset++) {
19
+ const port = preferredPort + offset
20
+ const ok = await canListen({ host, port })
21
+ if (ok) return port
22
+ }
23
+
24
+ throw new Error(
25
+ `No available port found starting at ${preferredPort} after ${maxTries} attempts`,
26
+ )
27
+ }
28
+
29
+ function canListen({ host, port }: { host: string; port: number }): Promise<boolean> {
30
+ return new Promise((resolve) => {
31
+ const server = net.createServer()
32
+
33
+ server.once("error", () => {
34
+ resolve(false)
35
+ })
36
+
37
+ server.once("listening", () => {
38
+ server.close(() => resolve(true))
39
+ })
40
+
41
+ server.listen(port, host)
42
+ })
43
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, expect, it } from "vitest"
2
+
3
+ import { computeWaitingDing } from "./ding-policy"
4
+
5
+ describe("computeWaitingDing", () => {
6
+ it("dings on first observation when waiting", () => {
7
+ const res = computeWaitingDing({
8
+ prev: { prevWaiting: null, lastLeftWaitingAtMs: null },
9
+ waiting: true,
10
+ nowMs: 1000,
11
+ })
12
+
13
+ expect(res.play).toBe(true)
14
+ expect(res.next.prevWaiting).toBe(true)
15
+ })
16
+
17
+ it("dings when entering waiting if last idle round-trip is >= 20s", () => {
18
+ const res = computeWaitingDing({
19
+ prev: { prevWaiting: false, lastLeftWaitingAtMs: 1000 },
20
+ waiting: true,
21
+ nowMs: 21_000,
22
+ })
23
+
24
+ expect(res.play).toBe(true)
25
+ })
26
+
27
+ it("suppresses ding when waiting round-trip is < 20s", () => {
28
+ const left = computeWaitingDing({
29
+ prev: { prevWaiting: true, lastLeftWaitingAtMs: null },
30
+ waiting: false,
31
+ nowMs: 1000,
32
+ })
33
+
34
+ expect(left.play).toBe(false)
35
+ expect(left.next.prevWaiting).toBe(false)
36
+ expect(left.next.lastLeftWaitingAtMs).toBe(1000)
37
+
38
+ const back = computeWaitingDing({
39
+ prev: left.next,
40
+ waiting: true,
41
+ nowMs: 20_999,
42
+ })
43
+
44
+ expect(back.play).toBe(false)
45
+ expect(back.next.prevWaiting).toBe(true)
46
+ expect(back.next.lastLeftWaitingAtMs).toBe(null)
47
+ })
48
+ })
@@ -0,0 +1,39 @@
1
+ export type WaitingDingState = {
2
+ prevWaiting: boolean | null
3
+ lastLeftWaitingAtMs: number | null
4
+ }
5
+
6
+ export function computeWaitingDing(opts: {
7
+ prev: WaitingDingState
8
+ waiting: boolean
9
+ nowMs: number
10
+ suppressMs?: number
11
+ }): { play: boolean; next: WaitingDingState } {
12
+ const suppressMs = opts.suppressMs ?? 20_000
13
+
14
+ let lastLeftWaitingAtMs = opts.prev.lastLeftWaitingAtMs
15
+
16
+ if (opts.prev.prevWaiting === true && opts.waiting === false) {
17
+ lastLeftWaitingAtMs = opts.nowMs
18
+ }
19
+
20
+ let play = false
21
+ if (opts.prev.prevWaiting === null && opts.waiting === true) {
22
+ play = true
23
+ } else if (opts.prev.prevWaiting === false && opts.waiting === true) {
24
+ const dt = typeof lastLeftWaitingAtMs === "number" ? opts.nowMs - lastLeftWaitingAtMs : null
25
+ play = !(typeof dt === "number" && dt >= 0 && dt < suppressMs)
26
+ }
27
+
28
+ if (opts.waiting === true) {
29
+ lastLeftWaitingAtMs = null
30
+ }
31
+
32
+ return {
33
+ play,
34
+ next: {
35
+ prevWaiting: opts.waiting,
36
+ lastLeftWaitingAtMs,
37
+ },
38
+ }
39
+ }