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/README.md +100 -0
- package/dashboard-ui.png +0 -0
- package/dist/assets/index-D6OVzN1o.css +1 -0
- package/dist/assets/index-SEmwze_4.js +40 -0
- package/dist/index.html +14 -0
- package/index.html +13 -0
- package/package.json +51 -0
- package/src/App.tsx +518 -0
- package/src/cli/dev.ts +139 -0
- package/src/cli/ports.test.ts +40 -0
- package/src/cli/ports.ts +43 -0
- package/src/ding-policy.test.ts +48 -0
- package/src/ding-policy.ts +39 -0
- package/src/ingest/background-tasks.test.ts +707 -0
- package/src/ingest/background-tasks.ts +317 -0
- package/src/ingest/boulder.test.ts +77 -0
- package/src/ingest/boulder.ts +71 -0
- package/src/ingest/paths.test.ts +82 -0
- package/src/ingest/paths.ts +76 -0
- package/src/ingest/session.test.ts +220 -0
- package/src/ingest/session.ts +283 -0
- package/src/main.tsx +10 -0
- package/src/server/api.test.ts +62 -0
- package/src/server/api.ts +16 -0
- package/src/server/build.ts +5 -0
- package/src/server/dashboard.test.ts +135 -0
- package/src/server/dashboard.ts +191 -0
- package/src/server/dev.ts +44 -0
- package/src/server/start.ts +93 -0
- package/src/sound.test.ts +55 -0
- package/src/sound.ts +89 -0
- package/src/styles.css +457 -0
- package/tsconfig.json +15 -0
- package/vite.config.ts +14 -0
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
|
+
})
|
package/src/cli/ports.ts
ADDED
|
@@ -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
|
+
}
|