ccus-cli 0.1.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 +178 -0
- package/dist/cli.js +555 -0
- package/dist/lib/aggregate-dashboard.js +749 -0
- package/dist/lib/aggregate.js +168 -0
- package/dist/lib/claude.js +199 -0
- package/dist/lib/dashboard.js +394 -0
- package/dist/lib/debug.js +61 -0
- package/dist/lib/export.js +275 -0
- package/dist/lib/git.js +39 -0
- package/dist/lib/install.js +73 -0
- package/dist/lib/io.js +16 -0
- package/dist/lib/open.js +26 -0
- package/dist/lib/paths.js +56 -0
- package/dist/lib/payload.js +219 -0
- package/dist/lib/storage.js +217 -0
- package/dist/lib/time.js +154 -0
- package/dist/types.js +3 -0
- package/package.json +35 -0
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.summarizeEvents = summarizeEvents;
|
|
4
|
+
exports.bucketizeEvents = bucketizeEvents;
|
|
5
|
+
exports.buildDashboardHtml = buildDashboardHtml;
|
|
6
|
+
const time_1 = require("./time");
|
|
7
|
+
/** 所有插入到 HTML/SVG 的动态文本都先转义,避免本地页面被注入内容。 */
|
|
8
|
+
function escapeHtml(value) {
|
|
9
|
+
return value
|
|
10
|
+
.replaceAll("&", "&")
|
|
11
|
+
.replaceAll("<", "<")
|
|
12
|
+
.replaceAll(">", ">")
|
|
13
|
+
.replaceAll('"', """)
|
|
14
|
+
.replaceAll("'", "'");
|
|
15
|
+
}
|
|
16
|
+
/** 简单平均值计算,配合 roundNumber 做展示层聚合。 */
|
|
17
|
+
function average(values) {
|
|
18
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 生成 dashboard 顶部卡片需要的摘要指标。
|
|
22
|
+
*
|
|
23
|
+
* 这里的 usage 指 Claude 的 5 小时额度使用率,而不是 context window 百分比。
|
|
24
|
+
*/
|
|
25
|
+
function summarizeEvents(events) {
|
|
26
|
+
const usages = events.map((event) => event.usagePct).filter((value) => value !== null);
|
|
27
|
+
const sevenDayUsages = events.map((event) => event.sevenDayUsagePct).filter((value) => value !== null);
|
|
28
|
+
const latestUsagePct = [...events]
|
|
29
|
+
.sort((left, right) => new Date(right.timestamp).getTime() - new Date(left.timestamp).getTime())
|
|
30
|
+
.find((event) => event.usagePct !== null)?.usagePct ?? null;
|
|
31
|
+
const latestSevenDayUsagePct = [...events]
|
|
32
|
+
.sort((left, right) => new Date(right.timestamp).getTime() - new Date(left.timestamp).getTime())
|
|
33
|
+
.find((event) => event.sevenDayUsagePct !== null)?.sevenDayUsagePct ?? null;
|
|
34
|
+
return {
|
|
35
|
+
fiveHourLatestUsagePct: latestUsagePct,
|
|
36
|
+
fiveHourPeakUsagePct: usages.length > 0 ? (0, time_1.roundNumber)(Math.max(...usages), 1) : null,
|
|
37
|
+
sevenDayLatestUsagePct: sevenDayUsages.length > 0 ? latestSevenDayUsagePct : null,
|
|
38
|
+
sevenDayPeakUsagePct: sevenDayUsages.length > 0 ? (0, time_1.roundNumber)(Math.max(...sevenDayUsages), 1) : null,
|
|
39
|
+
sampleCount: events.length,
|
|
40
|
+
uniqueSessions: new Set(events.map((event) => event.sessionId).filter(Boolean)).size,
|
|
41
|
+
uniqueWorkspaces: new Set(events.map((event) => event.workspaceDir).filter(Boolean)).size,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* 把事件按固定时间桶聚合,供“5 小时额度使用率趋势”绘制使用。
|
|
46
|
+
*/
|
|
47
|
+
function bucketizeEvents(events, start, end, bucketMinutes = 5) {
|
|
48
|
+
const bucketMs = bucketMinutes * 60 * 1000;
|
|
49
|
+
const buckets = new Map();
|
|
50
|
+
for (let cursor = start.getTime(); cursor <= end.getTime(); cursor += bucketMs) {
|
|
51
|
+
buckets.set(cursor, []);
|
|
52
|
+
}
|
|
53
|
+
for (const event of events) {
|
|
54
|
+
if (event.usagePct === null) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const ts = new Date(event.timestamp).getTime();
|
|
58
|
+
if (ts < start.getTime() || ts > end.getTime()) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const bucketStart = start.getTime() + Math.floor((ts - start.getTime()) / bucketMs) * bucketMs;
|
|
62
|
+
const values = buckets.get(bucketStart);
|
|
63
|
+
if (values) {
|
|
64
|
+
values.push(event.usagePct);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return [...buckets.entries()].map(([bucketStart, values]) => ({
|
|
68
|
+
bucketStart: new Date(bucketStart).toISOString(),
|
|
69
|
+
avgUsagePct: values.length > 0 ? (0, time_1.roundNumber)(average(values), 1) : null,
|
|
70
|
+
maxUsagePct: values.length > 0 ? (0, time_1.roundNumber)(Math.max(...values), 1) : null,
|
|
71
|
+
minUsagePct: values.length > 0 ? (0, time_1.roundNumber)(Math.min(...values), 1) : null,
|
|
72
|
+
sampleCount: values.length,
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
/** 直接输出内联 SVG 曲线图,避免额外引入前端框架或图表依赖。 */
|
|
76
|
+
function renderChart(buckets) {
|
|
77
|
+
const width = 920;
|
|
78
|
+
const height = 280;
|
|
79
|
+
const paddingX = 36;
|
|
80
|
+
const paddingY = 28;
|
|
81
|
+
const innerWidth = width - paddingX * 2;
|
|
82
|
+
const innerHeight = height - paddingY * 2;
|
|
83
|
+
const values = buckets.map((bucket) => bucket.avgUsagePct ?? 0);
|
|
84
|
+
const points = buckets.map((bucket, index) => {
|
|
85
|
+
const x = paddingX + (index / Math.max(buckets.length - 1, 1)) * innerWidth;
|
|
86
|
+
const usage = bucket.avgUsagePct ?? 0;
|
|
87
|
+
const y = paddingY + ((100 - usage) / 100) * innerHeight;
|
|
88
|
+
return { x, y, usage, label: (0, time_1.formatLocalTimestamp)(new Date(bucket.bucketStart)) };
|
|
89
|
+
});
|
|
90
|
+
const linePath = points.map((point, index) => `${index === 0 ? "M" : "L"}${point.x.toFixed(2)} ${point.y.toFixed(2)}`).join(" ");
|
|
91
|
+
const areaPath = `${linePath} L${points.at(-1)?.x.toFixed(2) ?? paddingX} ${(height - paddingY).toFixed(2)} L${points[0]?.x.toFixed(2) ?? paddingX} ${(height - paddingY).toFixed(2)} Z`;
|
|
92
|
+
const ticks = [0, 25, 50, 75, 100].map((tick) => {
|
|
93
|
+
const y = paddingY + ((100 - tick) / 100) * innerHeight;
|
|
94
|
+
return `<g><line x1="${paddingX}" x2="${width - paddingX}" y1="${y}" y2="${y}" class="chart-grid" /><text x="8" y="${y + 4}" class="chart-axis">${tick}%</text></g>`;
|
|
95
|
+
}).join("");
|
|
96
|
+
const markers = points
|
|
97
|
+
.filter((_, index) => index === points.length - 1 || index % Math.max(Math.floor(points.length / 6), 1) === 0)
|
|
98
|
+
.map((point) => `<g><line x1="${point.x}" x2="${point.x}" y1="${height - paddingY}" y2="${height - paddingY + 6}" class="chart-axis-line" /><text x="${point.x}" y="${height - 2}" text-anchor="middle" class="chart-axis">${escapeHtml(point.label)}</text></g>`)
|
|
99
|
+
.join("");
|
|
100
|
+
const tooltips = points
|
|
101
|
+
.filter((point) => point.usage > 0)
|
|
102
|
+
.map((point) => `<circle cx="${point.x}" cy="${point.y}" r="3.5" class="chart-point"><title>${escapeHtml(point.label)} · ${point.usage.toFixed(1)}%</title></circle>`)
|
|
103
|
+
.join("");
|
|
104
|
+
const noData = values.every((value) => value === 0)
|
|
105
|
+
? `<div class="empty-state">当前时间窗口里还没有可绘制的 Claude 5 小时使用率样本。</div>`
|
|
106
|
+
: "";
|
|
107
|
+
return `
|
|
108
|
+
<section class="panel chart-panel">
|
|
109
|
+
<div class="panel-header">
|
|
110
|
+
<div>
|
|
111
|
+
<p class="eyebrow">Recent Trend</p>
|
|
112
|
+
<h2>Claude 5 小时使用率趋势</h2>
|
|
113
|
+
</div>
|
|
114
|
+
<p class="muted">按采样时间聚合,Y 轴为 rate_limits.five_hour.used_percentage</p>
|
|
115
|
+
</div>
|
|
116
|
+
${noData}
|
|
117
|
+
<svg viewBox="0 0 ${width} ${height}" class="chart" role="img" aria-label="Claude 5 小时使用率趋势">
|
|
118
|
+
${ticks}
|
|
119
|
+
<path d="${areaPath}" class="chart-area"></path>
|
|
120
|
+
<path d="${linePath}" class="chart-line"></path>
|
|
121
|
+
${markers}
|
|
122
|
+
${tooltips}
|
|
123
|
+
</svg>
|
|
124
|
+
</section>
|
|
125
|
+
`;
|
|
126
|
+
}
|
|
127
|
+
/** 摘要卡片统一的空值展示策略。 */
|
|
128
|
+
function statValue(value, suffix = "%") {
|
|
129
|
+
return value === null ? "--" : `${value.toFixed(1)}${suffix}`;
|
|
130
|
+
}
|
|
131
|
+
/** 最近事件表帮助回看 statusline 在某一时刻到底输出了什么。 */
|
|
132
|
+
function renderRecentEvents(events) {
|
|
133
|
+
const rows = [...events]
|
|
134
|
+
.sort((left, right) => new Date(right.timestamp).getTime() - new Date(left.timestamp).getTime())
|
|
135
|
+
.slice(0, 20)
|
|
136
|
+
.map((event) => `
|
|
137
|
+
<tr>
|
|
138
|
+
<td>${escapeHtml((0, time_1.formatLocalTimestamp)(new Date(event.timestamp)))}</td>
|
|
139
|
+
<td>${escapeHtml(event.workspaceName ?? "--")}</td>
|
|
140
|
+
<td>${escapeHtml(event.modelName ?? "--")}</td>
|
|
141
|
+
<td>${escapeHtml(event.sessionId ?? "--")}</td>
|
|
142
|
+
<td>${escapeHtml(event.usagePct === null ? "--" : `${event.usagePct.toFixed(1)}%`)}</td>
|
|
143
|
+
<td>${escapeHtml(event.statusLine)}</td>
|
|
144
|
+
</tr>`)
|
|
145
|
+
.join("");
|
|
146
|
+
return `
|
|
147
|
+
<section class="panel table-panel">
|
|
148
|
+
<div class="panel-header">
|
|
149
|
+
<div>
|
|
150
|
+
<p class="eyebrow">Recent Events</p>
|
|
151
|
+
<h2>最近 20 条采样</h2>
|
|
152
|
+
</div>
|
|
153
|
+
<p class="muted">按时间倒序展示,便于回看 Claude 5 小时使用率与上下文占用。</p>
|
|
154
|
+
</div>
|
|
155
|
+
<div class="table-wrap">
|
|
156
|
+
<table>
|
|
157
|
+
<thead>
|
|
158
|
+
<tr>
|
|
159
|
+
<th>时间</th>
|
|
160
|
+
<th>项目</th>
|
|
161
|
+
<th>模型</th>
|
|
162
|
+
<th>Session</th>
|
|
163
|
+
<th>5h 使用率</th>
|
|
164
|
+
<th>Statusline</th>
|
|
165
|
+
</tr>
|
|
166
|
+
</thead>
|
|
167
|
+
<tbody>${rows}</tbody>
|
|
168
|
+
</table>
|
|
169
|
+
</div>
|
|
170
|
+
</section>
|
|
171
|
+
`;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* 生成完整静态 HTML。
|
|
175
|
+
*
|
|
176
|
+
* 整个 dashboard 保持“单文件可打开”,方便导出和直接分享本地结果。
|
|
177
|
+
*/
|
|
178
|
+
function buildDashboardHtml(events, rangeLabel, start, end) {
|
|
179
|
+
const summary = summarizeEvents(events);
|
|
180
|
+
const buckets = bucketizeEvents(events, start, end, 5);
|
|
181
|
+
return `<!doctype html>
|
|
182
|
+
<html lang="zh-CN">
|
|
183
|
+
<head>
|
|
184
|
+
<meta charset="utf-8" />
|
|
185
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
186
|
+
<title>ccus dashboard</title>
|
|
187
|
+
<style>
|
|
188
|
+
:root {
|
|
189
|
+
--bg: #0a0d12;
|
|
190
|
+
--panel: rgba(16, 21, 31, 0.84);
|
|
191
|
+
--panel-border: rgba(120, 141, 173, 0.18);
|
|
192
|
+
--text: #ecf3ff;
|
|
193
|
+
--muted: #91a0b8;
|
|
194
|
+
--accent: #5eead4;
|
|
195
|
+
--accent-strong: #22c55e;
|
|
196
|
+
--warning: #f59e0b;
|
|
197
|
+
--grid: rgba(145, 160, 184, 0.15);
|
|
198
|
+
--shadow: 0 24px 80px rgba(0, 0, 0, 0.35);
|
|
199
|
+
}
|
|
200
|
+
* { box-sizing: border-box; }
|
|
201
|
+
body {
|
|
202
|
+
margin: 0;
|
|
203
|
+
font-family: Georgia, "Times New Roman", serif;
|
|
204
|
+
color: var(--text);
|
|
205
|
+
background:
|
|
206
|
+
radial-gradient(circle at top left, rgba(34, 197, 94, 0.18), transparent 30%),
|
|
207
|
+
radial-gradient(circle at top right, rgba(94, 234, 212, 0.16), transparent 28%),
|
|
208
|
+
linear-gradient(160deg, #06080c 0%, #0a0d12 48%, #101520 100%);
|
|
209
|
+
min-height: 100vh;
|
|
210
|
+
}
|
|
211
|
+
.shell {
|
|
212
|
+
max-width: 1180px;
|
|
213
|
+
margin: 0 auto;
|
|
214
|
+
padding: 40px 24px 64px;
|
|
215
|
+
}
|
|
216
|
+
.hero {
|
|
217
|
+
display: grid;
|
|
218
|
+
gap: 16px;
|
|
219
|
+
padding: 28px 0 18px;
|
|
220
|
+
}
|
|
221
|
+
.eyebrow {
|
|
222
|
+
margin: 0 0 8px;
|
|
223
|
+
color: var(--accent);
|
|
224
|
+
text-transform: uppercase;
|
|
225
|
+
letter-spacing: 0.14em;
|
|
226
|
+
font-size: 12px;
|
|
227
|
+
}
|
|
228
|
+
h1, h2, p { margin: 0; }
|
|
229
|
+
h1 {
|
|
230
|
+
font-size: clamp(36px, 5vw, 60px);
|
|
231
|
+
line-height: 0.95;
|
|
232
|
+
font-weight: 600;
|
|
233
|
+
}
|
|
234
|
+
.subtitle {
|
|
235
|
+
max-width: 820px;
|
|
236
|
+
color: var(--muted);
|
|
237
|
+
font-size: 16px;
|
|
238
|
+
line-height: 1.6;
|
|
239
|
+
}
|
|
240
|
+
.hero-meta {
|
|
241
|
+
display: flex;
|
|
242
|
+
flex-wrap: wrap;
|
|
243
|
+
gap: 12px;
|
|
244
|
+
color: var(--muted);
|
|
245
|
+
font-size: 14px;
|
|
246
|
+
}
|
|
247
|
+
.hero-chip {
|
|
248
|
+
padding: 8px 12px;
|
|
249
|
+
border-radius: 999px;
|
|
250
|
+
border: 1px solid var(--panel-border);
|
|
251
|
+
background: rgba(9, 12, 18, 0.56);
|
|
252
|
+
}
|
|
253
|
+
.stats {
|
|
254
|
+
display: grid;
|
|
255
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
256
|
+
gap: 16px;
|
|
257
|
+
margin: 24px 0 28px;
|
|
258
|
+
}
|
|
259
|
+
.panel {
|
|
260
|
+
background: var(--panel);
|
|
261
|
+
backdrop-filter: blur(18px);
|
|
262
|
+
border: 1px solid var(--panel-border);
|
|
263
|
+
border-radius: 24px;
|
|
264
|
+
box-shadow: var(--shadow);
|
|
265
|
+
}
|
|
266
|
+
.stat-card {
|
|
267
|
+
padding: 20px;
|
|
268
|
+
min-height: 128px;
|
|
269
|
+
}
|
|
270
|
+
.stat-card h2 {
|
|
271
|
+
font-size: 14px;
|
|
272
|
+
color: var(--muted);
|
|
273
|
+
font-weight: 500;
|
|
274
|
+
}
|
|
275
|
+
.stat-value {
|
|
276
|
+
margin-top: 16px;
|
|
277
|
+
font-size: 40px;
|
|
278
|
+
line-height: 0.95;
|
|
279
|
+
}
|
|
280
|
+
.stat-note {
|
|
281
|
+
margin-top: 12px;
|
|
282
|
+
color: var(--muted);
|
|
283
|
+
font-size: 14px;
|
|
284
|
+
}
|
|
285
|
+
.panel-header {
|
|
286
|
+
display: flex;
|
|
287
|
+
align-items: end;
|
|
288
|
+
justify-content: space-between;
|
|
289
|
+
gap: 16px;
|
|
290
|
+
padding: 24px 24px 0;
|
|
291
|
+
}
|
|
292
|
+
.panel-header h2 {
|
|
293
|
+
font-size: 28px;
|
|
294
|
+
line-height: 1;
|
|
295
|
+
}
|
|
296
|
+
.muted {
|
|
297
|
+
color: var(--muted);
|
|
298
|
+
font-size: 14px;
|
|
299
|
+
}
|
|
300
|
+
.chart-panel { padding-bottom: 22px; }
|
|
301
|
+
.chart {
|
|
302
|
+
width: 100%;
|
|
303
|
+
height: auto;
|
|
304
|
+
display: block;
|
|
305
|
+
padding: 12px 20px 6px;
|
|
306
|
+
}
|
|
307
|
+
.chart-grid { stroke: var(--grid); stroke-width: 1; }
|
|
308
|
+
.chart-axis { fill: var(--muted); font-size: 11px; }
|
|
309
|
+
.chart-axis-line { stroke: var(--grid); stroke-width: 1; }
|
|
310
|
+
.chart-area { fill: rgba(94, 234, 212, 0.14); }
|
|
311
|
+
.chart-line { fill: none; stroke: var(--accent); stroke-width: 3; stroke-linecap: round; stroke-linejoin: round; }
|
|
312
|
+
.chart-point { fill: var(--accent); }
|
|
313
|
+
.empty-state {
|
|
314
|
+
margin: 18px 24px 0;
|
|
315
|
+
padding: 16px 18px;
|
|
316
|
+
border-radius: 16px;
|
|
317
|
+
background: rgba(145, 160, 184, 0.08);
|
|
318
|
+
color: var(--muted);
|
|
319
|
+
}
|
|
320
|
+
.table-panel { margin-top: 22px; }
|
|
321
|
+
.table-wrap { overflow: auto; padding: 16px 20px 22px; }
|
|
322
|
+
table {
|
|
323
|
+
width: 100%;
|
|
324
|
+
border-collapse: collapse;
|
|
325
|
+
min-width: 760px;
|
|
326
|
+
}
|
|
327
|
+
th, td {
|
|
328
|
+
text-align: left;
|
|
329
|
+
padding: 12px 10px;
|
|
330
|
+
border-bottom: 1px solid rgba(145, 160, 184, 0.12);
|
|
331
|
+
vertical-align: top;
|
|
332
|
+
}
|
|
333
|
+
th {
|
|
334
|
+
color: var(--muted);
|
|
335
|
+
font-size: 12px;
|
|
336
|
+
text-transform: uppercase;
|
|
337
|
+
letter-spacing: 0.08em;
|
|
338
|
+
font-weight: 500;
|
|
339
|
+
}
|
|
340
|
+
td { font-size: 14px; }
|
|
341
|
+
code {
|
|
342
|
+
font-family: "Cascadia Code", "SFMono-Regular", Consolas, monospace;
|
|
343
|
+
font-size: 12px;
|
|
344
|
+
}
|
|
345
|
+
@media (max-width: 720px) {
|
|
346
|
+
.shell { padding-inline: 16px; }
|
|
347
|
+
.panel-header { flex-direction: column; align-items: start; }
|
|
348
|
+
}
|
|
349
|
+
</style>
|
|
350
|
+
</head>
|
|
351
|
+
<body>
|
|
352
|
+
<main class="shell">
|
|
353
|
+
<section class="hero">
|
|
354
|
+
<div>
|
|
355
|
+
<p class="eyebrow">Claude Code Usage Surface</p>
|
|
356
|
+
<h1>ccus dashboard</h1>
|
|
357
|
+
</div>
|
|
358
|
+
<p class="subtitle">围绕 Claude Code statusline 的本地采样面板。每次刷新记录一条事件,再把 Claude 自带的 5 小时使用率百分比随时间的变化绘制成静态 Web 页面,适合快速回看与导出周报。</p>
|
|
359
|
+
<div class="hero-meta">
|
|
360
|
+
<span class="hero-chip">时间范围:${escapeHtml(rangeLabel)}</span>
|
|
361
|
+
<span class="hero-chip">开始:${escapeHtml((0, time_1.formatLocalTimestamp)(start))}</span>
|
|
362
|
+
<span class="hero-chip">结束:${escapeHtml((0, time_1.formatLocalTimestamp)(end))}</span>
|
|
363
|
+
<span class="hero-chip">事件:${summary.sampleCount}</span>
|
|
364
|
+
</div>
|
|
365
|
+
</section>
|
|
366
|
+
<section class="stats">
|
|
367
|
+
<article class="panel stat-card">
|
|
368
|
+
<h2>Latest 5h usage</h2>
|
|
369
|
+
<p class="stat-value">${escapeHtml(statValue(summary.fiveHourLatestUsagePct))}</p>
|
|
370
|
+
<p class="stat-note">最后一条有效 5 小时 usage 样本</p>
|
|
371
|
+
</article>
|
|
372
|
+
<article class="panel stat-card">
|
|
373
|
+
<h2>Peak 5h usage</h2>
|
|
374
|
+
<p class="stat-value">${escapeHtml(statValue(summary.fiveHourPeakUsagePct))}</p>
|
|
375
|
+
<p class="stat-note">窗口内观测到的 5 小时使用率峰值</p>
|
|
376
|
+
</article>
|
|
377
|
+
<article class="panel stat-card">
|
|
378
|
+
<h2>Sessions</h2>
|
|
379
|
+
<p class="stat-value">${summary.uniqueSessions}</p>
|
|
380
|
+
<p class="stat-note">去重 session 数</p>
|
|
381
|
+
</article>
|
|
382
|
+
<article class="panel stat-card">
|
|
383
|
+
<h2>Workspaces</h2>
|
|
384
|
+
<p class="stat-value">${summary.uniqueWorkspaces}</p>
|
|
385
|
+
<p class="stat-note">去重项目目录数</p>
|
|
386
|
+
</article>
|
|
387
|
+
</section>
|
|
388
|
+
${renderChart(buckets)}
|
|
389
|
+
${renderRecentEvents(events)}
|
|
390
|
+
</main>
|
|
391
|
+
</body>
|
|
392
|
+
</html>`;
|
|
393
|
+
}
|
|
394
|
+
//# sourceMappingURL=dashboard.js.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.setDebugEnabled = setDebugEnabled;
|
|
4
|
+
exports.isDebugEnabled = isDebugEnabled;
|
|
5
|
+
exports.resolveDebugEnabled = resolveDebugEnabled;
|
|
6
|
+
exports.debugLog = debugLog;
|
|
7
|
+
let debugEnabled = false;
|
|
8
|
+
/**
|
|
9
|
+
* 打开 / 关闭调试日志。
|
|
10
|
+
*
|
|
11
|
+
* CLI 的 `--verbose` / `--debug` 参数,或 `CCUS_DEBUG=1` 环境变量都会调用它打开。
|
|
12
|
+
*/
|
|
13
|
+
function setDebugEnabled(enabled) {
|
|
14
|
+
debugEnabled = enabled;
|
|
15
|
+
}
|
|
16
|
+
/** 当前是否在输出调试日志。 */
|
|
17
|
+
function isDebugEnabled() {
|
|
18
|
+
return debugEnabled;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 从 CLI 参数和环境变量推断是否应该开启调试日志。
|
|
22
|
+
*
|
|
23
|
+
* 支持 `--verbose` / `--debug` / `-v` 三种写法,以及 `CCUS_DEBUG=1|true`。
|
|
24
|
+
* 环境变量对 statusline 尤其有用:Claude Code 调用 `ccus statusline emit` 时不方便临时加参数。
|
|
25
|
+
*/
|
|
26
|
+
function resolveDebugEnabled(args, env = process.env) {
|
|
27
|
+
const fromArgs = args.includes("--verbose") || args.includes("--debug") || args.includes("-v");
|
|
28
|
+
const envValue = (env.CCUS_DEBUG ?? "").trim().toLowerCase();
|
|
29
|
+
const fromEnv = envValue === "1" || envValue === "true" || envValue === "yes";
|
|
30
|
+
return fromArgs || fromEnv;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 统一的调试日志出口。
|
|
34
|
+
*
|
|
35
|
+
* 关键约束:日志一律写 stderr。statusline 的 stdout 契约只允许输出一行状态文本,
|
|
36
|
+
* 调试信息绝不能混进 stdout,否则会污染 Claude Code 的 statusline 渲染。
|
|
37
|
+
*/
|
|
38
|
+
function debugLog(scope, message, detail) {
|
|
39
|
+
if (!debugEnabled) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const timestamp = new Date().toISOString();
|
|
43
|
+
let line = `[ccus ${timestamp}] ${scope}: ${message}`;
|
|
44
|
+
if (detail !== undefined) {
|
|
45
|
+
let serialized;
|
|
46
|
+
if (typeof detail === "string") {
|
|
47
|
+
serialized = detail;
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
try {
|
|
51
|
+
serialized = JSON.stringify(detail);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
serialized = String(detail);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
line += ` ${serialized}`;
|
|
58
|
+
}
|
|
59
|
+
process.stderr.write(`${line}\n`);
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=debug.js.map
|