cc-viewer 1.8.7 → 1.8.9

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/dist/index.html CHANGED
@@ -21,7 +21,7 @@
21
21
  // 整体显示大小已弃用 CSS zoom:Electron 改用 webFrame.setZoomFactor(首屏抢占见
22
22
  // electron/tab-content-preload.js),纯浏览器交由用户用浏览器自带快捷键缩放,故此处不再设 zoom。
23
23
  </script>
24
- <script type="module" crossorigin src="./assets/index-gBheYuc_.js"></script>
24
+ <script type="module" crossorigin src="./assets/index-BbF89uTe.js"></script>
25
25
  <link rel="modulepreload" crossorigin href="./assets/vendor-antd-FhK5cQx7.js">
26
26
  <link rel="modulepreload" crossorigin href="./assets/vendor-virtuoso-B_7lc2Ot.js">
27
27
  <link rel="modulepreload" crossorigin href="./assets/vendor-codemirror-DL3yyKRQ.js">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-viewer",
3
- "version": "1.8.7",
3
+ "version": "1.8.9",
4
4
  "description": "Claude Code logging, visualization, and management toolkit — launch a web viewer alongside Claude Code with full request/response tracing, proxy, and mobile support",
5
5
  "license": "MIT",
6
6
  "main": "server.js",
@@ -1,8 +1,9 @@
1
1
  // Read-oriented file-access routes (moved verbatim from server.js handleRequest).
2
- import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, realpathSync } from 'node:fs';
2
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync, realpathSync, createReadStream } from 'node:fs';
3
3
  import { join, basename, dirname, resolve, sep } from 'node:path';
4
4
  import { isReadAllowed, reasonToStatus } from '../lib/file-access-policy.js';
5
5
  import { ERROR_STATUS_MAP } from '../lib/file-api.js';
6
+ import { reportSwallowed } from '@ccv/core/error-report';
6
7
  import { discoverClaudeMdCandidates, readCandidateById } from '../lib/claude-md-discovery.js';
7
8
  import { getClaudeConfigDir } from '../../findcc.js';
8
9
  import { _projectName } from '../interceptor.js';
@@ -329,6 +330,70 @@ function fileRaw(req, res, parsedUrl) {
329
330
  }
330
331
  }
331
332
 
333
+ // Browser download endpoint: unlike fileRaw (inline preview, 10MB cap), this streams
334
+ // the file with Content-Disposition: attachment so the browser saves it to disk.
335
+ // No size cap — a download is precisely the path for files too large to preview.
336
+ function fileDownload(req, res, parsedUrl) {
337
+ const reqPath = parsedUrl.searchParams.get('path');
338
+ const cwd = process.env.CCV_PROJECT_DIR || process.cwd();
339
+ try {
340
+ if (!reqPath) {
341
+ res.writeHead(400, { 'Content-Type': 'application/json' });
342
+ res.end(JSON.stringify({ error: 'Invalid path' }));
343
+ return;
344
+ }
345
+ // Same path contract as fileContentGet: relative paths with .. are rejected outright
346
+ const isAbs = /^([a-zA-Z]:[\\/]|[\\/])/.test(reqPath);
347
+ if (!isAbs && reqPath.includes('..')) {
348
+ res.writeHead(400, { 'Content-Type': 'application/json' });
349
+ res.end(JSON.stringify({ error: 'Invalid path' }));
350
+ return;
351
+ }
352
+ const absPath = isAbs ? reqPath : resolve(cwd, reqPath);
353
+ const policy = isReadAllowed(absPath);
354
+ if (!policy.ok) {
355
+ const status = reasonToStatus(policy.reason);
356
+ const errLabel = status === 404 ? 'File not found'
357
+ : status === 400 ? 'Invalid path'
358
+ : 'Forbidden';
359
+ const body = { error: errLabel, reason: policy.reason };
360
+ if (policy.allowedRoots) body.allowedRoots = policy.allowedRoots;
361
+ res.writeHead(status, { 'Content-Type': 'application/json' });
362
+ res.end(JSON.stringify(body));
363
+ return;
364
+ }
365
+ const targetFile = policy.real;
366
+ const stat = statSync(targetFile);
367
+ if (!stat.isFile()) {
368
+ res.writeHead(400, { 'Content-Type': 'application/json' });
369
+ res.end(JSON.stringify({ error: 'Not a file' }));
370
+ return;
371
+ }
372
+ const headers = {
373
+ 'Content-Type': 'application/octet-stream',
374
+ 'Content-Length': stat.size,
375
+ // RFC 5987 encoding keeps non-ASCII filenames intact (same style as download-log)
376
+ 'Content-Disposition': `attachment; filename*=UTF-8''${encodeURIComponent(basename(targetFile))}`,
377
+ };
378
+ if (req.method === 'HEAD') {
379
+ res.writeHead(200, headers);
380
+ res.end();
381
+ return;
382
+ }
383
+ res.writeHead(200, headers);
384
+ const stream = createReadStream(targetFile);
385
+ // Headers are already sent by the time any read error can fire, so we can only
386
+ // abort the response mid-download (client sees a truncated file) — report it.
387
+ stream.on('error', (err) => { reportSwallowed('fileDownload.stream', err); res.end(); });
388
+ stream.pipe(res);
389
+ } catch (err) {
390
+ const status = ERROR_STATUS_MAP[err.code] || 500;
391
+ const message = status === 500 ? `Cannot read file: ${err.message}` : err.message;
392
+ res.writeHead(status, { 'Content-Type': 'application/json' });
393
+ res.end(JSON.stringify({ error: message }));
394
+ }
395
+ }
396
+
332
397
  function fileContentPost(req, res) {
333
398
  const MAX_BODY = 5 * 1024 * 1024; // 5MB,与 GET 路由限制对齐
334
399
  let body = '';
@@ -427,5 +492,6 @@ export const filesContentRoutes = [
427
492
  { method: 'GET', match: 'exact', path: '/api/project-memory', handler: projectMemory },
428
493
  { method: 'GET', match: 'exact', path: '/api/claude-md', handler: claudeMd },
429
494
  { predicate: (url, method) => (url === '/api/file-raw' || url.startsWith('/api/file-raw/')) && (method === 'GET' || method === 'HEAD'), handler: fileRaw },
495
+ { predicate: (url, method) => url === '/api/download-file' && (method === 'GET' || method === 'HEAD'), handler: fileDownload },
430
496
  { method: 'POST', match: 'exact', path: '/api/file-content', handler: fileContentPost },
431
497
  ];