cc-viewer 1.6.274 → 1.6.276
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/README.md +11 -0
- package/cli.js +29 -0
- package/dist/assets/App-VfOsEOzR.css +1 -0
- package/dist/assets/App-g0FMGNbK.js +1 -0
- package/dist/assets/{MdxEditorPanel-DvqnmX-m.css → MdxEditorPanel-1vSx6ymU.css} +1 -1
- package/dist/assets/{MdxEditorPanel-BAnfnKiF.js → MdxEditorPanel-lkBOg8Jh.js} +1 -1
- package/dist/assets/Mobile-CfP2RFHS.js +1 -0
- package/dist/assets/{_baseUniq-CPJrFyUF.js → _baseUniq-IhLiJsz9.js} +1 -1
- package/dist/assets/{arc-BLBrFElt.js → arc-CI5WB-FW.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-CbnBsMiQ.js → architectureDiagram-Q4EWVU46-YOnqcO8r.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-0mYr6-Fl.js → blockDiagram-DXYQGD6D-B6oK5mZJ.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-CS7vcr0z.js → c4Diagram-AHTNJAMY-JsynU_tx.js} +1 -1
- package/dist/assets/{channel-CF3zZzSR.js → channel-DLsvHA9V.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-1FZYtnJ7.js → chunk-4BX2VUAB-DGhBdIp1.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-COs1qui5.js → chunk-4TB4RGXK-crHwtw-n.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-p77Qw3wN.js → chunk-55IACEB6-CHTfiCuV.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-5qaIrQKg.js → chunk-EDXVE4YY-buLXKlmZ.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DmCR8mDZ.js → chunk-FMBD7UC4-DSfXNwCE.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-D6xDfgW3.js → chunk-OYMX7WX6-16K1lWZl.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-B6cmUU0N.js → chunk-QZHKN3VN-DgDuGTt2.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-l-OyOqnn.js → chunk-YZCP3GAM-DHqswL_6.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BCPnChlV.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-BCPnChlV.js +1 -0
- package/dist/assets/clone-CJyRHxRw.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-CEzaS8XS.js → cose-bilkent-S5V4N54A-D1j0CqN7.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-B3U2njWW.js → dagre-KV5264BT-XKbpcECY.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-BRSDbyBr.js → diagram-5BDNPKRD-CcbZUyrk.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-BZfOi9B7.js → diagram-G4DWMVQ6-UxGWOz8H.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-CWpb3Cg0.js → diagram-MMDJMWI5-COG8DLtM.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-CTJyBSVj.js → diagram-TYMM5635-_dGuaThe.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-CHtTRd5S.js → erDiagram-SMLLAGMA-CNu-hiRP.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-Bda8X-WJ.js → flowDiagram-DWJPFMVM-DQHinAg1.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CPfnGu5V.js → ganttDiagram-T4ZO3ILL-BAySDtwF.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-B5QxesQg.js → gitGraphDiagram-UUTBAWPF-BBO_XdYa.js} +1 -1
- package/dist/assets/{graph-ChltdhTU.js → graph-D9285_n5.js} +1 -1
- package/dist/assets/index-0hGZ2hk-.js +2 -0
- package/dist/assets/{index-BK4sui_O.js → index-6IDcGlHG.js} +1 -1
- package/dist/assets/{index-C2fhupP6.js → index-CXhN926Q.js} +1 -1
- package/dist/assets/{index-CfEkC3bc.js → index-CaKHIO3v.js} +1 -1
- package/dist/assets/{index-yClPXlMf.js → index-D18bphjK.js} +1 -1
- package/dist/assets/{index-B7lK5fJz.js → index-DTi4w1dY.js} +1 -1
- package/dist/assets/{index-C5PA4OJg.js → index-gSNq4ykV.js} +1 -1
- package/dist/assets/{index-DyGa-jNv.js → index-pEgUz6fU.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-CD4TS4O2.js → infoDiagram-42DDH7IO-DODFGD-6.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-jhiYabQ7.js → ishikawaDiagram-UXIWVN3A-kn6kHfYH.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-BDzIXxxt.js → journeyDiagram-VCZTEJTY-BRaTTmzg.js} +1 -1
- package/dist/assets/{jszip.min-CuGGBMI4.js → jszip.min-CnLSX52a.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-D34MYATQ.js → kanban-definition-6JOO6SKY-CzLQb2u1.js} +1 -1
- package/dist/assets/{layout-D6sLAapX.js → layout-B9TjxURJ.js} +1 -1
- package/dist/assets/{linear-CFV4P-wn.js → linear-Dpck4eFk.js} +1 -1
- package/dist/assets/{mermaid.core-YmJi7T-s.js → mermaid.core-Dd_GyvDR.js} +2 -2
- package/dist/assets/{min-akJQqRMn.js → min-CUkc58GS.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-w5zaJyrN.js → mindmap-definition-QFDTVHPH-BteQHO2X.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-B6EM4Ow6.js → pieDiagram-DEJITSTG-CaOYrVcc.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-2TmBxFy-.js → quadrantDiagram-34T5L4WZ-DTcmYcmQ.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-EOjvbxUy.js → requirementDiagram-MS252O5E-CY8WoU0d.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-BmTQD5eT.js → sankeyDiagram-XADWPNL6-9NPSQPLx.js} +1 -1
- package/dist/assets/seqResourceLoaders-BgEHTs14.js +2 -0
- package/dist/assets/seqResourceLoaders-DPmXT2Hh.css +41 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-CLjtqA1D.js → sequenceDiagram-FGHM5R23-e7V5HhHU.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-Cbq90iZ8.js → stateDiagram-FHFEXIEX-B6R9nxzj.js} +1 -1
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-sUx4g5Jk.js +1 -0
- package/dist/assets/{timeline-definition-GMOUNBTQ-f3kEDay0.js → timeline-definition-GMOUNBTQ-CqpCMRBB.js} +1 -1
- package/dist/assets/{vendor-antd-5xE7sz6B.js → vendor-antd-BqzW9CXo.js} +2 -2
- package/dist/assets/{vendor-codemirror-ib-jPbXC.js → vendor-codemirror-B5WQ33Y0.js} +1 -1
- package/dist/assets/vendor-markdown-DOJHsAxX.js +3 -0
- package/dist/assets/{vendor-mdxeditor-BdKMdw6O.js → vendor-mdxeditor-DwzTmNBd.js} +2 -2
- package/dist/assets/{vendor-qrcode-vKlE-WYu.js → vendor-qrcode-C9ZMcovz.js} +1 -1
- package/dist/assets/{vendor-virtuoso-DOIfjLfU.js → vendor-virtuoso-CQj5-1oy.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-0GDSFVnH.js → vennDiagram-DHZGUBPP-CxzTP1Mu.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-B2rj5j7G.js → wardley-RL74JXVD-_Dcq-0iY.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-COVTciJP.js → wardleyDiagram-NUSXRM2D-CltoIcVL.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-DXoLnGxX.js → xychartDiagram-5P7HB3ND-DXKa-Cxe.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +1 -1
- package/server/i18n.js +186 -2
- package/server/lib/auth.js +348 -0
- package/server/lib/voice-pack-events.js +2 -2
- package/server/routes/_dispatch.js +43 -0
- package/server/routes/ask-perm.js +452 -0
- package/server/routes/auth.js +166 -0
- package/server/routes/events.js +335 -0
- package/server/routes/files-content.js +421 -0
- package/server/routes/files-fs.js +955 -0
- package/server/routes/git.js +228 -0
- package/server/routes/logs.js +201 -0
- package/server/routes/misc.js +22 -0
- package/server/routes/plugins.js +96 -0
- package/server/routes/preferences.js +216 -0
- package/server/routes/project-meta.js +118 -0
- package/server/routes/skills.js +261 -0
- package/server/routes/team.js +169 -0
- package/server/routes/voice-pack.js +235 -0
- package/server/routes/workspaces.js +171 -0
- package/server/server.js +340 -3953
- package/dist/assets/App-BediOgt6.js +0 -1
- package/dist/assets/App-TGGslOeT.css +0 -1
- package/dist/assets/Mobile-C_nqfEXb.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BiCYgTHO.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-BiCYgTHO.js +0 -1
- package/dist/assets/clone-ChTCnPsO.js +0 -1
- package/dist/assets/index-CuE3VwXB.js +0 -2
- package/dist/assets/seqResourceLoaders-CgNKpehN.js +0 -2
- package/dist/assets/seqResourceLoaders-Dd_x1M9-.css +0 -41
- package/dist/assets/stateDiagram-v2-QKLJ7IA2-BKDg-3t_.js +0 -1
- package/dist/assets/vendor-markdown-BFrYfpb0.js +0 -1
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// Git routes (moved verbatim from server.js handleRequest).
|
|
2
|
+
import { existsSync, readdirSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join, basename, resolve, sep } from 'node:path';
|
|
4
|
+
import { getGitDiffs, countUntrackedLines, getUnpushedCommits, isValidCommitHash } from '../lib/git-diff.js';
|
|
5
|
+
|
|
6
|
+
const GIT_RESTORE_LOCK_CLEANUP_MS = 30000;
|
|
7
|
+
|
|
8
|
+
function gitRepos(req, res) {
|
|
9
|
+
try {
|
|
10
|
+
const projectDir = process.env.CCV_PROJECT_DIR || process.cwd();
|
|
11
|
+
const repos = [];
|
|
12
|
+
if (existsSync(join(projectDir, '.git'))) {
|
|
13
|
+
repos.push({ name: basename(projectDir), path: '.', isRoot: true });
|
|
14
|
+
}
|
|
15
|
+
const entries = readdirSync(projectDir, { withFileTypes: true });
|
|
16
|
+
for (const entry of entries) {
|
|
17
|
+
if (!entry.isDirectory()) continue;
|
|
18
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
19
|
+
try {
|
|
20
|
+
if (existsSync(join(projectDir, entry.name, '.git'))) {
|
|
21
|
+
repos.push({ name: entry.name, path: entry.name, isRoot: false });
|
|
22
|
+
}
|
|
23
|
+
} catch {}
|
|
24
|
+
}
|
|
25
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
26
|
+
res.end(JSON.stringify({ repos }));
|
|
27
|
+
} catch (err) {
|
|
28
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
29
|
+
res.end(JSON.stringify({ error: err.message, repos: [] }));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function gitRestore(req, res, parsedUrl, isLocal, deps) {
|
|
34
|
+
let body = '';
|
|
35
|
+
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
36
|
+
req.on('end', async () => {
|
|
37
|
+
let parsed;
|
|
38
|
+
try { parsed = JSON.parse(body); } catch {
|
|
39
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
40
|
+
res.end(JSON.stringify({ error: 'Invalid request body' }));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const { path: filePath, repo: repoParam } = parsed;
|
|
45
|
+
if (!filePath) {
|
|
46
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
47
|
+
res.end(JSON.stringify({ error: 'Missing path' }));
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
if (filePath.startsWith('/') || filePath.includes('..')) {
|
|
51
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
52
|
+
res.end(JSON.stringify({ error: 'Invalid path' }));
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const cwd = deps.resolveRepoCwd(repoParam);
|
|
56
|
+
if (!cwd) {
|
|
57
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
58
|
+
res.end(JSON.stringify({ error: 'Invalid repo parameter' }));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const fullPath = join(cwd, filePath);
|
|
62
|
+
if (existsSync(fullPath)) {
|
|
63
|
+
const realFull = realpathSync(fullPath);
|
|
64
|
+
const realCwd = realpathSync(cwd);
|
|
65
|
+
if (!realFull.startsWith(realCwd + sep)) {
|
|
66
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
67
|
+
res.end(JSON.stringify({ error: 'Path traversal not allowed' }));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// Per-file mutex 序列化「git status + checkout/clean」子命令对。多 tab 并发同文件
|
|
72
|
+
// revert 时不再有 race 让两个 checkout 交错执行(最终状态不可预测)。
|
|
73
|
+
// resolve() 规整 `./foo.js` / `foo.js` / `.//foo.js` 为同一 lockKey,防形变绕过。
|
|
74
|
+
const lockKey = resolve(join(cwd, filePath));
|
|
75
|
+
const prev = deps.gitRestoreLocks.get(lockKey) || Promise.resolve();
|
|
76
|
+
const current = prev.then(async () => {
|
|
77
|
+
const { stdout: statusOut } = await deps.execFileAsync('git', ['status', '--porcelain', '--', filePath], { cwd, encoding: 'utf-8', timeout: 5000 });
|
|
78
|
+
const isUntracked = statusOut.trim().startsWith('??');
|
|
79
|
+
if (isUntracked) {
|
|
80
|
+
await deps.execFileAsync('git', ['clean', '-fd', '--', filePath], { cwd, timeout: 10000 });
|
|
81
|
+
} else {
|
|
82
|
+
await deps.execFileAsync('git', ['checkout', '--', filePath], { cwd, timeout: 10000 });
|
|
83
|
+
}
|
|
84
|
+
}).finally(() => {
|
|
85
|
+
if (deps.gitRestoreLocks.get(lockKey) === current) deps.gitRestoreLocks.delete(lockKey);
|
|
86
|
+
});
|
|
87
|
+
deps.gitRestoreLocks.set(lockKey, current);
|
|
88
|
+
// setTimeout 兜底——防 finally 异常吞 entry 累积内存。
|
|
89
|
+
setTimeout(() => {
|
|
90
|
+
if (deps.gitRestoreLocks.get(lockKey) === current) deps.gitRestoreLocks.delete(lockKey);
|
|
91
|
+
}, GIT_RESTORE_LOCK_CLEANUP_MS).unref();
|
|
92
|
+
await current;
|
|
93
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
94
|
+
res.end(JSON.stringify({ ok: true }));
|
|
95
|
+
} catch (err) {
|
|
96
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
97
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function gitStatus(req, res, parsedUrl, isLocal, deps) {
|
|
103
|
+
try {
|
|
104
|
+
const repoParam = parsedUrl.searchParams.get('repo');
|
|
105
|
+
const cwd = deps.resolveRepoCwd(repoParam);
|
|
106
|
+
if (!cwd) {
|
|
107
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
108
|
+
res.end(JSON.stringify({ error: 'Invalid repo parameter', changes: [] }));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// `-uall` 让 git 把新增目录展开成具体文件,而不是收敛为 `?? path/`
|
|
112
|
+
// 否则前端树会把整个新目录当一个「空文件名」叶子渲染,且行数统计为 0。
|
|
113
|
+
// maxBuffer 拉到 10MB——默认 1MB 在 node_modules 未 gitignore 之类的极端
|
|
114
|
+
// 场景下会被截断,导致后续 split 解析错位。
|
|
115
|
+
const { stdout: output } = await deps.execFileAsync('git', ['status', '--porcelain', '-uall'], { cwd, encoding: 'utf-8', timeout: 5000, maxBuffer: 10 * 1024 * 1024 });
|
|
116
|
+
// Win 上 git stdout 是 CRLF;现状靠下文 .trim() 兜底,加正则更稳防未来 strict 比较破窗。
|
|
117
|
+
const lines = output.split(/\r?\n/).filter(line => line.trim());
|
|
118
|
+
const changes = lines.map(line => {
|
|
119
|
+
const status = line.substring(0, 2).trim();
|
|
120
|
+
let file = line.substring(3).trim();
|
|
121
|
+
// git status --porcelain quotes paths with non-ASCII chars using octal escapes
|
|
122
|
+
if (file.startsWith('"') && file.endsWith('"')) {
|
|
123
|
+
file = file.slice(1, -1)
|
|
124
|
+
.replace(/\\([0-7]{3})/g, (_, oct) => String.fromCharCode(parseInt(oct, 8)))
|
|
125
|
+
.replace(/\\t/g, '\t').replace(/\\n/g, '\n')
|
|
126
|
+
.replace(/\\\\/g, '\\').replace(/\\"/g, '"');
|
|
127
|
+
file = Buffer.from(file, 'latin1').toString('utf8');
|
|
128
|
+
}
|
|
129
|
+
return { status, file };
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Collect per-file insertions/deletions via git diff --numstat (tracked) + --cached --numstat (staged).
|
|
133
|
+
// Neither covers untracked files, so add their line counts separately via countUntrackedLines
|
|
134
|
+
// — matching git's numstat semantics (binary and >5MB files contribute 0).
|
|
135
|
+
let insertions = 0, deletions = 0;
|
|
136
|
+
try {
|
|
137
|
+
const [{ stdout: numstat }, { stdout: cachedNumstat }] = await Promise.all([
|
|
138
|
+
deps.execFileAsync('git', ['diff', '--numstat'], { cwd, encoding: 'utf-8', timeout: 5000 }),
|
|
139
|
+
deps.execFileAsync('git', ['diff', '--cached', '--numstat'], { cwd, encoding: 'utf-8', timeout: 5000 }),
|
|
140
|
+
]);
|
|
141
|
+
for (const raw of [numstat, cachedNumstat]) {
|
|
142
|
+
for (const l of raw.split('\n')) {
|
|
143
|
+
const m = l.match(/^(\d+)\t(\d+)\t/);
|
|
144
|
+
if (m) { insertions += Number(m[1]); deletions += Number(m[2]); }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
} catch { /* non-critical — stats just stay 0 */ }
|
|
148
|
+
|
|
149
|
+
// Cap untracked-file processing to keep the event loop responsive if a
|
|
150
|
+
// repo forgets to gitignore a huge directory (e.g. node_modules).
|
|
151
|
+
// 超限时仍继续计数,但不再调 countUntrackedLines——用 insertions_capped
|
|
152
|
+
// 通知前端"此数据被硬上限截断",避免静默少算给用户造成误判。
|
|
153
|
+
const MAX_UNTRACKED = 5000;
|
|
154
|
+
let untrackedProcessed = 0;
|
|
155
|
+
let untrackedTotal = 0;
|
|
156
|
+
for (const c of changes) {
|
|
157
|
+
if (c.status !== '??') continue;
|
|
158
|
+
untrackedTotal++;
|
|
159
|
+
if (untrackedProcessed >= MAX_UNTRACKED) continue;
|
|
160
|
+
insertions += countUntrackedLines(cwd, c.file);
|
|
161
|
+
untrackedProcessed++;
|
|
162
|
+
}
|
|
163
|
+
const insertions_capped = untrackedTotal > MAX_UNTRACKED;
|
|
164
|
+
|
|
165
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
166
|
+
res.end(JSON.stringify({ changes, insertions, deletions, insertions_capped }));
|
|
167
|
+
} catch (err) {
|
|
168
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
169
|
+
res.end(JSON.stringify({ error: err.message, changes: [] }));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function gitDiff(req, res, parsedUrl, isLocal, deps) {
|
|
174
|
+
try {
|
|
175
|
+
const repoParam = parsedUrl.searchParams.get('repo');
|
|
176
|
+
const cwd = deps.resolveRepoCwd(repoParam);
|
|
177
|
+
if (!cwd) {
|
|
178
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
179
|
+
res.end(JSON.stringify({ error: 'Invalid repo parameter', diffs: [] }));
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const filesParam = parsedUrl.searchParams.get('files');
|
|
183
|
+
|
|
184
|
+
if (!filesParam) {
|
|
185
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
186
|
+
res.end(JSON.stringify({ error: 'Missing files parameter' }));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const files = filesParam.split(',').map(f => f.trim()).filter(Boolean);
|
|
191
|
+
const commitParam = parsedUrl.searchParams.get('commit');
|
|
192
|
+
// Reject malformed commit hashes; falsy = working-tree mode
|
|
193
|
+
const commitHash = commitParam && isValidCommitHash(commitParam) ? commitParam : undefined;
|
|
194
|
+
const diffs = await getGitDiffs(cwd, files, commitHash);
|
|
195
|
+
|
|
196
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
197
|
+
res.end(JSON.stringify({ diffs }));
|
|
198
|
+
} catch (err) {
|
|
199
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
200
|
+
res.end(JSON.stringify({ error: err.message, diffs: [] }));
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
async function gitLogUnpushed(req, res, parsedUrl, isLocal, deps) {
|
|
205
|
+
try {
|
|
206
|
+
const repoParam = parsedUrl.searchParams.get('repo');
|
|
207
|
+
const cwd = deps.resolveRepoCwd(repoParam);
|
|
208
|
+
if (!cwd) {
|
|
209
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
210
|
+
res.end(JSON.stringify({ error: 'Invalid repo parameter', commits: [], hasUpstream: false }));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const result = await getUnpushedCommits(cwd);
|
|
214
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
215
|
+
res.end(JSON.stringify(result));
|
|
216
|
+
} catch (err) {
|
|
217
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
218
|
+
res.end(JSON.stringify({ error: err.message, commits: [], hasUpstream: false }));
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export const gitRoutes = [
|
|
223
|
+
{ method: 'GET', match: 'exact', path: '/api/git-repos', handler: gitRepos },
|
|
224
|
+
{ method: 'POST', match: 'exact', path: '/api/git-restore', handler: gitRestore },
|
|
225
|
+
{ method: 'GET', match: 'exact', path: '/api/git-status', handler: gitStatus },
|
|
226
|
+
{ method: 'GET', match: 'prefix', path: '/api/git-diff', handler: gitDiff },
|
|
227
|
+
{ method: 'GET', match: 'prefix', path: '/api/git-log-unpushed', handler: gitLogUnpushed },
|
|
228
|
+
];
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Local log management routes (moved verbatim from server.js handleRequest).
|
|
2
|
+
import { existsSync, realpathSync, statSync, createReadStream } from 'node:fs';
|
|
3
|
+
import { join, basename } from 'node:path';
|
|
4
|
+
import { LOG_DIR } from '../../findcc.js';
|
|
5
|
+
import { _projectName } from '../interceptor.js';
|
|
6
|
+
import { listLocalLogs, deleteLogFiles, mergeLogFiles, archiveLogFiles, validateLogPath } from '../lib/log-management.js';
|
|
7
|
+
import { countLogEntries, streamRawEntriesAsync } from '../lib/log-stream.js';
|
|
8
|
+
|
|
9
|
+
function localLogs(req, res) {
|
|
10
|
+
try {
|
|
11
|
+
const result = listLocalLogs(LOG_DIR, _projectName);
|
|
12
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
13
|
+
res.end(JSON.stringify(result));
|
|
14
|
+
} catch (err) {
|
|
15
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
16
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function downloadLog(req, res, parsedUrl) {
|
|
21
|
+
const file = parsedUrl.searchParams.get('file');
|
|
22
|
+
if (!file || file.includes('..')) {
|
|
23
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
24
|
+
res.end(JSON.stringify({ error: 'Invalid file name' }));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (!file.endsWith('.jsonl') && !file.endsWith('.jsonl.zip')) {
|
|
28
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
29
|
+
res.end(JSON.stringify({ error: 'Invalid file type' }));
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const filePath = join(LOG_DIR, file);
|
|
33
|
+
try {
|
|
34
|
+
if (!existsSync(filePath)) {
|
|
35
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
36
|
+
res.end(JSON.stringify({ error: 'File not found' }));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const realPath = realpathSync(filePath);
|
|
40
|
+
const realLogDir = realpathSync(LOG_DIR);
|
|
41
|
+
if (!realPath.startsWith(realLogDir)) {
|
|
42
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
43
|
+
res.end(JSON.stringify({ error: 'Access denied' }));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const format = parsedUrl.searchParams.get('format');
|
|
47
|
+
// 默认下载重建后的 .jsonl;raw 下载原始字节(.zip 或 .jsonl 视实际而定)
|
|
48
|
+
const fileName = (format !== 'raw' && file.endsWith('.jsonl.zip'))
|
|
49
|
+
? basename(file).slice(0, -4)
|
|
50
|
+
: basename(file);
|
|
51
|
+
// Delta storage: format=raw 下载原始文件;默认下载重建后的全量格式
|
|
52
|
+
if (format === 'raw') {
|
|
53
|
+
const stat = statSync(realPath);
|
|
54
|
+
res.writeHead(200, {
|
|
55
|
+
'Content-Type': 'application/octet-stream',
|
|
56
|
+
'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
|
|
57
|
+
'Content-Length': stat.size,
|
|
58
|
+
});
|
|
59
|
+
const stream = createReadStream(realPath);
|
|
60
|
+
stream.pipe(res);
|
|
61
|
+
} else {
|
|
62
|
+
// 流式下载原始条目(不重建,保持 delta 格式),避免 OOM
|
|
63
|
+
res.writeHead(200, {
|
|
64
|
+
'Content-Type': 'application/octet-stream',
|
|
65
|
+
'Content-Disposition': `attachment; filename="${encodeURIComponent(fileName)}"`,
|
|
66
|
+
'Transfer-Encoding': 'chunked',
|
|
67
|
+
});
|
|
68
|
+
await streamRawEntriesAsync(realPath, (raw) => {
|
|
69
|
+
res.write(raw);
|
|
70
|
+
res.write('\n---\n');
|
|
71
|
+
});
|
|
72
|
+
res.end();
|
|
73
|
+
}
|
|
74
|
+
} catch (err) {
|
|
75
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
76
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function localLog(req, res, parsedUrl) {
|
|
81
|
+
const file = parsedUrl.searchParams.get('file');
|
|
82
|
+
if (!file || file.includes('..')) {
|
|
83
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
84
|
+
res.end(JSON.stringify({ error: 'Invalid file name' }));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 验证文件类型:允许 .jsonl 或 .jsonl.zip
|
|
89
|
+
if (!file.endsWith('.jsonl') && !file.endsWith('.jsonl.zip')) {
|
|
90
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
91
|
+
res.end(JSON.stringify({ error: 'Invalid file type. Only .jsonl(.zip) files are allowed.' }));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
// 独立 SSE 流:直接向请求方返回 event-stream,不走 /events 广播
|
|
97
|
+
validateLogPath(LOG_DIR, file);
|
|
98
|
+
const filePath = join(LOG_DIR, file);
|
|
99
|
+
const total = countLogEntries(filePath);
|
|
100
|
+
|
|
101
|
+
res.writeHead(200, {
|
|
102
|
+
'Content-Type': 'text/event-stream',
|
|
103
|
+
'Cache-Control': 'no-cache',
|
|
104
|
+
'Connection': 'keep-alive',
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
res.write(`event: load_start\ndata: ${JSON.stringify({ total, incremental: false })}\n\n`);
|
|
108
|
+
await streamRawEntriesAsync(filePath, (raw) => {
|
|
109
|
+
res.write('event: load_chunk\ndata: [');
|
|
110
|
+
res.write(raw.includes('\n') ? raw.replace(/\n/g, '') : raw);
|
|
111
|
+
res.write(']\n\n');
|
|
112
|
+
});
|
|
113
|
+
res.write(`event: load_end\ndata: {}\n\n`);
|
|
114
|
+
res.end();
|
|
115
|
+
} catch (err) {
|
|
116
|
+
// 如果 headers 未发送,返回 JSON 错误;否则关闭连接
|
|
117
|
+
// 落 stderr 让用户在 ccv 终端能看到 .jsonl.zip 解压失败等具体原因(SSE onerror 在客户端
|
|
118
|
+
// 不携带错误明细,只能从服务端日志反查)
|
|
119
|
+
console.error('[local-log]', file, err && err.stack || err);
|
|
120
|
+
if (!res.headersSent) {
|
|
121
|
+
const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'ACCESS_DENIED' ? 403 : 500;
|
|
122
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
123
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
124
|
+
} else {
|
|
125
|
+
res.end();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function deleteLogs(req, res, parsedUrl, isLocal, deps) {
|
|
131
|
+
let body = '';
|
|
132
|
+
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
133
|
+
req.on('end', () => {
|
|
134
|
+
try {
|
|
135
|
+
const { files } = JSON.parse(body);
|
|
136
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
137
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
138
|
+
res.end(JSON.stringify({ error: 'No files specified' }));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
const results = deleteLogFiles(LOG_DIR, files);
|
|
142
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
143
|
+
res.end(JSON.stringify({ results }));
|
|
144
|
+
} catch {
|
|
145
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
146
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function mergeLogs(req, res, parsedUrl, isLocal, deps) {
|
|
152
|
+
let body = '';
|
|
153
|
+
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
154
|
+
req.on('end', () => {
|
|
155
|
+
try {
|
|
156
|
+
const { files } = JSON.parse(body);
|
|
157
|
+
const merged = mergeLogFiles(LOG_DIR, files);
|
|
158
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
159
|
+
res.end(JSON.stringify({ ok: true, merged }));
|
|
160
|
+
} catch (err) {
|
|
161
|
+
const status = err.code === 'NOT_FOUND' ? 404 : err.code === 'INVALID_INPUT' ? 400 : 500;
|
|
162
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
163
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function archiveLogs(req, res, parsedUrl, isLocal, deps) {
|
|
169
|
+
let body = '';
|
|
170
|
+
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
171
|
+
req.on('end', () => {
|
|
172
|
+
try {
|
|
173
|
+
const { files } = JSON.parse(body);
|
|
174
|
+
if (!Array.isArray(files) || files.length === 0) {
|
|
175
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
176
|
+
res.end(JSON.stringify({ error: 'No files specified' }));
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
if (files.length > 50) {
|
|
180
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
181
|
+
res.end(JSON.stringify({ error: 'Too many files (max 50 per request)' }));
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const result = archiveLogFiles(LOG_DIR, files);
|
|
185
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
186
|
+
res.end(JSON.stringify(result));
|
|
187
|
+
} catch {
|
|
188
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
189
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export const logsRoutes = [
|
|
195
|
+
{ method: 'GET', match: 'exact', path: '/api/local-logs', handler: localLogs },
|
|
196
|
+
{ method: 'GET', match: 'exact', path: '/api/download-log', handler: downloadLog },
|
|
197
|
+
{ method: 'GET', match: 'exact', path: '/api/local-log', handler: localLog },
|
|
198
|
+
{ method: 'POST', match: 'exact', path: '/api/delete-logs', handler: deleteLogs },
|
|
199
|
+
{ method: 'POST', match: 'exact', path: '/api/merge-logs', handler: mergeLogs },
|
|
200
|
+
{ method: 'POST', match: 'exact', path: '/api/archive-logs', handler: archiveLogs },
|
|
201
|
+
];
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Miscellaneous small routes (moved verbatim from server.js handleRequest).
|
|
2
|
+
import { getUserProfile } from '../lib/user-profile.js';
|
|
3
|
+
import { runWaterfallHook } from '../lib/plugin-loader.js';
|
|
4
|
+
|
|
5
|
+
async function userProfile(req, res) {
|
|
6
|
+
const profile = await getUserProfile();
|
|
7
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
8
|
+
res.end(JSON.stringify(profile));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function localUrl(req, res, parsedUrl, isLocal, deps) {
|
|
12
|
+
const localIp = deps.getLocalIp();
|
|
13
|
+
const defaultUrl = `${deps.protocol}://${localIp}:${deps.actualPort}?token=${deps.ACCESS_TOKEN}`;
|
|
14
|
+
const hookResult = await runWaterfallHook('localUrl', { url: defaultUrl, ip: localIp, port: deps.actualPort, token: deps.ACCESS_TOKEN });
|
|
15
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
16
|
+
res.end(JSON.stringify({ url: hookResult.url }));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const miscRoutes = [
|
|
20
|
+
{ method: 'GET', match: 'exact', path: '/api/user-profile', handler: userProfile },
|
|
21
|
+
{ method: 'GET', match: 'exact', path: '/api/local-url', handler: localUrl },
|
|
22
|
+
];
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// Plugin management routes (moved verbatim from server.js handleRequest).
|
|
2
|
+
import { existsSync, unlinkSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { loadPlugins, getPluginsInfo, getPluginsDir } from '../lib/plugin-loader.js';
|
|
5
|
+
import { uploadPlugins, installPluginFromUrl } from '../lib/plugin-manager.js';
|
|
6
|
+
import { SERVER_LIB } from '../_paths.js';
|
|
7
|
+
|
|
8
|
+
function pluginsList(req, res) {
|
|
9
|
+
const plugins = getPluginsInfo();
|
|
10
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
11
|
+
res.end(JSON.stringify({ plugins, pluginsDir: getPluginsDir() }));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function pluginsDelete(req, res, parsedUrl) {
|
|
15
|
+
const file = parsedUrl.searchParams.get('file');
|
|
16
|
+
if (!file || file.includes('..') || file.includes('/') || file.includes('\\')) {
|
|
17
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
18
|
+
res.end(JSON.stringify({ error: 'Invalid file name' }));
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const filePath = join(getPluginsDir(), file);
|
|
22
|
+
try {
|
|
23
|
+
if (!existsSync(filePath)) {
|
|
24
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
25
|
+
res.end(JSON.stringify({ error: 'File not found' }));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
unlinkSync(filePath);
|
|
29
|
+
await loadPlugins();
|
|
30
|
+
const plugins = getPluginsInfo();
|
|
31
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
32
|
+
res.end(JSON.stringify({ ok: true, plugins, pluginsDir: getPluginsDir() }));
|
|
33
|
+
} catch (err) {
|
|
34
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
35
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function pluginsReload(req, res) {
|
|
40
|
+
try {
|
|
41
|
+
await loadPlugins();
|
|
42
|
+
const plugins = getPluginsInfo();
|
|
43
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
44
|
+
res.end(JSON.stringify({ ok: true, plugins, pluginsDir: getPluginsDir() }));
|
|
45
|
+
} catch (err) {
|
|
46
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
47
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pluginsUpload(req, res, parsedUrl, isLocal, deps) {
|
|
52
|
+
let body = '';
|
|
53
|
+
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
54
|
+
req.on('end', async () => {
|
|
55
|
+
try {
|
|
56
|
+
const { files: fileList } = JSON.parse(body);
|
|
57
|
+
uploadPlugins(getPluginsDir(), fileList);
|
|
58
|
+
await loadPlugins();
|
|
59
|
+
const plugins = getPluginsInfo();
|
|
60
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
61
|
+
res.end(JSON.stringify({ ok: true, plugins, pluginsDir: getPluginsDir() }));
|
|
62
|
+
} catch (err) {
|
|
63
|
+
const status = err.statusCode || 500;
|
|
64
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
65
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function pluginsInstallFromUrl(req, res, parsedUrl, isLocal, deps) {
|
|
71
|
+
let body = '';
|
|
72
|
+
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
73
|
+
req.on('end', async () => {
|
|
74
|
+
try {
|
|
75
|
+
const { url: fileUrl } = JSON.parse(body);
|
|
76
|
+
const extractScript = join(SERVER_LIB, 'extract-plugin-name.mjs');
|
|
77
|
+
await installPluginFromUrl(getPluginsDir(), fileUrl, extractScript);
|
|
78
|
+
await loadPlugins();
|
|
79
|
+
const plugins = getPluginsInfo();
|
|
80
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
81
|
+
res.end(JSON.stringify({ ok: true, plugins, pluginsDir: getPluginsDir() }));
|
|
82
|
+
} catch (err) {
|
|
83
|
+
const status = err.statusCode || 500;
|
|
84
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
85
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const pluginsRoutes = [
|
|
91
|
+
{ method: 'GET', match: 'exact', path: '/api/plugins', handler: pluginsList },
|
|
92
|
+
{ method: 'DELETE', match: 'exact', path: '/api/plugins', handler: pluginsDelete },
|
|
93
|
+
{ method: 'POST', match: 'exact', path: '/api/plugins/reload', handler: pluginsReload },
|
|
94
|
+
{ method: 'POST', match: 'exact', path: '/api/plugins/upload', handler: pluginsUpload },
|
|
95
|
+
{ method: 'POST', match: 'exact', path: '/api/plugins/install-from-url', handler: pluginsInstallFromUrl },
|
|
96
|
+
];
|