codedash-app 1.0.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/src/html.js ADDED
@@ -0,0 +1,26 @@
1
+ // Assembles the full HTML page by inlining CSS and JS from frontend/ files
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+
5
+ const FRONTEND_DIR = path.join(__dirname, 'frontend');
6
+
7
+ function buildHTML() {
8
+ const template = fs.readFileSync(path.join(FRONTEND_DIR, 'index.html'), 'utf8');
9
+ const styles = fs.readFileSync(path.join(FRONTEND_DIR, 'styles.css'), 'utf8');
10
+ const script = fs.readFileSync(path.join(FRONTEND_DIR, 'app.js'), 'utf8');
11
+
12
+ return template
13
+ .replace('{{STYLES}}', styles)
14
+ .replace('{{SCRIPT}}', script);
15
+ }
16
+
17
+ // Cache in production
18
+ let cached = null;
19
+ function getHTML() {
20
+ if (process.env.NODE_ENV === 'development' || !cached) {
21
+ cached = buildHTML();
22
+ }
23
+ return cached;
24
+ }
25
+
26
+ module.exports = { getHTML };
package/src/server.js ADDED
@@ -0,0 +1,142 @@
1
+ // HTTP server + API routes
2
+ const http = require('http');
3
+ const { URL } = require('url');
4
+ const { exec } = require('child_process');
5
+ const { loadSessions, loadSessionDetail, deleteSession, getGitCommits, exportSessionMarkdown } = require('./data');
6
+ const { detectTerminals, openInTerminal } = require('./terminals');
7
+ const { getHTML } = require('./html');
8
+
9
+ function startServer(port, openBrowser = true) {
10
+ const server = http.createServer((req, res) => {
11
+ const parsed = new URL(req.url, `http://localhost:${port}`);
12
+ const pathname = parsed.pathname;
13
+
14
+ // ── Static ──────────────────────────────
15
+ if (req.method === 'GET' && (pathname === '/' || pathname === '/index.html')) {
16
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
17
+ res.end(getHTML());
18
+ }
19
+
20
+ // ── Sessions API ────────────────────────
21
+ else if (req.method === 'GET' && pathname === '/api/sessions') {
22
+ const sessions = loadSessions();
23
+ json(res, sessions);
24
+ }
25
+
26
+ else if (req.method === 'GET' && pathname.startsWith('/api/session/') && !pathname.includes('/export')) {
27
+ const sessionId = pathname.split('/').pop();
28
+ const project = parsed.searchParams.get('project') || '';
29
+ const data = loadSessionDetail(sessionId, project);
30
+ json(res, data);
31
+ }
32
+
33
+ // ── Export Markdown ─────────────────────
34
+ else if (req.method === 'GET' && pathname.includes('/export')) {
35
+ // /api/session/<id>/export?project=...
36
+ const parts = pathname.split('/');
37
+ const sessionId = parts[parts.indexOf('session') + 1];
38
+ const project = parsed.searchParams.get('project') || '';
39
+ const md = exportSessionMarkdown(sessionId, project);
40
+ res.writeHead(200, {
41
+ 'Content-Type': 'text/markdown; charset=utf-8',
42
+ 'Content-Disposition': `attachment; filename="session-${sessionId.slice(0, 8)}.md"`,
43
+ });
44
+ res.end(md);
45
+ }
46
+
47
+ // ── Terminals ───────────────────────────
48
+ else if (req.method === 'GET' && pathname === '/api/terminals') {
49
+ const terminals = detectTerminals();
50
+ json(res, terminals);
51
+ }
52
+
53
+ // ── Launch ──────────────────────────────
54
+ else if (req.method === 'POST' && pathname === '/api/launch') {
55
+ readBody(req, body => {
56
+ try {
57
+ const { sessionId, tool, flags, project, terminal } = JSON.parse(body);
58
+ openInTerminal(sessionId, tool || 'claude', flags || [], project || '', terminal || '');
59
+ json(res, { ok: true });
60
+ } catch (e) {
61
+ json(res, { ok: false, error: e.message }, 400);
62
+ }
63
+ });
64
+ }
65
+
66
+ // ── Delete ──────────────────────────────
67
+ else if (req.method === 'DELETE' && pathname.startsWith('/api/session/')) {
68
+ const sessionId = pathname.split('/').pop();
69
+ readBody(req, body => {
70
+ try {
71
+ const { project } = JSON.parse(body || '{}');
72
+ const deleted = deleteSession(sessionId, project || '');
73
+ json(res, { ok: true, deleted });
74
+ } catch (e) {
75
+ json(res, { ok: false, error: e.message }, 400);
76
+ }
77
+ });
78
+ }
79
+
80
+ // ── Bulk Delete ─────────────────────────
81
+ else if (req.method === 'POST' && pathname === '/api/bulk-delete') {
82
+ readBody(req, body => {
83
+ try {
84
+ const { sessions } = JSON.parse(body); // [{id, project}, ...]
85
+ const results = [];
86
+ for (const s of sessions) {
87
+ const deleted = deleteSession(s.id, s.project || '');
88
+ results.push({ id: s.id, deleted });
89
+ }
90
+ json(res, { ok: true, results });
91
+ } catch (e) {
92
+ json(res, { ok: false, error: e.message }, 400);
93
+ }
94
+ });
95
+ }
96
+
97
+ // ── Git Commits ─────────────────────────
98
+ else if (req.method === 'GET' && pathname === '/api/git-commits') {
99
+ const project = parsed.searchParams.get('project') || '';
100
+ const from = parseInt(parsed.searchParams.get('from') || '0');
101
+ const to = parseInt(parsed.searchParams.get('to') || Date.now().toString());
102
+ const commits = getGitCommits(project, from, to);
103
+ json(res, commits);
104
+ }
105
+
106
+ // ── 404 ─────────────────────────────────
107
+ else {
108
+ res.writeHead(404);
109
+ res.end('Not found');
110
+ }
111
+ });
112
+
113
+ server.listen(port, '127.0.0.1', () => {
114
+ console.log('');
115
+ console.log(' \x1b[36m\x1b[1mcodedash\x1b[0m — Claude & Codex Sessions Dashboard');
116
+ console.log(` \x1b[2mhttp://localhost:${port}\x1b[0m`);
117
+ console.log(' \x1b[2mPress Ctrl+C to stop\x1b[0m');
118
+ console.log('');
119
+
120
+ if (openBrowser) {
121
+ if (process.platform === 'darwin') {
122
+ exec(`open http://localhost:${port}`);
123
+ } else if (process.platform === 'linux') {
124
+ exec(`xdg-open http://localhost:${port}`);
125
+ }
126
+ }
127
+ });
128
+ }
129
+
130
+ // ── Helpers ─────────────────────────────────
131
+ function json(res, data, status = 200) {
132
+ res.writeHead(status, { 'Content-Type': 'application/json' });
133
+ res.end(JSON.stringify(data));
134
+ }
135
+
136
+ function readBody(req, cb) {
137
+ let body = '';
138
+ req.on('data', chunk => body += chunk);
139
+ req.on('end', () => cb(body));
140
+ }
141
+
142
+ module.exports = { startServer };
@@ -0,0 +1,160 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const { execSync, exec } = require('child_process');
5
+
6
+ // ── Detect available terminals ──────────────────────────────
7
+
8
+ function detectTerminals() {
9
+ const terminals = [];
10
+ const platform = process.platform;
11
+
12
+ if (platform === 'darwin') {
13
+ // Check iTerm2
14
+ try {
15
+ execSync('osascript -e \'application id "com.googlecode.iterm2"\'', { stdio: 'pipe' });
16
+ terminals.push({ id: 'iterm2', name: 'iTerm2', available: true });
17
+ } catch {
18
+ terminals.push({ id: 'iterm2', name: 'iTerm2', available: false });
19
+ }
20
+ // Terminal.app always available on macOS
21
+ terminals.push({ id: 'terminal', name: 'Terminal.app', available: true });
22
+ // Check Warp
23
+ try {
24
+ if (fs.existsSync('/Applications/Warp.app')) {
25
+ terminals.push({ id: 'warp', name: 'Warp', available: true });
26
+ }
27
+ } catch {}
28
+ // Check Kitty
29
+ try {
30
+ execSync('which kitty', { stdio: 'pipe' });
31
+ terminals.push({ id: 'kitty', name: 'Kitty', available: true });
32
+ } catch {}
33
+ // Check Alacritty
34
+ try {
35
+ execSync('which alacritty', { stdio: 'pipe' });
36
+ terminals.push({ id: 'alacritty', name: 'Alacritty', available: true });
37
+ } catch {}
38
+ } else if (platform === 'linux') {
39
+ const linuxTerms = [
40
+ { id: 'gnome-terminal', name: 'GNOME Terminal', cmd: 'gnome-terminal' },
41
+ { id: 'konsole', name: 'Konsole', cmd: 'konsole' },
42
+ { id: 'kitty', name: 'Kitty', cmd: 'kitty' },
43
+ { id: 'alacritty', name: 'Alacritty', cmd: 'alacritty' },
44
+ { id: 'xterm', name: 'xterm', cmd: 'xterm' },
45
+ ];
46
+ for (const t of linuxTerms) {
47
+ try {
48
+ execSync(`which ${t.cmd}`, { stdio: 'pipe' });
49
+ terminals.push({ ...t, available: true });
50
+ } catch {
51
+ terminals.push({ ...t, available: false });
52
+ }
53
+ }
54
+ } else {
55
+ terminals.push({ id: 'cmd', name: 'Command Prompt', available: true });
56
+ terminals.push({ id: 'powershell', name: 'PowerShell', available: true });
57
+ try {
58
+ execSync('where wt', { stdio: 'pipe' });
59
+ terminals.push({ id: 'windows-terminal', name: 'Windows Terminal', available: true });
60
+ } catch {}
61
+ }
62
+
63
+ return terminals;
64
+ }
65
+
66
+ // ── Terminal launch ─────────────────────────────────────────
67
+
68
+ function openInTerminal(sessionId, tool, flags, projectDir, terminalId) {
69
+ const skipPerms = flags.includes('skip-permissions');
70
+ let cmd;
71
+
72
+ if (tool === 'codex') {
73
+ cmd = `codex --resume ${sessionId}`;
74
+ } else {
75
+ cmd = `claude --resume ${sessionId}`;
76
+ if (skipPerms) cmd += ' --dangerously-skip-permissions';
77
+ }
78
+
79
+ const cdPart = projectDir ? `cd ${JSON.stringify(projectDir)} && ` : '';
80
+ const fullCmd = cdPart + cmd;
81
+ const escapedCmd = fullCmd.replace(/"/g, '\\"');
82
+
83
+ const platform = process.platform;
84
+
85
+ if (platform === 'darwin') {
86
+ switch (terminalId) {
87
+ case 'terminal':
88
+ execSync(`osascript -e 'tell application "Terminal"
89
+ activate
90
+ do script "${escapedCmd}"
91
+ end tell'`);
92
+ break;
93
+ case 'warp':
94
+ execSync(`osascript -e 'tell application "Warp"
95
+ activate
96
+ end tell'`);
97
+ // Warp doesn't have great AppleScript support, use open
98
+ setTimeout(() => exec(`osascript -e 'tell application "System Events" to keystroke "${fullCmd}" & return'`), 500);
99
+ break;
100
+ case 'kitty':
101
+ exec(`kitty --single-instance bash -c '${fullCmd}; exec bash'`);
102
+ break;
103
+ case 'alacritty':
104
+ exec(`alacritty -e bash -c '${fullCmd}; exec bash'`);
105
+ break;
106
+ case 'iterm2':
107
+ default: {
108
+ const script = `
109
+ tell application "iTerm"
110
+ activate
111
+ set newWindow to (create window with default profile)
112
+ tell current session of newWindow
113
+ write text "${escapedCmd}"
114
+ end tell
115
+ end tell
116
+ `;
117
+ try {
118
+ execSync(`osascript -e '${script.replace(/'/g, "'\\''")}'`, { stdio: 'pipe' });
119
+ } catch {
120
+ // Fallback to Terminal.app
121
+ execSync(`osascript -e 'tell application "Terminal" to do script "${escapedCmd}"'`);
122
+ }
123
+ break;
124
+ }
125
+ }
126
+ } else if (platform === 'linux') {
127
+ switch (terminalId) {
128
+ case 'kitty':
129
+ exec(`kitty bash -c '${fullCmd}; exec bash'`);
130
+ break;
131
+ case 'alacritty':
132
+ exec(`alacritty -e bash -c '${fullCmd}; exec bash'`);
133
+ break;
134
+ case 'konsole':
135
+ exec(`konsole -e bash -c '${fullCmd}; exec bash'`);
136
+ break;
137
+ case 'xterm':
138
+ exec(`xterm -e bash -c '${fullCmd}; exec bash'`);
139
+ break;
140
+ case 'gnome-terminal':
141
+ default:
142
+ exec(`gnome-terminal -- bash -c "${fullCmd}; exec bash"`);
143
+ break;
144
+ }
145
+ } else {
146
+ switch (terminalId) {
147
+ case 'powershell':
148
+ exec(`start powershell -NoExit -Command "${fullCmd}"`);
149
+ break;
150
+ case 'windows-terminal':
151
+ exec(`wt new-tab cmd /k "${fullCmd}"`);
152
+ break;
153
+ default:
154
+ exec(`start cmd /k "${fullCmd}"`);
155
+ break;
156
+ }
157
+ }
158
+ }
159
+
160
+ module.exports = { detectTerminals, openInTerminal };