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.
Files changed (2) hide show
  1. package/bin/ccakashic.js +132 -12
  2. 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
- server.listen(PORT, '127.0.0.1', async () => {
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
- openInBrowser(openUrl);
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.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"