@viren/claude-code-dashboard 0.0.6 → 0.0.8
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 +251 -619
- package/package.json +1 -1
- package/src/analysis.mjs +19 -12
- package/src/assembler.mjs +4 -1
- package/src/cli.mjs +7 -2
- package/src/constants.mjs +39 -1
- package/src/demo.mjs +269 -248
- package/src/mcp.mjs +97 -2
- package/src/pipeline.mjs +596 -0
- package/src/sections.mjs +50 -2
- package/template/dashboard.css +78 -0
- package/template/dashboard.js +18 -0
package/generate-dashboard.mjs
CHANGED
|
@@ -28,39 +28,31 @@ 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
|
|
45
|
+
import { getFreshness } from "./src/freshness.mjs";
|
|
53
46
|
import {
|
|
54
47
|
parseUserMcpConfig,
|
|
55
48
|
parseProjectMcpConfig,
|
|
56
|
-
findPromotionCandidates,
|
|
57
49
|
scanHistoricalMcpServers,
|
|
58
|
-
|
|
50
|
+
fetchRegistryServers,
|
|
59
51
|
} from "./src/mcp.mjs";
|
|
60
|
-
import { aggregateSessionMeta } from "./src/usage.mjs";
|
|
61
52
|
import { handleInit } from "./src/templates.mjs";
|
|
62
53
|
import { generateCatalogHtml } from "./src/render.mjs";
|
|
63
54
|
import { generateDashboardHtml } from "./src/assembler.mjs";
|
|
55
|
+
import { buildDashboardData } from "./src/pipeline.mjs";
|
|
64
56
|
import { startWatch } from "./src/watch.mjs";
|
|
65
57
|
|
|
66
58
|
// ── CLI ──────────────────────────────────────────────────────────────────────
|
|
@@ -73,8 +65,9 @@ if (cliArgs.command === "init") handleInit(cliArgs);
|
|
|
73
65
|
// ── Demo Mode ────────────────────────────────────────────────────────────────
|
|
74
66
|
|
|
75
67
|
if (cliArgs.demo) {
|
|
76
|
-
const
|
|
77
|
-
const
|
|
68
|
+
const rawInputs = generateDemoRawInputs();
|
|
69
|
+
const data = buildDashboardData(rawInputs);
|
|
70
|
+
const html = generateDashboardHtml(data);
|
|
78
71
|
|
|
79
72
|
const outputPath = cliArgs.output;
|
|
80
73
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
@@ -97,593 +90,254 @@ if (cliArgs.demo) {
|
|
|
97
90
|
process.exit(0);
|
|
98
91
|
}
|
|
99
92
|
|
|
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,
|
|
93
|
+
// ── Collect Raw Inputs ────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
async function collectRawInputs() {
|
|
96
|
+
const scanRoots = getScanRoots();
|
|
97
|
+
const allRepoPaths = findGitRepos(scanRoots, MAX_DEPTH);
|
|
98
|
+
|
|
99
|
+
// Global config
|
|
100
|
+
const globalCmds = scanMdDir(join(CLAUDE_DIR, "commands"));
|
|
101
|
+
const globalRules = scanMdDir(join(CLAUDE_DIR, "rules"));
|
|
102
|
+
const globalSkills = scanSkillsDir(join(CLAUDE_DIR, "skills"));
|
|
103
|
+
|
|
104
|
+
// Repo discovery and scanning
|
|
105
|
+
const repos = [];
|
|
106
|
+
for (const repoDir of allRepoPaths) {
|
|
107
|
+
const name = basename(repoDir);
|
|
108
|
+
const commands = scanMdDir(join(repoDir, ".claude", "commands"));
|
|
109
|
+
const rules = scanMdDir(join(repoDir, ".claude", "rules"));
|
|
110
|
+
|
|
111
|
+
// AGENTS.md / CLAUDE.md
|
|
112
|
+
let agentsFile = null;
|
|
113
|
+
if (existsSync(join(repoDir, "AGENTS.md"))) agentsFile = join(repoDir, "AGENTS.md");
|
|
114
|
+
else if (existsSync(join(repoDir, "CLAUDE.md"))) agentsFile = join(repoDir, "CLAUDE.md");
|
|
115
|
+
|
|
116
|
+
const desc = agentsFile ? extractProjectDesc(agentsFile) : [];
|
|
117
|
+
const sections = agentsFile ? extractSections(agentsFile) : [];
|
|
118
|
+
|
|
119
|
+
const stackInfo = detectTechStack(repoDir);
|
|
120
|
+
const hasConfig = commands.length > 0 || rules.length > 0 || agentsFile;
|
|
121
|
+
const freshness = hasConfig ? getFreshness(repoDir) : 0;
|
|
122
|
+
|
|
123
|
+
// Compute gitRevCount for configured repos (used by pipeline for drift)
|
|
124
|
+
const gitRevCount = hasConfig ? getGitRevCount(repoDir, freshness) : null;
|
|
125
|
+
|
|
126
|
+
repos.push({
|
|
127
|
+
name,
|
|
128
|
+
path: repoDir,
|
|
129
|
+
shortPath: shortPath(repoDir),
|
|
130
|
+
commands,
|
|
131
|
+
rules,
|
|
132
|
+
agentsFile,
|
|
133
|
+
desc,
|
|
134
|
+
sections,
|
|
135
|
+
techStack: stackInfo.stacks,
|
|
136
|
+
freshness,
|
|
137
|
+
gitRevCount,
|
|
164
138
|
});
|
|
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
|
-
}
|
|
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
139
|
}
|
|
199
|
-
}
|
|
200
140
|
|
|
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 [];
|
|
141
|
+
// Dependency chains from config
|
|
247
142
|
const chains = [];
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
143
|
+
if (existsSync(CONF)) {
|
|
144
|
+
for (const line of readFileSync(CONF, "utf8").split("\n")) {
|
|
145
|
+
const m = line.match(/^chain:\s*(.+)/i);
|
|
146
|
+
if (!m) continue;
|
|
147
|
+
const raw = m[1];
|
|
148
|
+
if (raw.includes("<-")) {
|
|
149
|
+
chains.push({ nodes: raw.split(/\s*<-\s*/), arrow: "←" });
|
|
150
|
+
} else {
|
|
151
|
+
chains.push({ nodes: raw.split(/\s*->\s*/), arrow: "→" });
|
|
152
|
+
}
|
|
256
153
|
}
|
|
257
154
|
}
|
|
258
|
-
return chains;
|
|
259
|
-
}
|
|
260
|
-
const chains = parseChains();
|
|
261
155
|
|
|
262
|
-
// MCP Server Discovery
|
|
263
|
-
const claudeJsonPath = join(HOME, ".claude.json");
|
|
264
|
-
const
|
|
156
|
+
// MCP Server Discovery
|
|
157
|
+
const claudeJsonPath = join(HOME, ".claude.json");
|
|
158
|
+
const userMcpServers = [];
|
|
265
159
|
|
|
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
|
-
}
|
|
290
|
-
|
|
291
|
-
for (const repoDir of allRepoPaths) {
|
|
292
|
-
const mcpPath = join(repoDir, ".mcp.json");
|
|
293
|
-
if (existsSync(mcpPath)) {
|
|
160
|
+
const userMcpPath = join(CLAUDE_DIR, "mcp_config.json");
|
|
161
|
+
if (existsSync(userMcpPath)) {
|
|
294
162
|
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;
|
|
163
|
+
const content = readFileSync(userMcpPath, "utf8");
|
|
164
|
+
userMcpServers.push(...parseUserMcpConfig(content));
|
|
301
165
|
} catch {
|
|
302
166
|
// skip if unreadable
|
|
303
167
|
}
|
|
304
168
|
}
|
|
305
|
-
}
|
|
306
169
|
|
|
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;
|
|
170
|
+
// ~/.claude.json is the primary location where `claude mcp add` writes
|
|
171
|
+
let claudeJsonParsed = null;
|
|
172
|
+
if (existsSync(claudeJsonPath)) {
|
|
173
|
+
try {
|
|
174
|
+
const content = readFileSync(claudeJsonPath, "utf8");
|
|
175
|
+
claudeJsonParsed = JSON.parse(content);
|
|
176
|
+
const existing = new Set(userMcpServers.filter((s) => s.scope === "user").map((s) => s.name));
|
|
177
|
+
for (const s of parseUserMcpConfig(content)) {
|
|
178
|
+
if (!existing.has(s.name)) userMcpServers.push(s);
|
|
320
179
|
}
|
|
180
|
+
} catch {
|
|
181
|
+
// skip if unreadable
|
|
321
182
|
}
|
|
322
|
-
} catch {
|
|
323
|
-
// skip if parse fails
|
|
324
183
|
}
|
|
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
184
|
|
|
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) {
|
|
185
|
+
// Project MCP servers
|
|
186
|
+
const projectMcpByRepo = {};
|
|
187
|
+
for (const repoDir of allRepoPaths) {
|
|
188
|
+
const mcpPath = join(repoDir, ".mcp.json");
|
|
189
|
+
if (existsSync(mcpPath)) {
|
|
396
190
|
try {
|
|
397
|
-
const content = readFileSync(
|
|
398
|
-
|
|
191
|
+
const content = readFileSync(mcpPath, "utf8");
|
|
192
|
+
const servers = parseProjectMcpConfig(content, shortPath(repoDir));
|
|
193
|
+
projectMcpByRepo[repoDir] = servers;
|
|
399
194
|
} catch {
|
|
400
|
-
// skip
|
|
195
|
+
// skip if unreadable
|
|
401
196
|
}
|
|
402
197
|
}
|
|
403
|
-
} catch {
|
|
404
|
-
// skip if directory unreadable
|
|
405
198
|
}
|
|
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
199
|
|
|
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 */
|
|
200
|
+
// Disabled MCP servers
|
|
201
|
+
const disabledMcpByRepo = {};
|
|
202
|
+
if (claudeJsonParsed) {
|
|
203
|
+
try {
|
|
204
|
+
for (const [path, entry] of Object.entries(claudeJsonParsed)) {
|
|
205
|
+
if (
|
|
206
|
+
typeof entry === "object" &&
|
|
207
|
+
entry !== null &&
|
|
208
|
+
Array.isArray(entry.disabledMcpServers) &&
|
|
209
|
+
entry.disabledMcpServers.length > 0
|
|
210
|
+
) {
|
|
211
|
+
disabledMcpByRepo[path] = entry.disabledMcpServers;
|
|
212
|
+
}
|
|
437
213
|
}
|
|
214
|
+
} catch {
|
|
215
|
+
// skip if parse fails
|
|
438
216
|
}
|
|
439
|
-
} catch {
|
|
440
|
-
// ccusage not installed or timed out
|
|
441
217
|
}
|
|
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
218
|
|
|
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
|
-
}
|
|
219
|
+
// Historical MCP servers — normalize project paths here (I/O side)
|
|
220
|
+
const historicalMcpMap = scanHistoricalMcpServers(CLAUDE_DIR);
|
|
221
|
+
for (const [, entry] of historicalMcpMap) {
|
|
222
|
+
entry.projects = new Set([...entry.projects].map((p) => shortPath(p)));
|
|
223
|
+
}
|
|
486
224
|
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
}
|
|
225
|
+
// MCP Registry servers
|
|
226
|
+
const registryServers = cliArgs.offline ? [] : await fetchRegistryServers();
|
|
494
227
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
228
|
+
// Usage data — session meta files
|
|
229
|
+
const SESSION_META_LIMIT = 1000;
|
|
230
|
+
const sessionMetaDir = join(CLAUDE_DIR, "usage-data", "session-meta");
|
|
231
|
+
const sessionMetaFiles = [];
|
|
232
|
+
if (existsSync(sessionMetaDir)) {
|
|
233
|
+
try {
|
|
234
|
+
const files = readdirSync(sessionMetaDir)
|
|
235
|
+
.filter((f) => f.endsWith(".json"))
|
|
236
|
+
.sort()
|
|
237
|
+
.slice(-SESSION_META_LIMIT);
|
|
238
|
+
for (const f of files) {
|
|
239
|
+
try {
|
|
240
|
+
const content = readFileSync(join(sessionMetaDir, f), "utf8");
|
|
241
|
+
sessionMetaFiles.push(JSON.parse(content));
|
|
242
|
+
} catch {
|
|
243
|
+
// skip unparseable files
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
// skip if directory unreadable
|
|
503
248
|
}
|
|
504
|
-
} catch {
|
|
505
|
-
// skip if unreadable
|
|
506
249
|
}
|
|
507
|
-
}
|
|
508
250
|
|
|
509
|
-
//
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
251
|
+
// ccusage integration
|
|
252
|
+
let ccusageData = null;
|
|
253
|
+
const ccusageCachePath = join(CLAUDE_DIR, "ccusage-cache.json");
|
|
254
|
+
const CCUSAGE_TTL_MS = 60 * 60 * 1000;
|
|
255
|
+
|
|
513
256
|
try {
|
|
514
|
-
|
|
257
|
+
const cached = JSON.parse(readFileSync(ccusageCachePath, "utf8"));
|
|
258
|
+
if (cached._ts && Date.now() - cached._ts < CCUSAGE_TTL_MS && cached.totals && cached.daily) {
|
|
259
|
+
ccusageData = cached;
|
|
260
|
+
}
|
|
515
261
|
} catch {
|
|
516
|
-
|
|
262
|
+
/* no cache or stale */
|
|
517
263
|
}
|
|
518
|
-
}
|
|
519
264
|
|
|
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
|
-
);
|
|
265
|
+
if (!ccusageData) {
|
|
266
|
+
try {
|
|
267
|
+
const raw = execFileSync("npx", ["ccusage", "--json"], {
|
|
268
|
+
encoding: "utf8",
|
|
269
|
+
timeout: 30_000,
|
|
270
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
271
|
+
});
|
|
272
|
+
const parsed = JSON.parse(raw);
|
|
273
|
+
if (parsed.totals && parsed.daily) {
|
|
274
|
+
ccusageData = parsed;
|
|
275
|
+
try {
|
|
276
|
+
writeFileSync(ccusageCachePath, JSON.stringify({ ...parsed, _ts: Date.now() }));
|
|
277
|
+
} catch {
|
|
278
|
+
/* non-critical */
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} catch {
|
|
282
|
+
// ccusage not installed or timed out
|
|
283
|
+
}
|
|
553
284
|
}
|
|
554
|
-
}
|
|
555
285
|
|
|
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
|
-
});
|
|
286
|
+
// Claude Code Insights report — read raw HTML, pipeline parses it
|
|
287
|
+
let insightsReportHtml = null;
|
|
288
|
+
const reportPath = join(CLAUDE_DIR, "usage-data", "report.html");
|
|
289
|
+
if (existsSync(reportPath)) {
|
|
290
|
+
try {
|
|
291
|
+
insightsReportHtml = readFileSync(reportPath, "utf8");
|
|
292
|
+
} catch {
|
|
293
|
+
// skip if unreadable
|
|
294
|
+
}
|
|
600
295
|
}
|
|
601
|
-
}
|
|
602
296
|
|
|
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);
|
|
297
|
+
// Stats cache
|
|
298
|
+
const statsCachePath = join(CLAUDE_DIR, "stats-cache.json");
|
|
299
|
+
let statsCache = {};
|
|
300
|
+
if (existsSync(statsCachePath)) {
|
|
301
|
+
try {
|
|
302
|
+
statsCache = JSON.parse(readFileSync(statsCachePath, "utf8"));
|
|
303
|
+
} catch {
|
|
304
|
+
// skip if parse fails
|
|
305
|
+
}
|
|
631
306
|
}
|
|
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
307
|
|
|
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
|
-
}
|
|
308
|
+
// Scan scope
|
|
309
|
+
const scanScope = existsSync(CONF) ? `config: ${shortPath(CONF)}` : "~/ (depth 5)";
|
|
661
310
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
311
|
+
return {
|
|
312
|
+
repos,
|
|
313
|
+
globalCmds,
|
|
314
|
+
globalRules,
|
|
315
|
+
globalSkills,
|
|
316
|
+
userMcpServers,
|
|
317
|
+
projectMcpByRepo,
|
|
318
|
+
disabledMcpByRepo,
|
|
319
|
+
historicalMcpMap,
|
|
320
|
+
registryServers,
|
|
321
|
+
sessionMetaFiles,
|
|
322
|
+
ccusageData,
|
|
323
|
+
statsCache,
|
|
324
|
+
insightsReportHtml,
|
|
325
|
+
chains,
|
|
326
|
+
scanScope,
|
|
327
|
+
insightsReportPath: reportPath,
|
|
328
|
+
};
|
|
670
329
|
}
|
|
671
330
|
|
|
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();
|
|
331
|
+
// ── Build Dashboard Data ─────────────────────────────────────────────────────
|
|
679
332
|
|
|
680
|
-
const
|
|
333
|
+
const rawInputs = await collectRawInputs();
|
|
334
|
+
const data = buildDashboardData(rawInputs);
|
|
681
335
|
|
|
682
336
|
// ── Lint Subcommand ──────────────────────────────────────────────────────────
|
|
683
337
|
|
|
684
338
|
if (cliArgs.command === "lint") {
|
|
685
339
|
let totalIssues = 0;
|
|
686
|
-
for (const repo of configured) {
|
|
340
|
+
for (const repo of data.configured) {
|
|
687
341
|
const issues = lintConfig(repo);
|
|
688
342
|
if (issues.length === 0) continue;
|
|
689
343
|
if (!cliArgs.quiet) console.log(`\n${repo.name} (${repo.shortPath}):`);
|
|
@@ -705,7 +359,10 @@ if (cliArgs.command === "lint") {
|
|
|
705
359
|
const SNAPSHOT_PATH = join(CLAUDE_DIR, "dashboard-snapshot.json");
|
|
706
360
|
if (cliArgs.diff) {
|
|
707
361
|
const currentSnapshot = {
|
|
708
|
-
configuredRepos: configured.map((r) => ({
|
|
362
|
+
configuredRepos: data.configured.map((r) => ({
|
|
363
|
+
name: r.name,
|
|
364
|
+
healthScore: r.healthScore || 0,
|
|
365
|
+
})),
|
|
709
366
|
};
|
|
710
367
|
if (existsSync(SNAPSHOT_PATH)) {
|
|
711
368
|
try {
|
|
@@ -732,59 +389,60 @@ if (cliArgs.diff) {
|
|
|
732
389
|
|
|
733
390
|
if (cliArgs.anonymize) {
|
|
734
391
|
anonymizeAll({
|
|
735
|
-
configured,
|
|
736
|
-
unconfigured,
|
|
737
|
-
globalCmds,
|
|
738
|
-
globalRules,
|
|
739
|
-
globalSkills,
|
|
740
|
-
chains,
|
|
741
|
-
mcpSummary,
|
|
742
|
-
mcpPromotions,
|
|
743
|
-
formerMcpServers,
|
|
744
|
-
consolidationGroups,
|
|
392
|
+
configured: data.configured,
|
|
393
|
+
unconfigured: data.unconfigured,
|
|
394
|
+
globalCmds: data.globalCmds,
|
|
395
|
+
globalRules: data.globalRules,
|
|
396
|
+
globalSkills: data.globalSkills,
|
|
397
|
+
chains: data.chains,
|
|
398
|
+
mcpSummary: data.mcpSummary,
|
|
399
|
+
mcpPromotions: data.mcpPromotions,
|
|
400
|
+
formerMcpServers: data.formerMcpServers,
|
|
401
|
+
consolidationGroups: data.consolidationGroups,
|
|
745
402
|
});
|
|
746
403
|
}
|
|
747
404
|
|
|
748
405
|
// ── JSON Output ──────────────────────────────────────────────────────────────
|
|
749
406
|
|
|
750
407
|
if (cliArgs.json) {
|
|
408
|
+
const now = new Date();
|
|
751
409
|
const jsonData = {
|
|
752
410
|
version: VERSION,
|
|
753
411
|
generatedAt: now.toISOString(),
|
|
754
|
-
scanScope,
|
|
412
|
+
scanScope: data.scanScope,
|
|
755
413
|
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
|
|
414
|
+
totalRepos: data.totalRepos,
|
|
415
|
+
configuredRepos: data.configuredCount,
|
|
416
|
+
unconfiguredRepos: data.unconfiguredCount,
|
|
417
|
+
coveragePct: data.coveragePct,
|
|
418
|
+
globalCommands: data.globalCmds.length,
|
|
419
|
+
globalRules: data.globalRules.length,
|
|
420
|
+
skills: data.globalSkills.length,
|
|
421
|
+
repoCommands: data.totalRepoCmds,
|
|
422
|
+
avgHealthScore: data.avgHealth,
|
|
423
|
+
driftingRepos: data.driftCount,
|
|
424
|
+
mcpServers: data.mcpCount,
|
|
425
|
+
...(data.ccusageData
|
|
768
426
|
? {
|
|
769
|
-
totalCost: ccusageData.totals.totalCost,
|
|
770
|
-
totalTokens: ccusageData.totals.totalTokens,
|
|
427
|
+
totalCost: data.ccusageData.totals.totalCost,
|
|
428
|
+
totalTokens: data.ccusageData.totals.totalTokens,
|
|
771
429
|
}
|
|
772
430
|
: {}),
|
|
773
|
-
errorCategories: usageAnalytics.errorCategories,
|
|
431
|
+
errorCategories: data.usageAnalytics.errorCategories,
|
|
774
432
|
},
|
|
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) => ({
|
|
433
|
+
globalCommands: data.globalCmds.map((c) => ({ name: c.name, description: c.desc })),
|
|
434
|
+
globalRules: data.globalRules.map((r) => ({ name: r.name, description: r.desc })),
|
|
435
|
+
skills: data.globalSkills.map((s) => ({
|
|
778
436
|
name: s.name,
|
|
779
437
|
description: s.desc,
|
|
780
438
|
source: s.source,
|
|
781
439
|
category: s.category,
|
|
782
440
|
})),
|
|
783
|
-
chains: chains.map((c) => ({
|
|
441
|
+
chains: data.chains.map((c) => ({
|
|
784
442
|
nodes: c.nodes.map((n) => n.trim()),
|
|
785
443
|
direction: c.arrow === "→" ? "forward" : "backward",
|
|
786
444
|
})),
|
|
787
|
-
configuredRepos: configured.map((r) => ({
|
|
445
|
+
configuredRepos: data.configured.map((r) => ({
|
|
788
446
|
name: r.name,
|
|
789
447
|
path: r.shortPath,
|
|
790
448
|
commands: r.commands.map((c) => ({ name: c.name, description: c.desc })),
|
|
@@ -805,8 +463,8 @@ if (cliArgs.json) {
|
|
|
805
463
|
similarRepos: r.similarRepos || [],
|
|
806
464
|
mcpServers: r.mcpServers || [],
|
|
807
465
|
})),
|
|
808
|
-
consolidationGroups,
|
|
809
|
-
unconfiguredRepos: unconfigured.map((r) => ({
|
|
466
|
+
consolidationGroups: data.consolidationGroups,
|
|
467
|
+
unconfiguredRepos: data.unconfigured.map((r) => ({
|
|
810
468
|
name: r.name,
|
|
811
469
|
path: r.shortPath,
|
|
812
470
|
techStack: r.techStack || [],
|
|
@@ -814,9 +472,9 @@ if (cliArgs.json) {
|
|
|
814
472
|
exemplar: r.exemplarName || "",
|
|
815
473
|
mcpServers: r.mcpServers || [],
|
|
816
474
|
})),
|
|
817
|
-
mcpServers: mcpSummary,
|
|
818
|
-
mcpPromotions,
|
|
819
|
-
formerMcpServers,
|
|
475
|
+
mcpServers: data.mcpSummary,
|
|
476
|
+
mcpPromotions: data.mcpPromotions,
|
|
477
|
+
formerMcpServers: data.formerMcpServers,
|
|
820
478
|
};
|
|
821
479
|
|
|
822
480
|
const jsonOutput = JSON.stringify(jsonData, null, 2);
|
|
@@ -834,8 +492,8 @@ if (cliArgs.json) {
|
|
|
834
492
|
// ── Catalog Output ───────────────────────────────────────────────────────────
|
|
835
493
|
|
|
836
494
|
if (cliArgs.catalog) {
|
|
837
|
-
const groups = groupSkillsByCategory(globalSkills);
|
|
838
|
-
const catalogHtml = generateCatalogHtml(groups, globalSkills.length, timestamp);
|
|
495
|
+
const groups = groupSkillsByCategory(data.globalSkills);
|
|
496
|
+
const catalogHtml = generateCatalogHtml(groups, data.globalSkills.length, data.timestamp);
|
|
839
497
|
const outputPath =
|
|
840
498
|
cliArgs.output !== DEFAULT_OUTPUT ? cliArgs.output : join(CLAUDE_DIR, "skill-catalog.html");
|
|
841
499
|
mkdirSync(dirname(outputPath), { recursive: true });
|
|
@@ -851,33 +509,7 @@ if (cliArgs.catalog) {
|
|
|
851
509
|
|
|
852
510
|
// ── Generate HTML Dashboard ──────────────────────────────────────────────────
|
|
853
511
|
|
|
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
|
-
});
|
|
512
|
+
const html = generateDashboardHtml(data);
|
|
881
513
|
|
|
882
514
|
// ── Write HTML Output ────────────────────────────────────────────────────────
|
|
883
515
|
|
|
@@ -889,10 +521,10 @@ if (!cliArgs.quiet) {
|
|
|
889
521
|
const sp = shortPath(outputPath);
|
|
890
522
|
console.log(`\n claude-code-dashboard v${VERSION}\n`);
|
|
891
523
|
console.log(
|
|
892
|
-
` ${configuredCount} configured · ${unconfiguredCount} unconfigured · ${totalRepos} repos`,
|
|
524
|
+
` ${data.configuredCount} configured · ${data.unconfiguredCount} unconfigured · ${data.totalRepos} repos`,
|
|
893
525
|
);
|
|
894
526
|
console.log(
|
|
895
|
-
` ${globalCmds.length} global commands · ${globalSkills.length} skills · ${mcpCount} MCP servers`,
|
|
527
|
+
` ${data.globalCmds.length} global commands · ${data.globalSkills.length} skills · ${data.mcpCount} MCP servers`,
|
|
896
528
|
);
|
|
897
529
|
console.log(`\n ✓ ${sp}`);
|
|
898
530
|
if (cliArgs.open) console.log(` ✓ opening in browser`);
|
|
@@ -909,5 +541,5 @@ if (cliArgs.open) {
|
|
|
909
541
|
// ── Watch Mode ───────────────────────────────────────────────────────────────
|
|
910
542
|
|
|
911
543
|
if (cliArgs.watch) {
|
|
912
|
-
startWatch(outputPath,
|
|
544
|
+
startWatch(outputPath, getScanRoots(), cliArgs);
|
|
913
545
|
}
|