@viren/claude-code-dashboard 0.0.1

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.
@@ -0,0 +1,221 @@
1
+ import { existsSync, writeFileSync } from "fs";
2
+ import { join, basename } from "path";
3
+ import { CONF, MAX_DEPTH } from "./constants.mjs";
4
+ import {
5
+ detectTechStack,
6
+ findExemplar,
7
+ detectConfigPattern,
8
+ computeHealthScore,
9
+ } from "./analysis.mjs";
10
+ import { getFreshness, freshnessClass } from "./freshness.mjs";
11
+ import { scanMdDir, extractSections, extractProjectDesc } from "./markdown.mjs";
12
+ import { findGitRepos, getScanRoots } from "./discovery.mjs";
13
+
14
+ export const TEMPLATE_SECTIONS = {
15
+ next: {
16
+ purpose: "Next.js web application",
17
+ commands: "npm run dev, npm run build, npm run lint, npm test",
18
+ rules: [
19
+ "Use App Router conventions",
20
+ "Server components by default, 'use client' only when needed",
21
+ "Use TypeScript strict mode",
22
+ ],
23
+ },
24
+ react: {
25
+ purpose: "React application",
26
+ commands: "npm start, npm run build, npm run lint, npm test",
27
+ rules: [
28
+ "Functional components with hooks",
29
+ "Co-locate component, test, and styles",
30
+ "Use TypeScript strict mode",
31
+ ],
32
+ },
33
+ python: {
34
+ purpose: "Python application",
35
+ commands: "pytest, ruff check ., ruff format .",
36
+ rules: [
37
+ "Type hints on all public functions",
38
+ "Use dataclasses/pydantic for data models",
39
+ "Keep modules focused and small",
40
+ ],
41
+ },
42
+ node: {
43
+ purpose: "Node.js application",
44
+ commands: "npm start, npm test, npm run lint",
45
+ rules: [
46
+ "Use ES modules (import/export)",
47
+ "Handle errors explicitly, never swallow",
48
+ "Use async/await over callbacks",
49
+ ],
50
+ },
51
+ go: {
52
+ purpose: "Go application",
53
+ commands: "go build ./..., go test ./..., golangci-lint run",
54
+ rules: [
55
+ "Handle all errors explicitly",
56
+ "Use interfaces for testability",
57
+ "Keep packages focused",
58
+ ],
59
+ },
60
+ expo: {
61
+ purpose: "Expo/React Native mobile application",
62
+ commands: "npx expo start, npm test, npm run lint",
63
+ rules: [
64
+ "Use Expo SDK APIs over bare React Native",
65
+ "Test on both iOS and Android",
66
+ "Use TypeScript strict mode",
67
+ ],
68
+ },
69
+ rust: {
70
+ purpose: "Rust application",
71
+ commands: "cargo build, cargo test, cargo clippy",
72
+ rules: [
73
+ "Prefer owned types in public APIs",
74
+ "Use Result for fallible operations",
75
+ "Document public items",
76
+ ],
77
+ },
78
+ swift: {
79
+ purpose: "Swift application",
80
+ commands: "swift build, swift test",
81
+ rules: [
82
+ "Use Swift concurrency (async/await)",
83
+ "Protocol-oriented design",
84
+ "Prefer value types",
85
+ ],
86
+ },
87
+ generic: {
88
+ purpose: "Software project",
89
+ commands: "",
90
+ rules: [
91
+ "Follow existing patterns in the codebase",
92
+ "Test before committing",
93
+ "Keep functions focused and small",
94
+ ],
95
+ },
96
+ };
97
+
98
+ function generateTemplate(stack, exemplar, pattern) {
99
+ const t = TEMPLATE_SECTIONS[stack] || TEMPLATE_SECTIONS.generic;
100
+ const lines = [];
101
+
102
+ lines.push(`# ${basename(process.cwd())}`);
103
+ lines.push("");
104
+ lines.push(`> ${t.purpose}`);
105
+ lines.push("");
106
+
107
+ if (t.commands) {
108
+ lines.push("## Commands");
109
+ lines.push("");
110
+ for (const cmd of t.commands.split(", ")) {
111
+ lines.push(`- \`${cmd}\``);
112
+ }
113
+ lines.push("");
114
+ }
115
+
116
+ lines.push("## Architecture");
117
+ lines.push("");
118
+ lines.push("<!-- Describe key directories, data flow, and patterns -->");
119
+ lines.push("");
120
+
121
+ lines.push("## Rules");
122
+ lines.push("");
123
+ for (const rule of t.rules) {
124
+ lines.push(`- ${rule}`);
125
+ }
126
+ lines.push("");
127
+
128
+ lines.push("## Quality Gates");
129
+ lines.push("");
130
+ lines.push("- [ ] All tests passing");
131
+ lines.push("- [ ] Linter clean");
132
+ const tsStacks = new Set(["next", "react", "expo", "node"]);
133
+ if (tsStacks.has(stack)) {
134
+ lines.push("- [ ] No TypeScript errors");
135
+ }
136
+ lines.push("");
137
+
138
+ if (exemplar) {
139
+ lines.push(
140
+ `<!-- Based on ${exemplar.name} (health: ${exemplar.healthScore}, pattern: ${pattern}) -->`,
141
+ );
142
+ }
143
+
144
+ return lines.join("\n");
145
+ }
146
+
147
+ export function handleInit(cliArgs) {
148
+ const cwd = process.cwd();
149
+ const stackInfo = detectTechStack(cwd);
150
+ const stack = cliArgs.template || stackInfo.stacks[0] || "generic";
151
+
152
+ if (cliArgs.template && !TEMPLATE_SECTIONS[cliArgs.template]) {
153
+ console.error(
154
+ `Warning: unknown stack '${cliArgs.template}', using generic template. Available: ${Object.keys(TEMPLATE_SECTIONS).join(", ")}`,
155
+ );
156
+ }
157
+
158
+ // Scan repos to find exemplar
159
+ let exemplar = null;
160
+ let pattern = "minimal";
161
+ if (existsSync(CONF)) {
162
+ const initRoots = getScanRoots();
163
+ const initRepoPaths = findGitRepos(initRoots, MAX_DEPTH);
164
+ const configuredForInit = [];
165
+ for (const repoDir of initRepoPaths) {
166
+ const commands = scanMdDir(join(repoDir, ".claude", "commands"));
167
+ const rules = scanMdDir(join(repoDir, ".claude", "rules"));
168
+ let agentsFile = null;
169
+ if (existsSync(join(repoDir, "AGENTS.md"))) agentsFile = join(repoDir, "AGENTS.md");
170
+ else if (existsSync(join(repoDir, "CLAUDE.md"))) agentsFile = join(repoDir, "CLAUDE.md");
171
+ if (!agentsFile && commands.length === 0 && rules.length === 0) continue;
172
+ const sections = agentsFile ? extractSections(agentsFile) : [];
173
+ const ts = detectTechStack(repoDir);
174
+ const fc = freshnessClass(getFreshness(repoDir));
175
+ const health = computeHealthScore({
176
+ hasAgentsFile: !!agentsFile,
177
+ desc: agentsFile ? extractProjectDesc(agentsFile) : [],
178
+ commandCount: commands.length,
179
+ ruleCount: rules.length,
180
+ sectionCount: sections.length,
181
+ freshnessClass: fc,
182
+ });
183
+ configuredForInit.push({
184
+ name: basename(repoDir),
185
+ commands,
186
+ rules,
187
+ sections,
188
+ techStack: ts.stacks,
189
+ healthScore: health.score,
190
+ hasAgentsFile: !!agentsFile,
191
+ });
192
+ }
193
+ exemplar = findExemplar([stack], configuredForInit);
194
+ if (exemplar) pattern = detectConfigPattern(exemplar);
195
+ }
196
+
197
+ const content = generateTemplate(stack, exemplar, pattern);
198
+ const claudeMdPath = join(cwd, "CLAUDE.md");
199
+
200
+ if (cliArgs.dryRun) {
201
+ console.log(`Would create: ${claudeMdPath}`);
202
+ console.log(`Stack: ${stack}`);
203
+ if (exemplar) console.log(`Exemplar: ${exemplar.name} (${pattern})`);
204
+ console.log("---");
205
+ console.log(content);
206
+ process.exit(0);
207
+ }
208
+
209
+ if (existsSync(claudeMdPath)) {
210
+ console.error(
211
+ `Error: ${claudeMdPath} already exists. Remove it first or use --dry-run to preview.`,
212
+ );
213
+ process.exit(1);
214
+ }
215
+
216
+ writeFileSync(claudeMdPath, content);
217
+ console.log(
218
+ `Created ${claudeMdPath} (stack: ${stack}${exemplar ? `, based on ${exemplar.name}` : ""})`,
219
+ );
220
+ process.exit(0);
221
+ }
package/src/usage.mjs ADDED
@@ -0,0 +1,60 @@
1
+ export function aggregateSessionMeta(sessions) {
2
+ if (!sessions || sessions.length === 0) {
3
+ return {
4
+ totalSessions: 0,
5
+ totalDuration: 0,
6
+ topTools: [],
7
+ topLanguages: [],
8
+ errorCategories: [],
9
+ heavySessions: 0,
10
+ };
11
+ }
12
+
13
+ let totalDuration = 0;
14
+ const toolCounts = {};
15
+ const langCounts = {};
16
+ const errorCounts = {};
17
+
18
+ for (const s of sessions) {
19
+ totalDuration += s.duration_minutes || 0;
20
+
21
+ if (s.tool_counts) {
22
+ for (const [name, count] of Object.entries(s.tool_counts)) {
23
+ toolCounts[name] = (toolCounts[name] || 0) + count;
24
+ }
25
+ }
26
+
27
+ if (s.languages) {
28
+ for (const [name, count] of Object.entries(s.languages)) {
29
+ langCounts[name] = (langCounts[name] || 0) + count;
30
+ }
31
+ }
32
+
33
+ if (s.tool_error_categories) {
34
+ for (const [name, count] of Object.entries(s.tool_error_categories)) {
35
+ errorCounts[name] = (errorCounts[name] || 0) + count;
36
+ }
37
+ }
38
+ }
39
+
40
+ let heavySessions = 0;
41
+ for (const s of sessions) {
42
+ const msgs = (s.user_message_count || 0) + (s.assistant_message_count || 0);
43
+ if (msgs > 50 || (s.duration_minutes || 0) > 30) heavySessions++;
44
+ }
45
+
46
+ const sortDesc = (obj, limit) =>
47
+ Object.entries(obj)
48
+ .map(([name, count]) => ({ name, count }))
49
+ .sort((a, b) => b.count - a.count)
50
+ .slice(0, limit);
51
+
52
+ return {
53
+ totalSessions: sessions.length,
54
+ totalDuration,
55
+ topTools: sortDesc(toolCounts, 10),
56
+ topLanguages: sortDesc(langCounts, 8),
57
+ errorCategories: sortDesc(errorCounts, 5),
58
+ heavySessions,
59
+ };
60
+ }
package/src/watch.mjs ADDED
@@ -0,0 +1,54 @@
1
+ import { execFileSync } from "child_process";
2
+ import { watch as fsWatch } from "fs";
3
+ import { basename, resolve } from "path";
4
+ import { CLAUDE_DIR } from "./constants.mjs";
5
+
6
+ export function startWatch(outputPath, scanRoots, cliArgs) {
7
+ if (!cliArgs.quiet) console.log("Watching for changes...");
8
+ let debounce = null;
9
+ let regenerating = false;
10
+ const watchDirs = [CLAUDE_DIR, ...scanRoots.slice(0, 5)];
11
+
12
+ // Forward original flags minus --watch and --diff to avoid nested watchers
13
+ // and noisy snapshot writes on every file change
14
+ const forwardedArgs = process.argv
15
+ .slice(2)
16
+ .filter((a) => a !== "--watch" && a !== "--diff")
17
+ .concat(["--quiet"]);
18
+
19
+ // Resolve output path to detect and ignore self-writes
20
+ const resolvedOutput = resolve(outputPath);
21
+
22
+ function regenerate(_eventType, filename) {
23
+ // Ignore changes to our own output file and cache files to prevent infinite loops
24
+ if (
25
+ filename &&
26
+ (filename === basename(resolvedOutput) ||
27
+ filename === "dashboard-snapshot.json" ||
28
+ filename === "ccusage-cache.json")
29
+ )
30
+ return;
31
+ if (regenerating) return;
32
+ if (debounce) globalThis.clearTimeout(debounce);
33
+ debounce = globalThis.setTimeout(() => {
34
+ regenerating = true;
35
+ if (!cliArgs.quiet) console.log("Change detected, regenerating...");
36
+ try {
37
+ execFileSync(process.execPath, [process.argv[1], ...forwardedArgs], {
38
+ stdio: "inherit",
39
+ });
40
+ if (!cliArgs.quiet) console.log(outputPath);
41
+ } catch (e) {
42
+ console.error("Regeneration failed:", e.message);
43
+ }
44
+ regenerating = false;
45
+ }, 500);
46
+ }
47
+ for (const dir of watchDirs) {
48
+ try {
49
+ fsWatch(dir, { recursive: true }, regenerate);
50
+ } catch {
51
+ /* unreadable */
52
+ }
53
+ }
54
+ }