ccakashic 0.2.2 → 0.2.4
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/bin/ccakashic.js +132 -12
- package/package.json +1 -1
package/bin/ccakashic.js
CHANGED
|
@@ -3,12 +3,14 @@
|
|
|
3
3
|
|
|
4
4
|
const http = require('http');
|
|
5
5
|
const fs = require('fs');
|
|
6
|
+
const os = require('os');
|
|
6
7
|
const path = require('path');
|
|
7
8
|
const { exec } = require('child_process');
|
|
8
9
|
const { listProjects, listSessions, findSessionForCwd } = require('../lib/discover');
|
|
9
10
|
const { parseSession } = require('../lib/parser');
|
|
10
11
|
const { generate } = require('../lib/html-generator');
|
|
11
12
|
const { generateIndex, generateSessionList } = require('../lib/pages');
|
|
13
|
+
const pkg = require('../package.json');
|
|
12
14
|
|
|
13
15
|
function openInBrowser(url) {
|
|
14
16
|
const cmd = process.platform === 'darwin' ? 'open'
|
|
@@ -18,14 +20,22 @@ function openInBrowser(url) {
|
|
|
18
20
|
}
|
|
19
21
|
|
|
20
22
|
const PORT = parseInt(process.env.CCAKASHIC_PORT) || 3333;
|
|
23
|
+
const MAX_PORT_TRIES = 20;
|
|
24
|
+
const LOCK_FILE = path.join(os.tmpdir(), `ccakashic-${os.userInfo().username || 'user'}.json`);
|
|
21
25
|
|
|
22
26
|
const server = http.createServer(async (req, res) => {
|
|
23
27
|
try {
|
|
24
28
|
const url = new URL(req.url, `http://localhost`);
|
|
25
29
|
const pathname = url.pathname;
|
|
26
30
|
|
|
31
|
+
// Health/identity endpoint used to detect an already-running ccakashic
|
|
32
|
+
if (pathname === '/__ccakashic') {
|
|
33
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
34
|
+
res.end(JSON.stringify({ name: 'ccakashic', version: pkg.version }));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
27
38
|
if (pathname === '/' || pathname === '') {
|
|
28
|
-
// Project index
|
|
29
39
|
const projects = listProjects();
|
|
30
40
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
31
41
|
res.end(generateIndex(projects));
|
|
@@ -34,7 +44,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
34
44
|
|
|
35
45
|
const projectMatch = pathname.match(/^\/project\/(.+)$/);
|
|
36
46
|
if (projectMatch && !pathname.includes('/session/')) {
|
|
37
|
-
// Session list for a project
|
|
38
47
|
const rawName = decodeURIComponent(projectMatch[1]);
|
|
39
48
|
const projects = listProjects();
|
|
40
49
|
const project = projects.find(p => p.rawName === rawName);
|
|
@@ -51,7 +60,6 @@ const server = http.createServer(async (req, res) => {
|
|
|
51
60
|
|
|
52
61
|
const sessionMatch = pathname.match(/^\/project\/(.+)\/session\/(.+)$/);
|
|
53
62
|
if (sessionMatch) {
|
|
54
|
-
// Render a specific session
|
|
55
63
|
const rawName = decodeURIComponent(sessionMatch[1]);
|
|
56
64
|
const sessionId = decodeURIComponent(sessionMatch[2]);
|
|
57
65
|
const projects = listProjects();
|
|
@@ -85,21 +93,133 @@ const server = http.createServer(async (req, res) => {
|
|
|
85
93
|
}
|
|
86
94
|
});
|
|
87
95
|
|
|
88
|
-
|
|
89
|
-
const addr = server.address();
|
|
90
|
-
const url = `http://127.0.0.1:${addr.port}`;
|
|
91
|
-
console.log(`ccakashic running at ${url}`);
|
|
92
|
-
console.log('Press Ctrl+C to stop');
|
|
93
|
-
|
|
94
|
-
let openUrl = url;
|
|
96
|
+
async function buildOpenUrl(baseUrl) {
|
|
95
97
|
try {
|
|
96
98
|
const match = await findSessionForCwd(process.cwd());
|
|
97
99
|
if (match) {
|
|
98
|
-
openUrl = `${url}/project/${encodeURIComponent(match.projectRawName)}/session/${encodeURIComponent(match.sessionId)}#session-bottom`;
|
|
99
100
|
console.log(`Detected session for ${process.cwd()} → opening at bottom`);
|
|
101
|
+
return `${baseUrl}/project/${encodeURIComponent(match.projectRawName)}/session/${encodeURIComponent(match.sessionId)}#session-bottom`;
|
|
100
102
|
}
|
|
101
103
|
} catch (err) {
|
|
102
104
|
console.error('Failed to auto-detect session:', err.message);
|
|
103
105
|
}
|
|
104
|
-
|
|
106
|
+
return baseUrl;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function probeCcakashic(port) {
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
const req = http.request({
|
|
112
|
+
host: '127.0.0.1',
|
|
113
|
+
port,
|
|
114
|
+
path: '/__ccakashic',
|
|
115
|
+
method: 'GET',
|
|
116
|
+
timeout: 500,
|
|
117
|
+
}, (res) => {
|
|
118
|
+
let data = '';
|
|
119
|
+
res.on('data', chunk => { data += chunk; });
|
|
120
|
+
res.on('end', () => {
|
|
121
|
+
try {
|
|
122
|
+
const parsed = JSON.parse(data);
|
|
123
|
+
resolve(parsed && parsed.name === 'ccakashic');
|
|
124
|
+
} catch {
|
|
125
|
+
resolve(false);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
req.on('error', () => resolve(false));
|
|
130
|
+
req.on('timeout', () => { req.destroy(); resolve(false); });
|
|
131
|
+
req.end();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function readLockPort() {
|
|
136
|
+
try {
|
|
137
|
+
const data = JSON.parse(fs.readFileSync(LOCK_FILE, 'utf-8'));
|
|
138
|
+
return typeof data.port === 'number' ? data.port : null;
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function writeLockFile(port) {
|
|
145
|
+
try {
|
|
146
|
+
fs.writeFileSync(LOCK_FILE, JSON.stringify({ port, pid: process.pid, startedAt: Date.now() }));
|
|
147
|
+
} catch {
|
|
148
|
+
// best-effort
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function cleanupLockFile() {
|
|
153
|
+
try { fs.unlinkSync(LOCK_FILE); } catch {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function listenOnPort(port) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const onError = (err) => { server.off('listening', onListening); reject(err); };
|
|
159
|
+
const onListening = () => { server.off('error', onError); resolve(); };
|
|
160
|
+
server.once('error', onError);
|
|
161
|
+
server.once('listening', onListening);
|
|
162
|
+
server.listen(port, '127.0.0.1');
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function findExistingCcakashic(startPort) {
|
|
167
|
+
const lockPort = readLockPort();
|
|
168
|
+
if (lockPort && await probeCcakashic(lockPort)) return lockPort;
|
|
169
|
+
if (startPort !== lockPort && await probeCcakashic(startPort)) return startPort;
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function startServer(startPort) {
|
|
174
|
+
for (let i = 0; i < MAX_PORT_TRIES; i++) {
|
|
175
|
+
const port = startPort + i;
|
|
176
|
+
try {
|
|
177
|
+
await listenOnPort(port);
|
|
178
|
+
return port;
|
|
179
|
+
} catch (err) {
|
|
180
|
+
if (err.code !== 'EADDRINUSE') throw err;
|
|
181
|
+
// Port is taken by something else; see if it's ccakashic
|
|
182
|
+
if (await probeCcakashic(port)) return -port; // negative = reuse signal
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
throw new Error(`No available port after ${MAX_PORT_TRIES} tries starting at ${startPort}`);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function main() {
|
|
189
|
+
const existing = await findExistingCcakashic(PORT);
|
|
190
|
+
if (existing) {
|
|
191
|
+
const url = `http://127.0.0.1:${existing}`;
|
|
192
|
+
console.log(`Reusing existing ccakashic at ${url}`);
|
|
193
|
+
writeLockFile(existing);
|
|
194
|
+
openInBrowser(await buildOpenUrl(url));
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const result = await startServer(PORT);
|
|
199
|
+
if (result < 0) {
|
|
200
|
+
const port = -result;
|
|
201
|
+
const url = `http://127.0.0.1:${port}`;
|
|
202
|
+
console.log(`Reusing existing ccakashic at ${url}`);
|
|
203
|
+
writeLockFile(port);
|
|
204
|
+
openInBrowser(await buildOpenUrl(url));
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const port = result;
|
|
209
|
+
const url = `http://127.0.0.1:${port}`;
|
|
210
|
+
console.log(`ccakashic running at ${url}`);
|
|
211
|
+
console.log('Press Ctrl+C to stop');
|
|
212
|
+
writeLockFile(port);
|
|
213
|
+
|
|
214
|
+
const cleanup = () => { cleanupLockFile(); process.exit(0); };
|
|
215
|
+
process.on('SIGINT', cleanup);
|
|
216
|
+
process.on('SIGTERM', cleanup);
|
|
217
|
+
process.on('exit', cleanupLockFile);
|
|
218
|
+
|
|
219
|
+
openInBrowser(await buildOpenUrl(url));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
main().catch((err) => {
|
|
223
|
+
console.error(err);
|
|
224
|
+
process.exit(1);
|
|
105
225
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ccakashic",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "Browse Claude Code session logs (~/.claude/projects/) as beautiful HTML in your browser — an Akashic Record of your Claude Code sessions",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ccakashic": "bin/ccakashic.js"
|