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/LICENSE +21 -0
- package/README.md +62 -0
- package/bin/cli.js +93 -0
- package/package.json +36 -0
- package/src/data.js +284 -0
- package/src/frontend/app.js +1048 -0
- package/src/frontend/index.html +113 -0
- package/src/frontend/styles.css +1385 -0
- package/src/html.js +26 -0
- package/src/server.js +142 -0
- package/src/terminals.js +160 -0
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 };
|
package/src/terminals.js
ADDED
|
@@ -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 };
|