@viren/claude-code-dashboard 0.0.6 → 0.0.7
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 +2 -0
- package/generate-dashboard.mjs +248 -625
- package/package.json +1 -1
- package/src/analysis.mjs +19 -12
- package/src/constants.mjs +1 -1
- package/src/demo.mjs +191 -248
- package/src/pipeline.mjs +500 -0
package/generate-dashboard.mjs
CHANGED
|
@@ -28,39 +28,26 @@ import {
|
|
|
28
28
|
CONF,
|
|
29
29
|
MAX_DEPTH,
|
|
30
30
|
REPO_URL,
|
|
31
|
-
SIMILARITY_THRESHOLD,
|
|
32
31
|
} from "./src/constants.mjs";
|
|
33
32
|
import { parseArgs, generateCompletions } from "./src/cli.mjs";
|
|
34
33
|
import { shortPath } from "./src/helpers.mjs";
|
|
35
34
|
import { anonymizeAll } from "./src/anonymize.mjs";
|
|
36
|
-
import {
|
|
35
|
+
import { generateDemoRawInputs } from "./src/demo.mjs";
|
|
37
36
|
import { findGitRepos, getScanRoots } from "./src/discovery.mjs";
|
|
38
37
|
import { extractProjectDesc, extractSections, scanMdDir } from "./src/markdown.mjs";
|
|
39
38
|
import { scanSkillsDir, groupSkillsByCategory } from "./src/skills.mjs";
|
|
40
39
|
import {
|
|
41
|
-
computeHealthScore,
|
|
42
40
|
detectTechStack,
|
|
43
|
-
|
|
44
|
-
findExemplar,
|
|
45
|
-
generateSuggestions,
|
|
46
|
-
detectConfigPattern,
|
|
47
|
-
computeConfigSimilarity,
|
|
48
|
-
matchSkillsToRepo,
|
|
41
|
+
getGitRevCount,
|
|
49
42
|
lintConfig,
|
|
50
43
|
computeDashboardDiff,
|
|
51
44
|
} from "./src/analysis.mjs";
|
|
52
|
-
import { getFreshness
|
|
53
|
-
import {
|
|
54
|
-
parseUserMcpConfig,
|
|
55
|
-
parseProjectMcpConfig,
|
|
56
|
-
findPromotionCandidates,
|
|
57
|
-
scanHistoricalMcpServers,
|
|
58
|
-
classifyHistoricalServers,
|
|
59
|
-
} from "./src/mcp.mjs";
|
|
60
|
-
import { aggregateSessionMeta } from "./src/usage.mjs";
|
|
45
|
+
import { getFreshness } from "./src/freshness.mjs";
|
|
46
|
+
import { parseUserMcpConfig, parseProjectMcpConfig, scanHistoricalMcpServers } from "./src/mcp.mjs";
|
|
61
47
|
import { handleInit } from "./src/templates.mjs";
|
|
62
48
|
import { generateCatalogHtml } from "./src/render.mjs";
|
|
63
49
|
import { generateDashboardHtml } from "./src/assembler.mjs";
|
|
50
|
+
import { buildDashboardData } from "./src/pipeline.mjs";
|
|
64
51
|
import { startWatch } from "./src/watch.mjs";
|
|
65
52
|
|
|
66
53
|
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
@@ -73,8 +60,9 @@ if (cliArgs.command === "init") handleInit(cliArgs);
|
|
|
73
60
|
// ── Demo Mode ────────────────────────────────────────────────────────────────
|
|
74
61
|
|
|
75
62
|
if (cliArgs.demo) {
|
|
76
|
-
const
|
|
77
|
-
const
|
|
63
|
+
const rawInputs = generateDemoRawInputs();
|
|
64
|
+
const data = buildDashboardData(rawInputs);
|
|
65
|
+
const html = generateDashboardHtml(data);
|
|
78
66
|
|
|
79
67
|
const outputPath = cliArgs.output;
|
|
80
68
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
@@ -97,593 +85,250 @@ if (cliArgs.demo) {
|
|
|
97
85
|
process.exit(0);
|
|
98
86
|
}
|
|
99
87
|
|
|
100
|
-
// ── Collect
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
for (const repoDir of allRepoPaths) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const hasConfig = repo.commands.length > 0 || repo.rules.length > 0 || agentsFile;
|
|
146
|
-
|
|
147
|
-
// Tech stack (for both configured and unconfigured)
|
|
148
|
-
const stackInfo = detectTechStack(repoDir);
|
|
149
|
-
repo.techStack = stackInfo.stacks;
|
|
150
|
-
|
|
151
|
-
if (hasConfig) {
|
|
152
|
-
repo.freshness = getFreshness(repoDir);
|
|
153
|
-
repo.freshnessText = relativeTime(repo.freshness);
|
|
154
|
-
repo.freshnessClass = freshnessClass(repo.freshness);
|
|
155
|
-
|
|
156
|
-
// Health score
|
|
157
|
-
const health = computeHealthScore({
|
|
158
|
-
hasAgentsFile: !!agentsFile,
|
|
159
|
-
desc: repo.desc,
|
|
160
|
-
commandCount: repo.commands.length,
|
|
161
|
-
ruleCount: repo.rules.length,
|
|
162
|
-
sectionCount: repo.sections.length,
|
|
163
|
-
freshnessClass: repo.freshnessClass,
|
|
88
|
+
// ── Collect Raw Inputs ────────────────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
function collectRawInputs() {
|
|
91
|
+
const scanRoots = getScanRoots();
|
|
92
|
+
const allRepoPaths = findGitRepos(scanRoots, MAX_DEPTH);
|
|
93
|
+
|
|
94
|
+
// Global config
|
|
95
|
+
const globalCmds = scanMdDir(join(CLAUDE_DIR, "commands"));
|
|
96
|
+
const globalRules = scanMdDir(join(CLAUDE_DIR, "rules"));
|
|
97
|
+
const globalSkills = scanSkillsDir(join(CLAUDE_DIR, "skills"));
|
|
98
|
+
|
|
99
|
+
// Repo discovery and scanning
|
|
100
|
+
const repos = [];
|
|
101
|
+
for (const repoDir of allRepoPaths) {
|
|
102
|
+
const name = basename(repoDir);
|
|
103
|
+
const commands = scanMdDir(join(repoDir, ".claude", "commands"));
|
|
104
|
+
const rules = scanMdDir(join(repoDir, ".claude", "rules"));
|
|
105
|
+
|
|
106
|
+
// AGENTS.md / CLAUDE.md
|
|
107
|
+
let agentsFile = null;
|
|
108
|
+
if (existsSync(join(repoDir, "AGENTS.md"))) agentsFile = join(repoDir, "AGENTS.md");
|
|
109
|
+
else if (existsSync(join(repoDir, "CLAUDE.md"))) agentsFile = join(repoDir, "CLAUDE.md");
|
|
110
|
+
|
|
111
|
+
const desc = agentsFile ? extractProjectDesc(agentsFile) : [];
|
|
112
|
+
const sections = agentsFile ? extractSections(agentsFile) : [];
|
|
113
|
+
|
|
114
|
+
const stackInfo = detectTechStack(repoDir);
|
|
115
|
+
const hasConfig = commands.length > 0 || rules.length > 0 || agentsFile;
|
|
116
|
+
const freshness = hasConfig ? getFreshness(repoDir) : 0;
|
|
117
|
+
|
|
118
|
+
// Compute gitRevCount for configured repos (used by pipeline for drift)
|
|
119
|
+
const gitRevCount = hasConfig ? getGitRevCount(repoDir, freshness) : null;
|
|
120
|
+
|
|
121
|
+
repos.push({
|
|
122
|
+
name,
|
|
123
|
+
path: repoDir,
|
|
124
|
+
shortPath: shortPath(repoDir),
|
|
125
|
+
commands,
|
|
126
|
+
rules,
|
|
127
|
+
agentsFile,
|
|
128
|
+
desc,
|
|
129
|
+
sections,
|
|
130
|
+
techStack: stackInfo.stacks,
|
|
131
|
+
freshness,
|
|
132
|
+
gitRevCount,
|
|
164
133
|
});
|
|
165
|
-
repo.healthScore = health.score;
|
|
166
|
-
repo.healthReasons = health.reasons;
|
|
167
|
-
repo.hasAgentsFile = !!agentsFile;
|
|
168
|
-
repo.configPattern = detectConfigPattern(repo);
|
|
169
|
-
|
|
170
|
-
// Drift detection
|
|
171
|
-
const drift = computeDrift(repoDir, repo.freshness);
|
|
172
|
-
repo.drift = drift;
|
|
173
|
-
|
|
174
|
-
configured.push(repo);
|
|
175
|
-
} else {
|
|
176
|
-
unconfigured.push(repo);
|
|
177
134
|
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Sort configured by richness (most config first)
|
|
181
|
-
configured.sort((a, b) => {
|
|
182
|
-
const score = (r) =>
|
|
183
|
-
r.commands.length * 3 + r.rules.length * 2 + r.sections.length + (r.desc.length > 0 ? 1 : 0);
|
|
184
|
-
return score(b) - score(a);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
unconfigured.sort((a, b) => a.name.localeCompare(b.name));
|
|
188
|
-
|
|
189
|
-
// Compute suggestions for unconfigured repos
|
|
190
|
-
for (const repo of unconfigured) {
|
|
191
|
-
const exemplar = findExemplar(repo.techStack, configured);
|
|
192
|
-
if (exemplar) {
|
|
193
|
-
repo.suggestions = generateSuggestions(exemplar);
|
|
194
|
-
repo.exemplarName = exemplar.name;
|
|
195
|
-
} else {
|
|
196
|
-
repo.suggestions = [];
|
|
197
|
-
repo.exemplarName = "";
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
135
|
|
|
201
|
-
//
|
|
202
|
-
for (const repo of configured) {
|
|
203
|
-
const similar = configured
|
|
204
|
-
.filter((r) => r !== repo)
|
|
205
|
-
.map((r) => ({ name: r.name, similarity: computeConfigSimilarity(repo, r) }))
|
|
206
|
-
.filter((r) => r.similarity >= SIMILARITY_THRESHOLD)
|
|
207
|
-
.sort((a, b) => b.similarity - a.similarity)
|
|
208
|
-
.slice(0, 2);
|
|
209
|
-
repo.similarRepos = similar;
|
|
210
|
-
repo.matchedSkills = matchSkillsToRepo(repo, globalSkills);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Detect consolidation opportunities
|
|
214
|
-
const consolidationGroups = [];
|
|
215
|
-
const byStack = {};
|
|
216
|
-
for (const repo of configured) {
|
|
217
|
-
for (const s of repo.techStack || []) {
|
|
218
|
-
if (!byStack[s]) byStack[s] = [];
|
|
219
|
-
byStack[s].push(repo);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
for (const [stack, repos] of Object.entries(byStack)) {
|
|
223
|
-
if (repos.length >= 3) {
|
|
224
|
-
let pairCount = 0;
|
|
225
|
-
let simSum = 0;
|
|
226
|
-
for (let i = 0; i < repos.length; i++) {
|
|
227
|
-
for (let j = i + 1; j < repos.length; j++) {
|
|
228
|
-
simSum += computeConfigSimilarity(repos[i], repos[j]);
|
|
229
|
-
pairCount++;
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
const avgSimilarity = pairCount > 0 ? Math.round(simSum / pairCount) : 0;
|
|
233
|
-
if (avgSimilarity >= 30) {
|
|
234
|
-
consolidationGroups.push({
|
|
235
|
-
stack,
|
|
236
|
-
repos: repos.map((r) => r.name),
|
|
237
|
-
avgSimilarity,
|
|
238
|
-
suggestion: `${repos.length} ${stack} repos with ${avgSimilarity}% avg similarity — consider shared global rules`,
|
|
239
|
-
});
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Dependency chains from config
|
|
245
|
-
function parseChains() {
|
|
246
|
-
if (!existsSync(CONF)) return [];
|
|
136
|
+
// Dependency chains from config
|
|
247
137
|
const chains = [];
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
138
|
+
if (existsSync(CONF)) {
|
|
139
|
+
for (const line of readFileSync(CONF, "utf8").split("\n")) {
|
|
140
|
+
const m = line.match(/^chain:\s*(.+)/i);
|
|
141
|
+
if (!m) continue;
|
|
142
|
+
const raw = m[1];
|
|
143
|
+
if (raw.includes("<-")) {
|
|
144
|
+
chains.push({ nodes: raw.split(/\s*<-\s*/), arrow: "←" });
|
|
145
|
+
} else {
|
|
146
|
+
chains.push({ nodes: raw.split(/\s*->\s*/), arrow: "→" });
|
|
147
|
+
}
|
|
256
148
|
}
|
|
257
149
|
}
|
|
258
|
-
return chains;
|
|
259
|
-
}
|
|
260
|
-
const chains = parseChains();
|
|
261
150
|
|
|
262
|
-
// MCP Server Discovery
|
|
263
|
-
const claudeJsonPath = join(HOME, ".claude.json");
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
const userMcpPath = join(CLAUDE_DIR, "mcp_config.json");
|
|
267
|
-
if (existsSync(userMcpPath)) {
|
|
268
|
-
try {
|
|
269
|
-
const content = readFileSync(userMcpPath, "utf8");
|
|
270
|
-
allMcpServers.push(...parseUserMcpConfig(content));
|
|
271
|
-
} catch {
|
|
272
|
-
// skip if unreadable
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// ~/.claude.json is the primary location where `claude mcp add` writes
|
|
277
|
-
let claudeJsonParsed = null;
|
|
278
|
-
if (existsSync(claudeJsonPath)) {
|
|
279
|
-
try {
|
|
280
|
-
const content = readFileSync(claudeJsonPath, "utf8");
|
|
281
|
-
claudeJsonParsed = JSON.parse(content);
|
|
282
|
-
const existing = new Set(allMcpServers.filter((s) => s.scope === "user").map((s) => s.name));
|
|
283
|
-
for (const s of parseUserMcpConfig(content)) {
|
|
284
|
-
if (!existing.has(s.name)) allMcpServers.push(s);
|
|
285
|
-
}
|
|
286
|
-
} catch {
|
|
287
|
-
// skip if unreadable
|
|
288
|
-
}
|
|
289
|
-
}
|
|
151
|
+
// MCP Server Discovery
|
|
152
|
+
const claudeJsonPath = join(HOME, ".claude.json");
|
|
153
|
+
const userMcpServers = [];
|
|
290
154
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
if (existsSync(mcpPath)) {
|
|
155
|
+
const userMcpPath = join(CLAUDE_DIR, "mcp_config.json");
|
|
156
|
+
if (existsSync(userMcpPath)) {
|
|
294
157
|
try {
|
|
295
|
-
const content = readFileSync(
|
|
296
|
-
|
|
297
|
-
allMcpServers.push(...servers);
|
|
298
|
-
const repo =
|
|
299
|
-
configured.find((r) => r.path === repoDir) || unconfigured.find((r) => r.path === repoDir);
|
|
300
|
-
if (repo) repo.mcpServers = servers;
|
|
158
|
+
const content = readFileSync(userMcpPath, "utf8");
|
|
159
|
+
userMcpServers.push(...parseUserMcpConfig(content));
|
|
301
160
|
} catch {
|
|
302
161
|
// skip if unreadable
|
|
303
162
|
}
|
|
304
163
|
}
|
|
305
|
-
}
|
|
306
164
|
|
|
307
|
-
//
|
|
308
|
-
|
|
309
|
-
if (
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
Array.isArray(entry.disabledMcpServers) &&
|
|
317
|
-
entry.disabledMcpServers.length > 0
|
|
318
|
-
) {
|
|
319
|
-
disabledMcpByRepo[path] = entry.disabledMcpServers;
|
|
165
|
+
// ~/.claude.json is the primary location where `claude mcp add` writes
|
|
166
|
+
let claudeJsonParsed = null;
|
|
167
|
+
if (existsSync(claudeJsonPath)) {
|
|
168
|
+
try {
|
|
169
|
+
const content = readFileSync(claudeJsonPath, "utf8");
|
|
170
|
+
claudeJsonParsed = JSON.parse(content);
|
|
171
|
+
const existing = new Set(userMcpServers.filter((s) => s.scope === "user").map((s) => s.name));
|
|
172
|
+
for (const s of parseUserMcpConfig(content)) {
|
|
173
|
+
if (!existing.has(s.name)) userMcpServers.push(s);
|
|
320
174
|
}
|
|
175
|
+
} catch {
|
|
176
|
+
// skip if unreadable
|
|
321
177
|
}
|
|
322
|
-
} catch {
|
|
323
|
-
// skip if parse fails
|
|
324
178
|
}
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
const mcpPromotions = findPromotionCandidates(allMcpServers);
|
|
328
|
-
|
|
329
|
-
const disabledByServer = {};
|
|
330
|
-
for (const [, names] of Object.entries(disabledMcpByRepo)) {
|
|
331
|
-
for (const name of names) {
|
|
332
|
-
disabledByServer[name] = (disabledByServer[name] || 0) + 1;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
const mcpByName = {};
|
|
337
|
-
for (const s of allMcpServers) {
|
|
338
|
-
if (!mcpByName[s.name])
|
|
339
|
-
mcpByName[s.name] = {
|
|
340
|
-
name: s.name,
|
|
341
|
-
type: s.type,
|
|
342
|
-
projects: [],
|
|
343
|
-
userLevel: false,
|
|
344
|
-
disabledIn: 0,
|
|
345
|
-
};
|
|
346
|
-
if (s.scope === "user") mcpByName[s.name].userLevel = true;
|
|
347
|
-
if (s.scope === "project") mcpByName[s.name].projects.push(s.source);
|
|
348
|
-
}
|
|
349
|
-
for (const entry of Object.values(mcpByName)) {
|
|
350
|
-
entry.disabledIn = disabledByServer[entry.name] || 0;
|
|
351
|
-
}
|
|
352
179
|
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
);
|
|
359
|
-
|
|
360
|
-
// Normalize all historical project paths
|
|
361
|
-
for (const server of [...recentMcpServers, ...formerMcpServers]) {
|
|
362
|
-
server.projects = server.projects.map((p) => shortPath(p));
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
// Merge recently-seen servers into allMcpServers so they show up as current
|
|
366
|
-
for (const server of recentMcpServers) {
|
|
367
|
-
if (!mcpByName[server.name]) {
|
|
368
|
-
mcpByName[server.name] = {
|
|
369
|
-
name: server.name,
|
|
370
|
-
type: "unknown",
|
|
371
|
-
projects: server.projects,
|
|
372
|
-
userLevel: false,
|
|
373
|
-
disabledIn: disabledByServer[server.name] || 0,
|
|
374
|
-
recentlyActive: true,
|
|
375
|
-
};
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
const mcpSummary = Object.values(mcpByName).sort((a, b) => {
|
|
379
|
-
if (a.userLevel !== b.userLevel) return a.userLevel ? -1 : 1;
|
|
380
|
-
return a.name.localeCompare(b.name);
|
|
381
|
-
});
|
|
382
|
-
const mcpCount = mcpSummary.length;
|
|
383
|
-
|
|
384
|
-
// ── Usage Analytics ──────────────────────────────────────────────────────────
|
|
385
|
-
|
|
386
|
-
const SESSION_META_LIMIT = 1000;
|
|
387
|
-
const sessionMetaDir = join(CLAUDE_DIR, "usage-data", "session-meta");
|
|
388
|
-
const sessionMetaFiles = [];
|
|
389
|
-
if (existsSync(sessionMetaDir)) {
|
|
390
|
-
try {
|
|
391
|
-
const files = readdirSync(sessionMetaDir)
|
|
392
|
-
.filter((f) => f.endsWith(".json"))
|
|
393
|
-
.sort()
|
|
394
|
-
.slice(-SESSION_META_LIMIT);
|
|
395
|
-
for (const f of files) {
|
|
180
|
+
// Project MCP servers
|
|
181
|
+
const projectMcpByRepo = {};
|
|
182
|
+
for (const repoDir of allRepoPaths) {
|
|
183
|
+
const mcpPath = join(repoDir, ".mcp.json");
|
|
184
|
+
if (existsSync(mcpPath)) {
|
|
396
185
|
try {
|
|
397
|
-
const content = readFileSync(
|
|
398
|
-
|
|
186
|
+
const content = readFileSync(mcpPath, "utf8");
|
|
187
|
+
const servers = parseProjectMcpConfig(content, shortPath(repoDir));
|
|
188
|
+
projectMcpByRepo[repoDir] = servers;
|
|
399
189
|
} catch {
|
|
400
|
-
// skip
|
|
190
|
+
// skip if unreadable
|
|
401
191
|
}
|
|
402
192
|
}
|
|
403
|
-
} catch {
|
|
404
|
-
// skip if directory unreadable
|
|
405
193
|
}
|
|
406
|
-
}
|
|
407
|
-
const usageAnalytics = aggregateSessionMeta(sessionMetaFiles);
|
|
408
|
-
|
|
409
|
-
// ccusage integration
|
|
410
|
-
let ccusageData = null;
|
|
411
|
-
const ccusageCachePath = join(CLAUDE_DIR, "ccusage-cache.json");
|
|
412
|
-
const CCUSAGE_TTL_MS = 60 * 60 * 1000;
|
|
413
194
|
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
timeout: 30_000,
|
|
428
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
429
|
-
});
|
|
430
|
-
const parsed = JSON.parse(raw);
|
|
431
|
-
if (parsed.totals && parsed.daily) {
|
|
432
|
-
ccusageData = parsed;
|
|
433
|
-
try {
|
|
434
|
-
writeFileSync(ccusageCachePath, JSON.stringify({ ...parsed, _ts: Date.now() }));
|
|
435
|
-
} catch {
|
|
436
|
-
/* non-critical */
|
|
195
|
+
// Disabled MCP servers
|
|
196
|
+
const disabledMcpByRepo = {};
|
|
197
|
+
if (claudeJsonParsed) {
|
|
198
|
+
try {
|
|
199
|
+
for (const [path, entry] of Object.entries(claudeJsonParsed)) {
|
|
200
|
+
if (
|
|
201
|
+
typeof entry === "object" &&
|
|
202
|
+
entry !== null &&
|
|
203
|
+
Array.isArray(entry.disabledMcpServers) &&
|
|
204
|
+
entry.disabledMcpServers.length > 0
|
|
205
|
+
) {
|
|
206
|
+
disabledMcpByRepo[path] = entry.disabledMcpServers;
|
|
207
|
+
}
|
|
437
208
|
}
|
|
209
|
+
} catch {
|
|
210
|
+
// skip if parse fails
|
|
438
211
|
}
|
|
439
|
-
} catch {
|
|
440
|
-
// ccusage not installed or timed out
|
|
441
212
|
}
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
// Claude Code Insights report (generated by /insights)
|
|
445
|
-
let insightsReport = null;
|
|
446
|
-
const reportPath = join(CLAUDE_DIR, "usage-data", "report.html");
|
|
447
|
-
if (existsSync(reportPath)) {
|
|
448
|
-
try {
|
|
449
|
-
const reportHtml = readFileSync(reportPath, "utf8");
|
|
450
|
-
|
|
451
|
-
// Extract subtitle — reformat ISO dates to readable format
|
|
452
|
-
const subtitleMatch = reportHtml.match(/<p class="subtitle">([^<]+)<\/p>/);
|
|
453
|
-
let subtitle = subtitleMatch ? subtitleMatch[1] : null;
|
|
454
|
-
if (subtitle) {
|
|
455
|
-
subtitle = subtitle.replace(/(\d{4})-(\d{2})-(\d{2})/g, (_, y, m2, d) => {
|
|
456
|
-
const dt = new Date(`${y}-${m2}-${d}T00:00:00Z`);
|
|
457
|
-
return dt.toLocaleDateString("en-US", {
|
|
458
|
-
month: "short",
|
|
459
|
-
day: "numeric",
|
|
460
|
-
year: "numeric",
|
|
461
|
-
timeZone: "UTC",
|
|
462
|
-
});
|
|
463
|
-
});
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
// Extract glance sections (content may contain <strong> tags)
|
|
467
|
-
const glanceSections = [];
|
|
468
|
-
const glanceRe =
|
|
469
|
-
/<div class="glance-section"><strong>([^<]+)<\/strong>\s*([\s\S]*?)<a[^>]*class="see-more"/g;
|
|
470
|
-
let m;
|
|
471
|
-
while ((m = glanceRe.exec(reportHtml)) !== null) {
|
|
472
|
-
const text = m[2].replace(/<[^>]+>/g, "").trim();
|
|
473
|
-
glanceSections.push({ label: m[1].replace(/:$/, ""), text });
|
|
474
|
-
}
|
|
475
213
|
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
const label = m[2];
|
|
482
|
-
// Mark lines stat for diff-style rendering
|
|
483
|
-
const isDiff = /^[+-]/.test(value) && value.includes("/");
|
|
484
|
-
reportStats.push({ value, label, isDiff });
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
// Extract friction categories
|
|
488
|
-
const frictionRe =
|
|
489
|
-
/<div class="friction-title">([^<]+)<\/div>\s*<div class="friction-desc">([^<]+)<\/div>/g;
|
|
490
|
-
const frictionPoints = [];
|
|
491
|
-
while ((m = frictionRe.exec(reportHtml)) !== null) {
|
|
492
|
-
frictionPoints.push({ title: m[1], desc: m[2] });
|
|
493
|
-
}
|
|
214
|
+
// Historical MCP servers — normalize project paths here (I/O side)
|
|
215
|
+
const historicalMcpMap = scanHistoricalMcpServers(CLAUDE_DIR);
|
|
216
|
+
for (const [, entry] of historicalMcpMap) {
|
|
217
|
+
entry.projects = new Set([...entry.projects].map((p) => shortPath(p)));
|
|
218
|
+
}
|
|
494
219
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
220
|
+
// Usage data — session meta files
|
|
221
|
+
const SESSION_META_LIMIT = 1000;
|
|
222
|
+
const sessionMetaDir = join(CLAUDE_DIR, "usage-data", "session-meta");
|
|
223
|
+
const sessionMetaFiles = [];
|
|
224
|
+
if (existsSync(sessionMetaDir)) {
|
|
225
|
+
try {
|
|
226
|
+
const files = readdirSync(sessionMetaDir)
|
|
227
|
+
.filter((f) => f.endsWith(".json"))
|
|
228
|
+
.sort()
|
|
229
|
+
.slice(-SESSION_META_LIMIT);
|
|
230
|
+
for (const f of files) {
|
|
231
|
+
try {
|
|
232
|
+
const content = readFileSync(join(sessionMetaDir, f), "utf8");
|
|
233
|
+
sessionMetaFiles.push(JSON.parse(content));
|
|
234
|
+
} catch {
|
|
235
|
+
// skip unparseable files
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
} catch {
|
|
239
|
+
// skip if directory unreadable
|
|
503
240
|
}
|
|
504
|
-
} catch {
|
|
505
|
-
// skip if unreadable
|
|
506
241
|
}
|
|
507
|
-
}
|
|
508
242
|
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
243
|
+
// ccusage integration
|
|
244
|
+
let ccusageData = null;
|
|
245
|
+
const ccusageCachePath = join(CLAUDE_DIR, "ccusage-cache.json");
|
|
246
|
+
const CCUSAGE_TTL_MS = 60 * 60 * 1000;
|
|
247
|
+
|
|
513
248
|
try {
|
|
514
|
-
|
|
249
|
+
const cached = JSON.parse(readFileSync(ccusageCachePath, "utf8"));
|
|
250
|
+
if (cached._ts && Date.now() - cached._ts < CCUSAGE_TTL_MS && cached.totals && cached.daily) {
|
|
251
|
+
ccusageData = cached;
|
|
252
|
+
}
|
|
515
253
|
} catch {
|
|
516
|
-
|
|
254
|
+
/* no cache or stale */
|
|
517
255
|
}
|
|
518
|
-
}
|
|
519
256
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
(
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
);
|
|
540
|
-
}
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// Supplement dailyActivity with ccusage data (fills gaps like Feb 17-22)
|
|
544
|
-
if (ccusageData && ccusageData.daily) {
|
|
545
|
-
const existingDates = new Set((statsCache.dailyActivity || []).map((d) => d.date));
|
|
546
|
-
const ccusageSupplemental = ccusageData.daily
|
|
547
|
-
.filter((d) => d.date && !existingDates.has(d.date) && d.totalTokens > 0)
|
|
548
|
-
.map((d) => ({ date: d.date, messageCount: Math.max(1, Math.round(d.totalTokens / 10000)) }));
|
|
549
|
-
if (ccusageSupplemental.length > 0) {
|
|
550
|
-
statsCache.dailyActivity = [...(statsCache.dailyActivity || []), ...ccusageSupplemental].sort(
|
|
551
|
-
(a, b) => a.date.localeCompare(b.date),
|
|
552
|
-
);
|
|
257
|
+
if (!ccusageData) {
|
|
258
|
+
try {
|
|
259
|
+
const raw = execFileSync("npx", ["ccusage", "--json"], {
|
|
260
|
+
encoding: "utf8",
|
|
261
|
+
timeout: 30_000,
|
|
262
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
263
|
+
});
|
|
264
|
+
const parsed = JSON.parse(raw);
|
|
265
|
+
if (parsed.totals && parsed.daily) {
|
|
266
|
+
ccusageData = parsed;
|
|
267
|
+
try {
|
|
268
|
+
writeFileSync(ccusageCachePath, JSON.stringify({ ...parsed, _ts: Date.now() }));
|
|
269
|
+
} catch {
|
|
270
|
+
/* non-critical */
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
} catch {
|
|
274
|
+
// ccusage not installed or timed out
|
|
275
|
+
}
|
|
553
276
|
}
|
|
554
|
-
}
|
|
555
277
|
|
|
556
|
-
//
|
|
557
|
-
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
? Math.round(configured.reduce((sum, r) => sum + (r.healthScore || 0), 0) / configured.length)
|
|
566
|
-
: 0;
|
|
567
|
-
const driftCount = configured.filter(
|
|
568
|
-
(r) => r.drift && (r.drift.level === "medium" || r.drift.level === "high"),
|
|
569
|
-
).length;
|
|
570
|
-
|
|
571
|
-
// ── Insights ──────────────────────────────────────────────────────────────────
|
|
572
|
-
const insights = [];
|
|
573
|
-
|
|
574
|
-
// Drift alerts
|
|
575
|
-
const highDriftRepos = configured.filter((r) => r.drift?.level === "high");
|
|
576
|
-
if (highDriftRepos.length > 0) {
|
|
577
|
-
insights.push({
|
|
578
|
-
type: "warning",
|
|
579
|
-
title: `${highDriftRepos.length} repo${highDriftRepos.length > 1 ? "s have" : " has"} high config drift`,
|
|
580
|
-
detail: highDriftRepos
|
|
581
|
-
.map((r) => `${r.name} (${r.drift.commitsSince} commits since config update)`)
|
|
582
|
-
.join(", "),
|
|
583
|
-
action: "Review and update CLAUDE.md in these repos",
|
|
584
|
-
});
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
// Coverage
|
|
588
|
-
if (unconfigured.length > 0 && totalRepos > 0) {
|
|
589
|
-
const pct = Math.round((unconfigured.length / totalRepos) * 100);
|
|
590
|
-
if (pct >= 40) {
|
|
591
|
-
const withStack = unconfigured.filter((r) => r.techStack?.length > 0).slice(0, 3);
|
|
592
|
-
insights.push({
|
|
593
|
-
type: "info",
|
|
594
|
-
title: `${unconfigured.length} repos unconfigured (${pct}%)`,
|
|
595
|
-
detail: withStack.length
|
|
596
|
-
? `Top candidates: ${withStack.map((r) => `${r.name} (${r.techStack.join(", ")})`).join(", ")}`
|
|
597
|
-
: "",
|
|
598
|
-
action: "Run claude-code-dashboard init --template <stack> in these repos",
|
|
599
|
-
});
|
|
278
|
+
// Claude Code Insights report — read raw HTML, pipeline parses it
|
|
279
|
+
let insightsReportHtml = null;
|
|
280
|
+
const reportPath = join(CLAUDE_DIR, "usage-data", "report.html");
|
|
281
|
+
if (existsSync(reportPath)) {
|
|
282
|
+
try {
|
|
283
|
+
insightsReportHtml = readFileSync(reportPath, "utf8");
|
|
284
|
+
} catch {
|
|
285
|
+
// skip if unreadable
|
|
286
|
+
}
|
|
600
287
|
}
|
|
601
|
-
}
|
|
602
288
|
|
|
603
|
-
//
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
}
|
|
612
|
-
|
|
613
|
-
// Redundant project-scope MCP configs (global server also in project .mcp.json)
|
|
614
|
-
const redundantMcp = Object.values(mcpByName).filter((s) => s.userLevel && s.projects.length > 0);
|
|
615
|
-
if (redundantMcp.length > 0) {
|
|
616
|
-
insights.push({
|
|
617
|
-
type: "tip",
|
|
618
|
-
title: `${redundantMcp.length} MCP server${redundantMcp.length > 1 ? "s are" : " is"} global but also in project .mcp.json`,
|
|
619
|
-
detail: redundantMcp.map((s) => `${s.name} (${s.projects.join(", ")})`).join("; "),
|
|
620
|
-
action: "Remove from project .mcp.json — global config already covers all projects",
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Skill sharing opportunities
|
|
625
|
-
const skillMatchCounts = {};
|
|
626
|
-
for (const r of configured) {
|
|
627
|
-
for (const sk of r.matchedSkills || []) {
|
|
628
|
-
const skName = typeof sk === "string" ? sk : sk.name;
|
|
629
|
-
if (!skillMatchCounts[skName]) skillMatchCounts[skName] = [];
|
|
630
|
-
skillMatchCounts[skName].push(r.name);
|
|
289
|
+
// Stats cache
|
|
290
|
+
const statsCachePath = join(CLAUDE_DIR, "stats-cache.json");
|
|
291
|
+
let statsCache = {};
|
|
292
|
+
if (existsSync(statsCachePath)) {
|
|
293
|
+
try {
|
|
294
|
+
statsCache = JSON.parse(readFileSync(statsCachePath, "utf8"));
|
|
295
|
+
} catch {
|
|
296
|
+
// skip if parse fails
|
|
297
|
+
}
|
|
631
298
|
}
|
|
632
|
-
}
|
|
633
|
-
const widelyRelevant = Object.entries(skillMatchCounts)
|
|
634
|
-
.filter(([, repos]) => repos.length >= 3)
|
|
635
|
-
.sort((a, b) => b[1].length - a[1].length);
|
|
636
|
-
if (widelyRelevant.length > 0) {
|
|
637
|
-
const top = widelyRelevant.slice(0, 3);
|
|
638
|
-
insights.push({
|
|
639
|
-
type: "info",
|
|
640
|
-
title: `${widelyRelevant.length} skill${widelyRelevant.length > 1 ? "s" : ""} relevant across 3+ repos`,
|
|
641
|
-
detail: top.map(([name, repos]) => `${name} (${repos.length} repos)`).join(", "),
|
|
642
|
-
action: "Consider adding these skills to your global config",
|
|
643
|
-
});
|
|
644
|
-
}
|
|
645
299
|
|
|
646
|
-
//
|
|
647
|
-
const
|
|
648
|
-
.filter((r) => r.healthScore > 0 && r.healthScore < 80 && r.healthReasons?.length > 0)
|
|
649
|
-
.sort((a, b) => b.healthScore - a.healthScore)
|
|
650
|
-
.slice(0, 3);
|
|
651
|
-
if (quickWinRepos.length > 0) {
|
|
652
|
-
insights.push({
|
|
653
|
-
type: "tip",
|
|
654
|
-
title: "Quick wins to improve config health",
|
|
655
|
-
detail: quickWinRepos
|
|
656
|
-
.map((r) => `${r.name} (${r.healthScore}/100): ${r.healthReasons[0]}`)
|
|
657
|
-
.join("; "),
|
|
658
|
-
action: "Small changes for measurable improvement",
|
|
659
|
-
});
|
|
660
|
-
}
|
|
300
|
+
// Scan scope
|
|
301
|
+
const scanScope = existsSync(CONF) ? `config: ${shortPath(CONF)}` : "~/ (depth 5)";
|
|
661
302
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
303
|
+
return {
|
|
304
|
+
repos,
|
|
305
|
+
globalCmds,
|
|
306
|
+
globalRules,
|
|
307
|
+
globalSkills,
|
|
308
|
+
userMcpServers,
|
|
309
|
+
projectMcpByRepo,
|
|
310
|
+
disabledMcpByRepo,
|
|
311
|
+
historicalMcpMap,
|
|
312
|
+
sessionMetaFiles,
|
|
313
|
+
ccusageData,
|
|
314
|
+
statsCache,
|
|
315
|
+
insightsReportHtml,
|
|
316
|
+
chains,
|
|
317
|
+
scanScope,
|
|
318
|
+
insightsReportPath: reportPath,
|
|
319
|
+
};
|
|
670
320
|
}
|
|
671
321
|
|
|
672
|
-
|
|
673
|
-
const timestamp =
|
|
674
|
-
now
|
|
675
|
-
.toLocaleDateString("en-US", { month: "short", day: "numeric", year: "numeric" })
|
|
676
|
-
.toLowerCase() +
|
|
677
|
-
" at " +
|
|
678
|
-
now.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit" }).toLowerCase();
|
|
322
|
+
// ── Build Dashboard Data ─────────────────────────────────────────────────────
|
|
679
323
|
|
|
680
|
-
const
|
|
324
|
+
const rawInputs = collectRawInputs();
|
|
325
|
+
const data = buildDashboardData(rawInputs);
|
|
681
326
|
|
|
682
327
|
// ── Lint Subcommand ──────────────────────────────────────────────────────────
|
|
683
328
|
|
|
684
329
|
if (cliArgs.command === "lint") {
|
|
685
330
|
let totalIssues = 0;
|
|
686
|
-
for (const repo of configured) {
|
|
331
|
+
for (const repo of data.configured) {
|
|
687
332
|
const issues = lintConfig(repo);
|
|
688
333
|
if (issues.length === 0) continue;
|
|
689
334
|
if (!cliArgs.quiet) console.log(`\n${repo.name} (${repo.shortPath}):`);
|
|
@@ -705,7 +350,10 @@ if (cliArgs.command === "lint") {
|
|
|
705
350
|
const SNAPSHOT_PATH = join(CLAUDE_DIR, "dashboard-snapshot.json");
|
|
706
351
|
if (cliArgs.diff) {
|
|
707
352
|
const currentSnapshot = {
|
|
708
|
-
configuredRepos: configured.map((r) => ({
|
|
353
|
+
configuredRepos: data.configured.map((r) => ({
|
|
354
|
+
name: r.name,
|
|
355
|
+
healthScore: r.healthScore || 0,
|
|
356
|
+
})),
|
|
709
357
|
};
|
|
710
358
|
if (existsSync(SNAPSHOT_PATH)) {
|
|
711
359
|
try {
|
|
@@ -732,59 +380,60 @@ if (cliArgs.diff) {
|
|
|
732
380
|
|
|
733
381
|
if (cliArgs.anonymize) {
|
|
734
382
|
anonymizeAll({
|
|
735
|
-
configured,
|
|
736
|
-
unconfigured,
|
|
737
|
-
globalCmds,
|
|
738
|
-
globalRules,
|
|
739
|
-
globalSkills,
|
|
740
|
-
chains,
|
|
741
|
-
mcpSummary,
|
|
742
|
-
mcpPromotions,
|
|
743
|
-
formerMcpServers,
|
|
744
|
-
consolidationGroups,
|
|
383
|
+
configured: data.configured,
|
|
384
|
+
unconfigured: data.unconfigured,
|
|
385
|
+
globalCmds: data.globalCmds,
|
|
386
|
+
globalRules: data.globalRules,
|
|
387
|
+
globalSkills: data.globalSkills,
|
|
388
|
+
chains: data.chains,
|
|
389
|
+
mcpSummary: data.mcpSummary,
|
|
390
|
+
mcpPromotions: data.mcpPromotions,
|
|
391
|
+
formerMcpServers: data.formerMcpServers,
|
|
392
|
+
consolidationGroups: data.consolidationGroups,
|
|
745
393
|
});
|
|
746
394
|
}
|
|
747
395
|
|
|
748
396
|
// ── JSON Output ──────────────────────────────────────────────────────────────
|
|
749
397
|
|
|
750
398
|
if (cliArgs.json) {
|
|
399
|
+
const now = new Date();
|
|
751
400
|
const jsonData = {
|
|
752
401
|
version: VERSION,
|
|
753
402
|
generatedAt: now.toISOString(),
|
|
754
|
-
scanScope,
|
|
403
|
+
scanScope: data.scanScope,
|
|
755
404
|
stats: {
|
|
756
|
-
totalRepos,
|
|
757
|
-
configuredRepos: configuredCount,
|
|
758
|
-
unconfiguredRepos: unconfiguredCount,
|
|
759
|
-
coveragePct,
|
|
760
|
-
globalCommands: globalCmds.length,
|
|
761
|
-
globalRules: globalRules.length,
|
|
762
|
-
skills: globalSkills.length,
|
|
763
|
-
repoCommands: totalRepoCmds,
|
|
764
|
-
avgHealthScore: avgHealth,
|
|
765
|
-
driftingRepos: driftCount,
|
|
766
|
-
mcpServers: mcpCount,
|
|
767
|
-
...(ccusageData
|
|
405
|
+
totalRepos: data.totalRepos,
|
|
406
|
+
configuredRepos: data.configuredCount,
|
|
407
|
+
unconfiguredRepos: data.unconfiguredCount,
|
|
408
|
+
coveragePct: data.coveragePct,
|
|
409
|
+
globalCommands: data.globalCmds.length,
|
|
410
|
+
globalRules: data.globalRules.length,
|
|
411
|
+
skills: data.globalSkills.length,
|
|
412
|
+
repoCommands: data.totalRepoCmds,
|
|
413
|
+
avgHealthScore: data.avgHealth,
|
|
414
|
+
driftingRepos: data.driftCount,
|
|
415
|
+
mcpServers: data.mcpCount,
|
|
416
|
+
...(data.ccusageData
|
|
768
417
|
? {
|
|
769
|
-
totalCost: ccusageData.totals.totalCost,
|
|
770
|
-
totalTokens: ccusageData.totals.totalTokens,
|
|
418
|
+
totalCost: data.ccusageData.totals.totalCost,
|
|
419
|
+
totalTokens: data.ccusageData.totals.totalTokens,
|
|
771
420
|
}
|
|
772
421
|
: {}),
|
|
773
|
-
errorCategories: usageAnalytics.errorCategories,
|
|
422
|
+
errorCategories: data.usageAnalytics.errorCategories,
|
|
774
423
|
},
|
|
775
|
-
globalCommands: globalCmds.map((c) => ({ name: c.name, description: c.desc })),
|
|
776
|
-
globalRules: globalRules.map((r) => ({ name: r.name, description: r.desc })),
|
|
777
|
-
skills: globalSkills.map((s) => ({
|
|
424
|
+
globalCommands: data.globalCmds.map((c) => ({ name: c.name, description: c.desc })),
|
|
425
|
+
globalRules: data.globalRules.map((r) => ({ name: r.name, description: r.desc })),
|
|
426
|
+
skills: data.globalSkills.map((s) => ({
|
|
778
427
|
name: s.name,
|
|
779
428
|
description: s.desc,
|
|
780
429
|
source: s.source,
|
|
781
430
|
category: s.category,
|
|
782
431
|
})),
|
|
783
|
-
chains: chains.map((c) => ({
|
|
432
|
+
chains: data.chains.map((c) => ({
|
|
784
433
|
nodes: c.nodes.map((n) => n.trim()),
|
|
785
434
|
direction: c.arrow === "→" ? "forward" : "backward",
|
|
786
435
|
})),
|
|
787
|
-
configuredRepos: configured.map((r) => ({
|
|
436
|
+
configuredRepos: data.configured.map((r) => ({
|
|
788
437
|
name: r.name,
|
|
789
438
|
path: r.shortPath,
|
|
790
439
|
commands: r.commands.map((c) => ({ name: c.name, description: c.desc })),
|
|
@@ -805,8 +454,8 @@ if (cliArgs.json) {
|
|
|
805
454
|
similarRepos: r.similarRepos || [],
|
|
806
455
|
mcpServers: r.mcpServers || [],
|
|
807
456
|
})),
|
|
808
|
-
consolidationGroups,
|
|
809
|
-
unconfiguredRepos: unconfigured.map((r) => ({
|
|
457
|
+
consolidationGroups: data.consolidationGroups,
|
|
458
|
+
unconfiguredRepos: data.unconfigured.map((r) => ({
|
|
810
459
|
name: r.name,
|
|
811
460
|
path: r.shortPath,
|
|
812
461
|
techStack: r.techStack || [],
|
|
@@ -814,9 +463,9 @@ if (cliArgs.json) {
|
|
|
814
463
|
exemplar: r.exemplarName || "",
|
|
815
464
|
mcpServers: r.mcpServers || [],
|
|
816
465
|
})),
|
|
817
|
-
mcpServers: mcpSummary,
|
|
818
|
-
mcpPromotions,
|
|
819
|
-
formerMcpServers,
|
|
466
|
+
mcpServers: data.mcpSummary,
|
|
467
|
+
mcpPromotions: data.mcpPromotions,
|
|
468
|
+
formerMcpServers: data.formerMcpServers,
|
|
820
469
|
};
|
|
821
470
|
|
|
822
471
|
const jsonOutput = JSON.stringify(jsonData, null, 2);
|
|
@@ -834,8 +483,8 @@ if (cliArgs.json) {
|
|
|
834
483
|
// ── Catalog Output ───────────────────────────────────────────────────────────
|
|
835
484
|
|
|
836
485
|
if (cliArgs.catalog) {
|
|
837
|
-
const groups = groupSkillsByCategory(globalSkills);
|
|
838
|
-
const catalogHtml = generateCatalogHtml(groups, globalSkills.length, timestamp);
|
|
486
|
+
const groups = groupSkillsByCategory(data.globalSkills);
|
|
487
|
+
const catalogHtml = generateCatalogHtml(groups, data.globalSkills.length, data.timestamp);
|
|
839
488
|
const outputPath =
|
|
840
489
|
cliArgs.output !== DEFAULT_OUTPUT ? cliArgs.output : join(CLAUDE_DIR, "skill-catalog.html");
|
|
841
490
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
@@ -851,33 +500,7 @@ if (cliArgs.catalog) {
|
|
|
851
500
|
|
|
852
501
|
// ── Generate HTML Dashboard ──────────────────────────────────────────────────
|
|
853
502
|
|
|
854
|
-
const html = generateDashboardHtml(
|
|
855
|
-
configured,
|
|
856
|
-
unconfigured,
|
|
857
|
-
globalCmds,
|
|
858
|
-
globalRules,
|
|
859
|
-
globalSkills,
|
|
860
|
-
chains,
|
|
861
|
-
mcpSummary,
|
|
862
|
-
mcpPromotions,
|
|
863
|
-
formerMcpServers,
|
|
864
|
-
consolidationGroups,
|
|
865
|
-
usageAnalytics,
|
|
866
|
-
ccusageData,
|
|
867
|
-
statsCache,
|
|
868
|
-
timestamp,
|
|
869
|
-
coveragePct,
|
|
870
|
-
totalRepos,
|
|
871
|
-
configuredCount,
|
|
872
|
-
unconfiguredCount,
|
|
873
|
-
totalRepoCmds,
|
|
874
|
-
avgHealth,
|
|
875
|
-
driftCount,
|
|
876
|
-
mcpCount,
|
|
877
|
-
scanScope,
|
|
878
|
-
insights,
|
|
879
|
-
insightsReport,
|
|
880
|
-
});
|
|
503
|
+
const html = generateDashboardHtml(data);
|
|
881
504
|
|
|
882
505
|
// ── Write HTML Output ────────────────────────────────────────────────────────
|
|
883
506
|
|
|
@@ -889,10 +512,10 @@ if (!cliArgs.quiet) {
|
|
|
889
512
|
const sp = shortPath(outputPath);
|
|
890
513
|
console.log(`\n claude-code-dashboard v${VERSION}\n`);
|
|
891
514
|
console.log(
|
|
892
|
-
` ${configuredCount} configured · ${unconfiguredCount} unconfigured · ${totalRepos} repos`,
|
|
515
|
+
` ${data.configuredCount} configured · ${data.unconfiguredCount} unconfigured · ${data.totalRepos} repos`,
|
|
893
516
|
);
|
|
894
517
|
console.log(
|
|
895
|
-
` ${globalCmds.length} global commands · ${globalSkills.length} skills · ${mcpCount} MCP servers`,
|
|
518
|
+
` ${data.globalCmds.length} global commands · ${data.globalSkills.length} skills · ${data.mcpCount} MCP servers`,
|
|
896
519
|
);
|
|
897
520
|
console.log(`\n ✓ ${sp}`);
|
|
898
521
|
if (cliArgs.open) console.log(` ✓ opening in browser`);
|
|
@@ -909,5 +532,5 @@ if (cliArgs.open) {
|
|
|
909
532
|
// ── Watch Mode ───────────────────────────────────────────────────────────────
|
|
910
533
|
|
|
911
534
|
if (cliArgs.watch) {
|
|
912
|
-
startWatch(outputPath,
|
|
535
|
+
startWatch(outputPath, getScanRoots(), cliArgs);
|
|
913
536
|
}
|