skill-statusline 2.3.1 → 2.4.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.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # skill-statusline
2
2
 
3
- Rich, themeable statusline for Claude Code with accurate context tracking, 5 color themes, 3 layout modes, and zero dependencies.
3
+ Rich, themeable statusline for Claude Code with accurate context tracking, 5 color themes, 3 layout modes, and zero dependencies. Uses a fast Node.js renderer on Windows (no Git Bash overhead), pure bash on Unix.
4
4
 
5
5
  ## Install
6
6
 
@@ -124,13 +124,19 @@ Stored in `~/.claude/statusline-config.json`:
124
124
 
125
125
  ## Architecture
126
126
 
127
- Pure bash at runtime zero dependencies. Node.js CLI is only for installation.
127
+ Two rendering enginesthe installer picks the right one for your platform:
128
+
129
+ - **Windows**: Node.js renderer (`statusline-node.js`) — single process, no subprocess spawning, ~30-50ms
130
+ - **macOS/Linux**: Bash engine (`core.sh`) — pure bash, zero dependencies, <50ms with caching
131
+
132
+ Git Bash on Windows has ~50-100ms overhead *per subprocess spawn* (awk, sed, grep, git, date). A bash statusline spawning 10-20 subprocesses = 500-1000ms. The Node.js renderer eliminates this by doing everything in a single process.
128
133
 
129
134
  ```
130
135
  ~/.claude/
131
- statusline-command.sh # Entry point (called by Claude Code)
136
+ statusline-command.sh # Bash entry point (macOS/Linux)
137
+ statusline-node.js # Node.js renderer (Windows)
132
138
  statusline/
133
- core.sh # Engine: parse JSON, compute fields, render
139
+ core.sh # Bash engine: parse JSON, compute fields, render
134
140
  json-parser.sh # Nested JSON extraction (no jq)
135
141
  helpers.sh # Utilities + filesystem caching
136
142
  themes/{default,nord,...}.sh # Color palettes
@@ -138,15 +144,13 @@ Pure bash at runtime — zero dependencies. Node.js CLI is only for installation
138
144
  statusline-config.json # User preferences
139
145
  ```
140
146
 
141
- Performance: <50ms with caching, <100ms cold. Git and transcript reads are cached with configurable TTL.
142
-
143
147
  Terminal width is auto-detected — layouts gracefully degrade on narrow terminals.
144
148
 
145
149
  ## Requirements
146
150
 
147
- - Bash (Git Bash on Windows, or any Unix shell)
151
+ - Node.js >=16 (installer + Windows renderer)
148
152
  - Git (for GitHub field)
149
- - Node.js >=16 (for installer only)
153
+ - Bash (macOS/Linux runtime only — not needed on Windows)
150
154
  - Works on Windows, macOS, Linux
151
155
 
152
156
  ## Part of the Thinqmesh Skills Ecosystem
package/bin/cli.js CHANGED
@@ -9,7 +9,7 @@ const readline = require('readline');
9
9
  const args = process.argv.slice(2);
10
10
  const command = args[0];
11
11
  const subcommand = args[1];
12
- const VERSION = '2.2.2';
12
+ const VERSION = '2.4.1';
13
13
 
14
14
  const PKG_DIR = path.resolve(__dirname, '..');
15
15
  const HOME = os.homedir();
@@ -110,16 +110,25 @@ function writeSettings(settings) {
110
110
  // instead of the built-in statusline-setup agent (which fails on Windows
111
111
  // and conflicts with our setup).
112
112
 
113
- const CLAUDE_MD_SECTION = `${CLAUDE_MD_START}
113
+ function getClaudeMdSection() {
114
+ const isWin = process.platform === 'win32';
115
+ const howItWorks = isWin
116
+ ? `### How It Works
117
+ - \`~/.claude/settings.json\` → runs \`node ~/.claude/statusline-node.js\`
118
+ - \`~/.claude/statusline-node.js\` → Node.js renderer (fast on Windows, no bash overhead)
119
+ - \`~/.claude/statusline-config.json\` → user preferences (theme, layout, options)`
120
+ : `### How It Works
121
+ - \`~/.claude/settings.json\` → points to \`bash ~/.claude/statusline-command.sh\`
122
+ - \`~/.claude/statusline-command.sh\` → entry point, delegates to v2 engine
123
+ - \`~/.claude/statusline/core.sh\` → v2 engine (themes, layouts, accurate context tracking)
124
+ - \`~/.claude/statusline-config.json\` → user preferences (theme, layout, options)`;
125
+
126
+ return `${CLAUDE_MD_START}
114
127
  ## Statusline — Managed by skill-statusline v2
115
128
 
116
129
  The Claude Code statusline is fully configured and managed by the \`skill-statusline\` npm package. Do NOT use the built-in \`statusline-setup\` agent for any statusline configuration — it is not compatible with this setup.
117
130
 
118
- ### How It Works
119
- - \`~/.claude/settings.json\` → points to \`bash ~/.claude/statusline-command.sh\`
120
- - \`~/.claude/statusline-command.sh\` → entry point, delegates to v2 engine
121
- - \`~/.claude/statusline/core.sh\` → v2 engine (themes, layouts, accurate context tracking)
122
- - \`~/.claude/statusline-config.json\` → user preferences (theme, layout, options)
131
+ ${howItWorks}
123
132
 
124
133
  ### When the user asks about statusline configuration, use these CLI commands:
125
134
 
@@ -157,6 +166,7 @@ Users can also use these slash commands inside Claude Code:
157
166
  - All statusline changes are made via \`ccsl\` CLI commands (run in terminal) or \`/sls-*\` slash commands
158
167
  - Changes take effect on next Claude Code restart (or next statusline refresh for config changes)
159
168
  ${CLAUDE_MD_END}`;
169
+ }
160
170
 
161
171
  function installClaudeMd() {
162
172
  let content = '';
@@ -171,7 +181,7 @@ function installClaudeMd() {
171
181
  }
172
182
  }
173
183
  // Append our section
174
- content = content ? content + '\n\n' + CLAUDE_MD_SECTION + '\n' : CLAUDE_MD_SECTION + '\n';
184
+ content = content ? content + '\n\n' + getClaudeMdSection() + '\n' : getClaudeMdSection() + '\n';
175
185
  fs.writeFileSync(CLAUDE_MD_PATH, content);
176
186
  }
177
187
 
@@ -271,10 +281,17 @@ function installFiles() {
271
281
  copyDir(layoutsSrc, path.join(SL_DIR, 'layouts'));
272
282
  }
273
283
 
274
- // Copy entry point
284
+ // Copy entry point (bash)
275
285
  const slSrc = path.join(PKG_DIR, 'bin', 'statusline.sh');
276
286
  fs.copyFileSync(slSrc, SCRIPT_DEST);
277
287
 
288
+ // Copy Node.js renderer (used on Windows for performance)
289
+ const nodeSrc = path.join(PKG_DIR, 'bin', 'statusline-node.js');
290
+ const nodeDest = path.join(CLAUDE_DIR, 'statusline-node.js');
291
+ if (fs.existsSync(nodeSrc)) {
292
+ fs.copyFileSync(nodeSrc, nodeDest);
293
+ }
294
+
278
295
  return true;
279
296
  }
280
297
 
@@ -344,31 +361,42 @@ async function install() {
344
361
  }
345
362
 
346
363
  // Install files
364
+ const isWin = process.platform === 'win32';
347
365
  installFiles();
348
- success(`${B}statusline/${R} directory installed to ~/.claude/`);
366
+ if (isWin) {
367
+ success(`${B}statusline-node.js${R} renderer installed to ~/.claude/`);
368
+ } else {
369
+ success(`${B}statusline/${R} engine installed to ~/.claude/`);
370
+ }
349
371
 
350
372
  // Write config
351
373
  if (!config.options) config.options = {};
352
374
  writeConfig(config);
353
375
  success(`Config: theme=${CYN}${config.theme}${R}, layout=${CYN}${config.layout}${R}`);
354
376
 
355
- // Update settings.json — use absolute paths on Windows to avoid WSL bash conflict
377
+ // Update settings.json
356
378
  const settings = readSettings();
357
- if (!settings.statusLine || settings.statusLine.command === 'bash ~/.claude/statusline-command.sh') {
358
- const isWin = process.platform === 'win32';
379
+ const prevCmd = settings.statusLine?.command || '';
380
+ const needsUpdate = !settings.statusLine
381
+ || prevCmd === 'bash ~/.claude/statusline-command.sh'
382
+ || (isWin && prevCmd.includes('bash.exe'))
383
+ || (isWin && prevCmd.includes('\\\\'));
384
+ if (needsUpdate) {
359
385
  let cmd;
360
386
  if (isWin) {
361
- // Windows has WSL bash at C:\Windows\System32\bash.exe which resolves ~ to /root/
362
- // Must use Git Bash explicitly with absolute Windows paths
363
- const gitBash = 'C:\\\\Program Files\\\\Git\\\\usr\\\\bin\\\\bash.exe';
364
- const script = SCRIPT_DEST.replace(/\//g, '\\\\');
365
- cmd = `"${gitBash}" "${script}"`;
387
+ // Windows: use Node.js renderer directly avoids Git Bash/MSYS2 overhead
388
+ // (~50-100ms per subprocess spawn in Git Bash vs single Node.js process)
389
+ const nodeScript = path.join(CLAUDE_DIR, 'statusline-node.js');
390
+ cmd = `node "${nodeScript.replace(/\\/g, '/')}"`;
366
391
  } else {
367
392
  cmd = 'bash ~/.claude/statusline-command.sh';
368
393
  }
369
394
  settings.statusLine = { type: 'command', command: cmd };
370
395
  writeSettings(settings);
371
396
  success(`${B}statusLine${R} config added to settings.json`);
397
+ if (isWin) {
398
+ info(`Using Node.js renderer (fast on Windows)`);
399
+ }
372
400
  } else {
373
401
  success(`statusLine already configured in settings.json`);
374
402
  }
@@ -408,8 +436,12 @@ async function install() {
408
436
  }
409
437
 
410
438
  blank();
411
- bar(`Script: ${R}${CYN}~/.claude/statusline-command.sh${R}`);
412
- bar(`Engine: ${R}${CYN}~/.claude/statusline/core.sh${R}`);
439
+ if (isWin) {
440
+ bar(`Renderer: ${R}${CYN}~/.claude/statusline-node.js${R} ${D}(Node.js)${R}`);
441
+ } else {
442
+ bar(`Script: ${R}${CYN}~/.claude/statusline-command.sh${R}`);
443
+ bar(`Engine: ${R}${CYN}~/.claude/statusline/core.sh${R}`);
444
+ }
413
445
  bar(`Config: ${R}${CYN}~/.claude/statusline-config.json${R}`);
414
446
  bar(`Settings: ${R}${CYN}~/.claude/settings.json${R}`);
415
447
  blank();
@@ -431,13 +463,18 @@ function uninstall() {
431
463
  success(`Removed ~/.claude/statusline/`);
432
464
  }
433
465
 
434
- // Remove script
466
+ // Remove scripts
435
467
  if (fs.existsSync(SCRIPT_DEST)) {
436
468
  fs.unlinkSync(SCRIPT_DEST);
437
469
  success(`Removed ~/.claude/statusline-command.sh`);
438
470
  } else {
439
471
  warn(`statusline-command.sh not found`);
440
472
  }
473
+ const nodeScript = path.join(CLAUDE_DIR, 'statusline-node.js');
474
+ if (fs.existsSync(nodeScript)) {
475
+ fs.unlinkSync(nodeScript);
476
+ success(`Removed ~/.claude/statusline-node.js`);
477
+ }
441
478
 
442
479
  // Remove config
443
480
  if (fs.existsSync(CONFIG_PATH)) {
@@ -524,40 +561,53 @@ function preview() {
524
561
  agent: { name: 'code-reviewer' }
525
562
  });
526
563
 
527
- // Check if v2 engine is installed
528
- const coreFile = path.join(SL_DIR, 'core.sh');
529
- let scriptPath;
530
- if (fs.existsSync(coreFile)) {
531
- scriptPath = coreFile;
532
- } else if (fs.existsSync(SCRIPT_DEST)) {
533
- scriptPath = SCRIPT_DEST;
564
+ const isWin = process.platform === 'win32';
565
+
566
+ // On Windows, prefer Node.js renderer for speed
567
+ const nodeRendererInstalled = path.join(CLAUDE_DIR, 'statusline-node.js');
568
+ const nodeRendererPkg = path.join(PKG_DIR, 'bin', 'statusline-node.js');
569
+ const nodeRenderer = fs.existsSync(nodeRendererInstalled) ? nodeRendererInstalled : nodeRendererPkg;
570
+
571
+ if (isWin || !fs.existsSync(path.join(SL_DIR, 'core.sh'))) {
572
+ // Use Node.js renderer
573
+ try {
574
+ const result = execSync(`node "${nodeRenderer.replace(/\\/g, '/')}"`, {
575
+ input: sampleJson,
576
+ encoding: 'utf8',
577
+ timeout: 5000
578
+ });
579
+ log('');
580
+ log(result);
581
+ log('');
582
+ } catch (e) {
583
+ warn(`Preview failed: ${e.message}`);
584
+ }
534
585
  } else {
535
- // Use the package's own script
536
- scriptPath = path.join(PKG_DIR, 'lib', 'core.sh');
537
- }
586
+ // Unix: use bash engine with theme/layout overrides
587
+ const coreFile = path.join(SL_DIR, 'core.sh');
588
+ const scriptPath = fs.existsSync(coreFile) ? coreFile : SCRIPT_DEST;
538
589
 
539
- const env = { ...process.env };
540
- if (themeName) env.STATUSLINE_THEME_OVERRIDE = themeName;
541
- if (layoutName) env.STATUSLINE_LAYOUT_OVERRIDE = layoutName;
590
+ const env = { ...process.env };
591
+ if (themeName) env.STATUSLINE_THEME_OVERRIDE = themeName;
592
+ if (layoutName) env.STATUSLINE_LAYOUT_OVERRIDE = layoutName;
542
593
 
543
- // For preview with package's own files, set STATUSLINE_DIR
544
- if (!fs.existsSync(path.join(SL_DIR, 'core.sh'))) {
545
- // Point to package's own lib directory structure
546
- env.HOME = PKG_DIR;
547
- }
594
+ if (!fs.existsSync(path.join(SL_DIR, 'core.sh'))) {
595
+ env.HOME = PKG_DIR;
596
+ }
548
597
 
549
- try {
550
- const escaped = sampleJson.replace(/'/g, "'\\''");
551
- const result = execSync(`printf '%s' '${escaped}' | bash "${scriptPath.replace(/\\/g, '/')}"`, {
552
- encoding: 'utf8',
553
- env,
554
- timeout: 5000
555
- });
556
- log('');
557
- log(result);
558
- log('');
559
- } catch (e) {
560
- warn(`Preview failed: ${e.message}`);
598
+ try {
599
+ const escaped = sampleJson.replace(/'/g, "'\\''");
600
+ const result = execSync(`printf '%s' '${escaped}' | bash "${scriptPath.replace(/\\/g, '/')}"`, {
601
+ encoding: 'utf8',
602
+ env,
603
+ timeout: 5000
604
+ });
605
+ log('');
606
+ log(result);
607
+ log('');
608
+ } catch (e) {
609
+ warn(`Preview failed: ${e.message}`);
610
+ }
561
611
  }
562
612
  }
563
613
 
@@ -816,23 +866,55 @@ function doctor() {
816
866
  warn(`No config file (using defaults)`);
817
867
  }
818
868
 
819
- // 9. Performance benchmark
869
+ // 9. Node.js renderer
870
+ const nodeRendererPath = path.join(CLAUDE_DIR, 'statusline-node.js');
871
+ if (fs.existsSync(nodeRendererPath)) {
872
+ success(`Node.js renderer installed (statusline-node.js)`);
873
+ } else if (process.platform === 'win32') {
874
+ fail(`Node.js renderer missing — required on Windows`);
875
+ info(`Run ${CYN}ccsl install${R} to fix`);
876
+ issues++;
877
+ } else {
878
+ bar(`Node.js renderer not installed (optional on Unix)`);
879
+ }
880
+
881
+ // 10. Performance benchmark
820
882
  blank();
821
883
  info(`${B}Performance benchmark${R}`);
822
884
  blank();
823
885
  try {
824
886
  const sampleJson = '{"model":{"id":"claude-opus-4-6","display_name":"Opus"},"workspace":{"current_dir":"/tmp"},"cost":{"total_cost_usd":0.5},"context_window":{"context_window_size":200000,"used_percentage":50,"current_usage":{"input_tokens":90000,"output_tokens":10000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}';
825
- const target = fs.existsSync(coreFile) ? coreFile : SCRIPT_DEST;
826
- if (target && fs.existsSync(target)) {
887
+
888
+ // Benchmark Node.js renderer
889
+ if (fs.existsSync(nodeRendererPath)) {
827
890
  const start = Date.now();
828
- execSync(`printf '%s' '${sampleJson}' | bash "${target.replace(/\\/g, '/')}"`, {
891
+ execSync(`node "${nodeRendererPath.replace(/\\/g, '/')}"`, {
892
+ input: sampleJson,
829
893
  encoding: 'utf8',
830
894
  timeout: 10000
831
895
  });
832
896
  const elapsed = Date.now() - start;
833
897
  const color = elapsed < 50 ? GRN : elapsed < 100 ? YLW : RED;
834
898
  const label = elapsed < 50 ? 'excellent' : elapsed < 100 ? 'good' : 'slow';
835
- log(` ${GRAY}\u2502${R} ${color}\u25CF${R} Execution: ${color}${B}${elapsed}ms${R} (${label}, target: <50ms)`);
899
+ log(` ${GRAY}\u2502${R} ${color}\u25CF${R} Node.js renderer: ${color}${B}${elapsed}ms${R} (${label})`);
900
+ }
901
+
902
+ // Benchmark bash engine (skip on Windows — known slow)
903
+ if (process.platform !== 'win32') {
904
+ const target = fs.existsSync(coreFile) ? coreFile : SCRIPT_DEST;
905
+ if (target && fs.existsSync(target)) {
906
+ const start = Date.now();
907
+ execSync(`printf '%s' '${sampleJson}' | bash "${target.replace(/\\/g, '/')}"`, {
908
+ encoding: 'utf8',
909
+ timeout: 10000
910
+ });
911
+ const elapsed = Date.now() - start;
912
+ const color = elapsed < 50 ? GRN : elapsed < 100 ? YLW : RED;
913
+ const label = elapsed < 50 ? 'excellent' : elapsed < 100 ? 'good' : 'slow';
914
+ log(` ${GRAY}\u2502${R} ${color}\u25CF${R} Bash engine: ${color}${B}${elapsed}ms${R} (${label})`);
915
+ }
916
+ } else {
917
+ bar(`Bash benchmark skipped (slow on Windows, using Node.js)`);
836
918
  }
837
919
  } catch (e) {
838
920
  fail(`Benchmark failed: ${e.message.substring(0, 50)}`);
@@ -1,17 +1,27 @@
1
1
  #!/usr/bin/env node
2
- // skill-statusline v2.3 — Node.js statusline renderer
2
+ // skill-statusline v2.4 — Node.js statusline renderer
3
3
  // Zero bash dependency — works on Windows, macOS, Linux
4
- // Synchronous stdin read + transcript tail for live activity
4
+ // Async stdin with hard 1.5s timeout to prevent hangs
5
5
 
6
6
  'use strict';
7
7
  const fs = require('fs');
8
8
  const path = require('path');
9
- try {
10
- const input = fs.readFileSync(0, 'utf8');
11
- if (input) render(JSON.parse(input));
12
- } catch (e) { /* silent exit on any error */ }
9
+
10
+ // Hard kill prevents hanging if stdin pipe never closes on Windows
11
+ setTimeout(() => process.exit(0), 1500);
12
+
13
+ let input = '';
14
+ process.stdin.setEncoding('utf8');
15
+ process.stdin.on('data', c => input += c);
16
+ process.stdin.on('end', () => {
17
+ try { if (input) render(JSON.parse(input)); } catch (e) {}
18
+ process.exit(0);
19
+ });
20
+ process.stdin.on('error', () => process.exit(0));
21
+ process.stdin.resume();
13
22
 
14
23
  function getActivity(transcriptPath) {
24
+ if (!transcriptPath) return 'Idle';
15
25
  try {
16
26
  const stat = fs.statSync(transcriptPath);
17
27
  const readSize = Math.min(16384, stat.size);
@@ -29,7 +39,6 @@ function getActivity(transcriptPath) {
29
39
  const last = toolUses[toolUses.length - 1];
30
40
  const name = last.name;
31
41
  const inp = last.input || {};
32
- // Enrich with context for special tools
33
42
  if (name === 'Task' && inp.subagent_type) {
34
43
  const desc = inp.description ? ': ' + inp.description.slice(0, 25) : '';
35
44
  return `Task(${inp.subagent_type}${desc})`;
@@ -50,13 +59,11 @@ function getGitInfo(projectDir) {
50
59
  const gitHead = fs.readFileSync(path.join(projectDir, '.git', 'HEAD'), 'utf8').trim();
51
60
  branch = gitHead.startsWith('ref: refs/heads/') ? gitHead.slice(16) : gitHead.slice(0, 7);
52
61
  } catch (e) { return 'no-git'; }
53
- // Parse remote URL from .git/config
54
62
  try {
55
63
  const config = fs.readFileSync(path.join(projectDir, '.git', 'config'), 'utf8');
56
64
  const urlMatch = config.match(/\[remote "origin"\][^[]*url\s*=\s*(.+)/);
57
65
  if (urlMatch) {
58
66
  const url = urlMatch[1].trim();
59
- // Extract owner/repo from various URL formats
60
67
  const ghMatch = url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
61
68
  if (ghMatch) remote = ghMatch[1] + '/' + ghMatch[2];
62
69
  }
@@ -69,26 +76,21 @@ function render(data) {
69
76
  const CYAN = '\x1b[38;2;6;182;212m', PURPLE = '\x1b[38;2;168;85;247m';
70
77
  const GREEN = '\x1b[38;2;34;197;94m', YELLOW = '\x1b[38;2;245;158;11m';
71
78
  const RED = '\x1b[38;2;239;68;68m', ORANGE = '\x1b[38;2;251;146;60m';
72
- const WHITE = '\x1b[38;2;228;228;231m', PINK = '\x1b[38;2;236;72;153m';
79
+ const WHITE = '\x1b[38;2;228;228;231m';
73
80
  const SEP = '\x1b[38;2;55;55;62m', DIM = '\x1b[38;2;40;40;45m';
74
81
  const BLUE = '\x1b[38;2;59;130;246m';
75
82
 
76
- // Model
77
83
  const model = data.model?.display_name || 'unknown';
78
84
 
79
- // Directory — last 3 path segments
80
85
  const cwd = (data.workspace?.current_dir || data.cwd || '').replace(/\\/g, '/').replace(/\/\/+/g, '/');
81
86
  const parts = cwd.split('/').filter(Boolean);
82
87
  const dir = parts.length > 3 ? parts.slice(-3).join('/') : parts.length > 0 ? parts.join('/') : '~';
83
88
 
84
- // Git — account/repo:branch from .git/HEAD + .git/config
85
89
  const projectDir = data.workspace?.project_dir || data.workspace?.current_dir || data.cwd || '';
86
90
  const gitInfo = getGitInfo(projectDir);
87
91
 
88
- // Live activity — tail transcript for current tool/skill/agent/task
89
92
  const activity = getActivity(data.transcript_path);
90
93
 
91
- // Context — use Claude's provided percentage
92
94
  let pct = Math.floor(data.context_window?.used_percentage || 0);
93
95
  if (pct > 100) pct = 100;
94
96
  const ctxClr = pct > 90 ? RED : pct > 75 ? ORANGE : pct > 40 ? YELLOW : WHITE;
@@ -96,11 +98,9 @@ function render(data) {
96
98
  const filled = Math.min(Math.floor(pct * barW / 100), barW);
97
99
  const bar = ctxClr + '\u2588'.repeat(filled) + RST + DIM + '\u2591'.repeat(barW - filled) + RST;
98
100
 
99
- // Cost
100
101
  const costRaw = data.cost?.total_cost_usd || 0;
101
102
  const cost = costRaw === 0 ? '$0.00' : costRaw < 0.01 ? `$${costRaw.toFixed(4)}` : `$${costRaw.toFixed(2)}`;
102
103
 
103
- // Tokens — session totals (in + out = total)
104
104
  const fmtTok = n => n >= 1000000 ? `${(n/1000000).toFixed(1)}M` : n >= 1000 ? `${(n/1000).toFixed(1)}k` : `${n}`;
105
105
  const totIn = data.context_window?.total_input_tokens || 0;
106
106
  const totOut = data.context_window?.total_output_tokens || 0;
@@ -108,16 +108,13 @@ function render(data) {
108
108
  const tokIn = fmtTok(totIn);
109
109
  const tokOut = fmtTok(totOut);
110
110
 
111
- // Session duration
112
111
  const durMs = data.cost?.total_duration_ms || 0;
113
112
  const durMin = Math.floor(durMs / 60000);
114
113
  const durSec = Math.floor((durMs % 60000) / 1000);
115
114
  const duration = durMin > 0 ? `${durMin}m ${durSec}s` : `${durSec}s`;
116
115
 
117
- // Activity color — highlight when active
118
116
  const actClr = activity === 'Idle' ? DIM : GREEN;
119
117
 
120
- // Separator + padding
121
118
  const S = ` ${SEP}\u2502${RST} `;
122
119
  const rpad = (s, w) => {
123
120
  const plain = s.replace(/\x1b\[[0-9;]*m/g, '');
@@ -125,7 +122,6 @@ function render(data) {
125
122
  };
126
123
  const C1 = 44;
127
124
 
128
- // Output 4 rows
129
125
  let out = '';
130
126
  out += ' ' + rpad(`${actClr}Action:${RST} ${actClr}${activity}${RST}`, C1) + S + `${WHITE}Git:${RST} ${WHITE}${gitInfo}${RST}\n`;
131
127
  out += ' ' + rpad(`${PURPLE}Model:${RST} ${PURPLE}${BOLD}${model}${RST}`, C1) + S + `${CYAN}Dir:${RST} ${CYAN}${dir}${RST}\n`;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "skill-statusline",
3
- "version": "2.3.1",
4
- "description": "Rich, themeable statusline for Claude Code — accurate context tracking, 5 themes, 3 layouts, token/cost/GitHub/skill display. Pure bash, zero deps.",
3
+ "version": "2.4.1",
4
+ "description": "Rich, themeable statusline for Claude Code — accurate context tracking, 5 themes, 3 layouts, token/cost/GitHub/skill display. Node.js renderer on Windows (no bash overhead), pure bash on Unix.",
5
5
  "bin": {
6
6
  "skill-statusline": "bin/cli.js",
7
7
  "ccsl": "bin/cli.js"
package/bin/cli.js.bak DELETED
@@ -1,837 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- const fs = require('fs');
4
- const path = require('path');
5
- const os = require('os');
6
- const { execSync } = require('child_process');
7
- const readline = require('readline');
8
-
9
- const args = process.argv.slice(2);
10
- const command = args[0];
11
- const subcommand = args[1];
12
- const VERSION = '2.0.0';
13
-
14
- const PKG_DIR = path.resolve(__dirname, '..');
15
- const HOME = os.homedir();
16
- const CLAUDE_DIR = path.join(HOME, '.claude');
17
- const SL_DIR = path.join(CLAUDE_DIR, 'statusline');
18
- const CONFIG_PATH = path.join(CLAUDE_DIR, 'statusline-config.json');
19
- const SETTINGS_PATH = path.join(CLAUDE_DIR, 'settings.json');
20
- const SCRIPT_DEST = path.join(CLAUDE_DIR, 'statusline-command.sh');
21
-
22
- const CLAUDE_MD_PATH = path.join(CLAUDE_DIR, 'CLAUDE.md');
23
- const THEMES = ['default', 'nord', 'tokyo-night', 'catppuccin', 'gruvbox'];
24
- const LAYOUTS = ['compact', 'standard', 'full'];
25
-
26
- // Marker for our managed section in CLAUDE.md
27
- const CLAUDE_MD_START = '<!-- skill-statusline:start -->';
28
- const CLAUDE_MD_END = '<!-- skill-statusline:end -->';
29
-
30
- // Terminal colors
31
- const R = '\x1b[0m';
32
- const B = '\x1b[1m';
33
- const D = '\x1b[2m';
34
- const GRN = '\x1b[32m';
35
- const YLW = '\x1b[33m';
36
- const RED = '\x1b[31m';
37
- const CYN = '\x1b[36m';
38
- const WHT = '\x1b[97m';
39
- const PURPLE = '\x1b[38;2;168;85;247m';
40
- const PINK = '\x1b[38;2;236;72;153m';
41
- const TEAL = '\x1b[38;2;6;182;212m';
42
- const GRAY = '\x1b[38;2;90;90;99m';
43
- const ORANGE = '\x1b[38;2;251;146;60m';
44
-
45
- function log(msg) { console.log(msg); }
46
- function success(msg) { log(` ${GRAY}\u2502${R} ${GRN}\u2713${R} ${msg}`); }
47
- function warn(msg) { log(` ${GRAY}\u2502${R} ${YLW}\u26A0${R} ${msg}`); }
48
- function fail(msg) { log(` ${GRAY}\u2502${R} ${RED}\u2717${R} ${msg}`); }
49
- function info(msg) { log(` ${GRAY}\u2502${R} ${CYN}\u2139${R} ${msg}`); }
50
- function bar(msg) { log(` ${GRAY}\u2502${R} ${D}${msg}${R}`); }
51
- function blank() { log(` ${GRAY}\u2502${R}`); }
52
-
53
- function header() {
54
- log('');
55
- log(` ${GRAY}\u250C${''.padEnd(58, '\u2500')}\u2510${R}`);
56
- log(` ${GRAY}\u2502${R} ${GRAY}\u2502${R}`);
57
- log(` ${GRAY}\u2502${R} ${PURPLE}${B}\u2588\u2588\u2588${R} ${PINK}${B}\u2588\u2588\u2588${R} ${WHT}${B}skill-statusline${R} ${D}v${VERSION}${R} ${GRAY}\u2502${R}`);
58
- log(` ${GRAY}\u2502${R} ${PURPLE}\u2588${R} ${PINK}\u2588${R} ${PURPLE}\u2588${R} ${D}Rich statusline for Claude Code${R} ${GRAY}\u2502${R}`);
59
- log(` ${GRAY}\u2502${R} ${PURPLE}${B}\u2588\u2588\u2588${R} ${PINK}${B}\u2588\u2588\u2588${R} ${GRAY}\u2502${R}`);
60
- log(` ${GRAY}\u2502${R} ${GRAY}\u2502${R}`);
61
- log(` ${GRAY}\u2502${R} ${TEAL}Thinqmesh Technologies${R} ${GRAY}\u2502${R}`);
62
- log(` ${GRAY}\u2502${R} ${GRAY}skills.thinqmesh.com${R} ${GRAY}\u2502${R}`);
63
- log(` ${GRAY}\u2502${R} ${GRAY}\u2502${R}`);
64
- log(` ${GRAY}\u251C${''.padEnd(58, '\u2500')}\u2524${R}`);
65
- }
66
-
67
- function footer() {
68
- log(` ${GRAY}\u2502${R}`);
69
- log(` ${GRAY}\u2514${''.padEnd(58, '\u2500')}\u2518${R}`);
70
- log('');
71
- }
72
-
73
- function divider() {
74
- log(` ${GRAY}\u251C${''.padEnd(58, '\u2500')}\u2524${R}`);
75
- }
76
-
77
- // ── Config helpers ──
78
-
79
- function readConfig() {
80
- try {
81
- if (fs.existsSync(CONFIG_PATH)) {
82
- return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
83
- }
84
- } catch (e) {}
85
- return { version: 2, theme: 'default', layout: 'standard', options: {} };
86
- }
87
-
88
- function writeConfig(config) {
89
- config.version = 2;
90
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n');
91
- }
92
-
93
- function readSettings() {
94
- try {
95
- if (fs.existsSync(SETTINGS_PATH)) {
96
- return JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
97
- }
98
- } catch (e) {}
99
- return {};
100
- }
101
-
102
- function writeSettings(settings) {
103
- fs.writeFileSync(SETTINGS_PATH, JSON.stringify(settings, null, 2) + '\n');
104
- }
105
-
106
- // ── CLAUDE.md management ──
107
- // Adds instructions to ~/.claude/CLAUDE.md so Claude Code knows to use ccsl
108
- // instead of the built-in statusline-setup agent (which fails on Windows
109
- // and conflicts with our setup).
110
-
111
- const CLAUDE_MD_SECTION = `${CLAUDE_MD_START}
112
- ## Statusline — Managed by skill-statusline v2
113
-
114
- The Claude Code statusline is fully configured and managed by the \`skill-statusline\` npm package. Do NOT use the built-in \`statusline-setup\` agent for any statusline configuration — it is not compatible with this setup.
115
-
116
- ### How It Works
117
- - \`~/.claude/settings.json\` → points to \`bash ~/.claude/statusline-command.sh\`
118
- - \`~/.claude/statusline-command.sh\` → entry point, delegates to v2 engine
119
- - \`~/.claude/statusline/core.sh\` → v2 engine (themes, layouts, accurate context tracking)
120
- - \`~/.claude/statusline-config.json\` → user preferences (theme, layout, options)
121
-
122
- ### When the user asks about statusline configuration, use these CLI commands:
123
-
124
- | Action | Command |
125
- |--------|---------|
126
- | Change theme | \`ccsl theme set <name>\` |
127
- | Change layout | \`ccsl layout set <name>\` |
128
- | List themes | \`ccsl theme\` |
129
- | List layouts | \`ccsl layout\` |
130
- | Preview | \`ccsl preview\` |
131
- | Preview with overrides | \`ccsl preview --theme <name> --layout <name>\` |
132
- | Show config | \`ccsl config\` |
133
- | Set config option | \`ccsl config set <key> <value>\` |
134
- | Diagnostics | \`ccsl doctor\` |
135
- | Update scripts | \`ccsl update\` |
136
- | Reinstall | \`ccsl install\` |
137
- | Uninstall | \`ccsl uninstall\` |
138
-
139
- **Available themes:** default, nord, tokyo-night, catppuccin, gruvbox
140
- **Available layouts:** compact (2 rows), standard (4 rows), full (6 rows)
141
- **Config options:** compaction_warning_threshold, bar_width, cache_ttl_seconds, show_burn_rate, show_vim_mode, show_agent_name
142
-
143
- ### IMPORTANT
144
- - NEVER spawn the \`statusline-setup\` agent — it will fail on Windows and conflict with this setup
145
- - All statusline changes are made via \`ccsl\` CLI commands (run in terminal)
146
- - Changes take effect on next Claude Code restart (or next statusline refresh for config changes)
147
- ${CLAUDE_MD_END}`;
148
-
149
- function installClaudeMd() {
150
- let content = '';
151
- if (fs.existsSync(CLAUDE_MD_PATH)) {
152
- content = fs.readFileSync(CLAUDE_MD_PATH, 'utf8');
153
- // Remove existing section if present
154
- const startIdx = content.indexOf(CLAUDE_MD_START);
155
- const endIdx = content.indexOf(CLAUDE_MD_END);
156
- if (startIdx !== -1 && endIdx !== -1) {
157
- content = content.substring(0, startIdx) + content.substring(endIdx + CLAUDE_MD_END.length);
158
- content = content.replace(/\n{3,}/g, '\n\n').trim();
159
- }
160
- }
161
- // Append our section
162
- content = content ? content + '\n\n' + CLAUDE_MD_SECTION + '\n' : CLAUDE_MD_SECTION + '\n';
163
- fs.writeFileSync(CLAUDE_MD_PATH, content);
164
- }
165
-
166
- function uninstallClaudeMd() {
167
- if (!fs.existsSync(CLAUDE_MD_PATH)) return false;
168
- let content = fs.readFileSync(CLAUDE_MD_PATH, 'utf8');
169
- const startIdx = content.indexOf(CLAUDE_MD_START);
170
- const endIdx = content.indexOf(CLAUDE_MD_END);
171
- if (startIdx === -1 || endIdx === -1) return false;
172
- content = content.substring(0, startIdx) + content.substring(endIdx + CLAUDE_MD_END.length);
173
- content = content.replace(/\n{3,}/g, '\n\n').trim();
174
- if (content) {
175
- fs.writeFileSync(CLAUDE_MD_PATH, content + '\n');
176
- } else {
177
- // File is empty after removing our section — delete it
178
- fs.unlinkSync(CLAUDE_MD_PATH);
179
- }
180
- return true;
181
- }
182
-
183
- // ── File copy helpers ──
184
-
185
- function ensureDir(dir) {
186
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
187
- }
188
-
189
- function copyDir(src, dest) {
190
- ensureDir(dest);
191
- for (const entry of fs.readdirSync(src)) {
192
- const srcPath = path.join(src, entry);
193
- const destPath = path.join(dest, entry);
194
- if (fs.statSync(srcPath).isDirectory()) {
195
- copyDir(srcPath, destPath);
196
- } else {
197
- fs.copyFileSync(srcPath, destPath);
198
- }
199
- }
200
- }
201
-
202
- function installFiles() {
203
- ensureDir(CLAUDE_DIR);
204
- ensureDir(SL_DIR);
205
-
206
- // Copy lib/ → ~/.claude/statusline/
207
- const libSrc = path.join(PKG_DIR, 'lib');
208
- if (fs.existsSync(libSrc)) {
209
- for (const f of fs.readdirSync(libSrc)) {
210
- fs.copyFileSync(path.join(libSrc, f), path.join(SL_DIR, f));
211
- }
212
- }
213
-
214
- // Copy themes/ → ~/.claude/statusline/themes/
215
- const themesSrc = path.join(PKG_DIR, 'themes');
216
- if (fs.existsSync(themesSrc)) {
217
- copyDir(themesSrc, path.join(SL_DIR, 'themes'));
218
- }
219
-
220
- // Copy layouts/ → ~/.claude/statusline/layouts/
221
- const layoutsSrc = path.join(PKG_DIR, 'layouts');
222
- if (fs.existsSync(layoutsSrc)) {
223
- copyDir(layoutsSrc, path.join(SL_DIR, 'layouts'));
224
- }
225
-
226
- // Copy entry point
227
- const slSrc = path.join(PKG_DIR, 'bin', 'statusline.sh');
228
- fs.copyFileSync(slSrc, SCRIPT_DEST);
229
-
230
- return true;
231
- }
232
-
233
- // ── Interactive prompt ──
234
-
235
- function ask(rl, question) {
236
- return new Promise(resolve => rl.question(question, resolve));
237
- }
238
-
239
- async function chooseFromList(rl, label, items, current) {
240
- blank();
241
- info(`${B}${label}${R}`);
242
- blank();
243
- items.forEach((item, i) => {
244
- const marker = item === current ? ` ${GRN}(current)${R}` : '';
245
- log(` ${GRAY}\u2502${R} ${CYN}[${i + 1}]${R} ${item}${marker}`);
246
- });
247
- blank();
248
- const answer = await ask(rl, ` ${GRAY}\u2502${R} > `);
249
- const idx = parseInt(answer, 10) - 1;
250
- if (idx >= 0 && idx < items.length) return items[idx];
251
- return current || items[0];
252
- }
253
-
254
- // ── Commands ──
255
-
256
- async function install() {
257
- const isQuick = args.includes('--quick');
258
- const config = readConfig();
259
-
260
- header();
261
-
262
- if (isQuick) {
263
- blank();
264
- info(`${B}Quick install${R} — using defaults`);
265
- blank();
266
- } else {
267
- // Interactive wizard
268
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
269
-
270
- const themeNames = ['Default (classic purple/pink/cyan)', 'Nord (arctic, blue-tinted)', 'Tokyo Night (vibrant neon)', 'Catppuccin (warm pastels)', 'Gruvbox (retro groovy)'];
271
- blank();
272
- info(`${B}Choose a theme:${R}`);
273
- blank();
274
- themeNames.forEach((name, i) => {
275
- log(` ${GRAY}\u2502${R} ${CYN}[${i + 1}]${R} ${name}`);
276
- });
277
- blank();
278
- const tAnswer = await ask(rl, ` ${GRAY}\u2502${R} > `);
279
- const tIdx = parseInt(tAnswer, 10) - 1;
280
- if (tIdx >= 0 && tIdx < THEMES.length) config.theme = THEMES[tIdx];
281
-
282
- const layoutNames = ['Compact (2 rows \u2014 minimal)', 'Standard (4 rows \u2014 balanced)', 'Full (6 rows \u2014 everything)'];
283
- blank();
284
- info(`${B}Choose a layout:${R}`);
285
- blank();
286
- layoutNames.forEach((name, i) => {
287
- log(` ${GRAY}\u2502${R} ${CYN}[${i + 1}]${R} ${name}`);
288
- });
289
- blank();
290
- const lAnswer = await ask(rl, ` ${GRAY}\u2502${R} > `);
291
- const lIdx = parseInt(lAnswer, 10) - 1;
292
- if (lIdx >= 0 && lIdx < LAYOUTS.length) config.layout = LAYOUTS[lIdx];
293
-
294
- rl.close();
295
- blank();
296
- }
297
-
298
- // Install files
299
- installFiles();
300
- success(`${B}statusline/${R} directory installed to ~/.claude/`);
301
-
302
- // Write config
303
- if (!config.options) config.options = {};
304
- writeConfig(config);
305
- success(`Config: theme=${CYN}${config.theme}${R}, layout=${CYN}${config.layout}${R}`);
306
-
307
- // Update settings.json
308
- const settings = readSettings();
309
- if (!settings.statusLine) {
310
- settings.statusLine = {
311
- type: 'command',
312
- command: 'bash ~/.claude/statusline-command.sh'
313
- };
314
- writeSettings(settings);
315
- success(`${B}statusLine${R} config added to settings.json`);
316
- } else {
317
- success(`statusLine already configured in settings.json`);
318
- }
319
-
320
- // Add CLAUDE.md instructions (prevents built-in statusline-setup agent)
321
- installClaudeMd();
322
- success(`CLAUDE.md updated (statusline agent redirect)`);
323
-
324
- divider();
325
- blank();
326
- log(` ${GRAY}\u2502${R} ${GRN}${B}Ready.${R} Restart Claude Code to see the statusline.`);
327
- blank();
328
- log(` ${GRAY}\u2502${R} ${WHT}${B}Layout: ${config.layout}${R} ${WHT}${B}Theme: ${config.theme}${R}`);
329
- blank();
330
-
331
- if (config.layout === 'compact') {
332
- log(` ${GRAY}\u2502${R} ${PURPLE}Opus 4.6${R} ${GRAY}\u2502${R} ${TEAL}Downloads/Project${R} ${GRAY}\u2502${R} ${WHT}47%${R} ${GRN}$1.23${R}`);
333
- log(` ${GRAY}\u2502${R} ${WHT}Context:${R} ${GRN}\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588${R}${D}\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591${R} 50%`);
334
- } else if (config.layout === 'full') {
335
- log(` ${GRAY}\u2502${R} ${PINK}Skill:${R} Edit ${GRAY}\u2502${R} ${WHT}GitHub:${R} User/Repo/main`);
336
- log(` ${GRAY}\u2502${R} ${PURPLE}Model:${R} Opus 4.6 ${GRAY}\u2502${R} ${TEAL}Dir:${R} Downloads/Project`);
337
- log(` ${GRAY}\u2502${R} ${YLW}Window:${R} 8.5k + 1.2k ${GRAY}\u2502${R} ${GRN}Cost:${R} $1.23`);
338
- log(` ${GRAY}\u2502${R} ${YLW}Session:${R} ${D}25k + 12k${R} ${GRAY}\u2502${R} ${D}+156/-23 12m34s${R}`);
339
- log(` ${GRAY}\u2502${R} ${CYN}Cache:${R} ${D}W:5k R:2k${R} ${GRAY}\u2502${R} ${TEAL}NORMAL${R} ${CYN}@reviewer${R}`);
340
- log(` ${GRAY}\u2502${R} ${WHT}Context:${R} ${GRN}\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588${R}${D}\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591${R} 50%`);
341
- } else {
342
- log(` ${GRAY}\u2502${R} ${PINK}Skill:${R} Edit ${GRAY}\u2502${R} ${WHT}GitHub:${R} User/Repo/main`);
343
- log(` ${GRAY}\u2502${R} ${PURPLE}Model:${R} Opus 4.6 ${GRAY}\u2502${R} ${TEAL}Dir:${R} Downloads/Project`);
344
- log(` ${GRAY}\u2502${R} ${YLW}Tokens:${R} 8.5k + 1.2k ${GRAY}\u2502${R} ${GRN}Cost:${R} $1.23`);
345
- log(` ${GRAY}\u2502${R} ${WHT}Context:${R} ${GRN}\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588${R}${D}\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591\u2591${R} 50%`);
346
- }
347
-
348
- blank();
349
- bar(`Script: ${R}${CYN}~/.claude/statusline-command.sh${R}`);
350
- bar(`Engine: ${R}${CYN}~/.claude/statusline/core.sh${R}`);
351
- bar(`Config: ${R}${CYN}~/.claude/statusline-config.json${R}`);
352
- bar(`Settings: ${R}${CYN}~/.claude/settings.json${R}`);
353
- blank();
354
- bar(`Docs ${R}${TEAL}https://skills.thinqmesh.com${R}`);
355
- bar(`GitHub ${R}${PURPLE}https://github.com/AnitChaudhry/skill-statusline${R}`);
356
-
357
- footer();
358
- }
359
-
360
- function uninstall() {
361
- header();
362
- blank();
363
- info(`${B}Uninstalling statusline${R}`);
364
- blank();
365
-
366
- // Remove statusline directory
367
- if (fs.existsSync(SL_DIR)) {
368
- fs.rmSync(SL_DIR, { recursive: true });
369
- success(`Removed ~/.claude/statusline/`);
370
- }
371
-
372
- // Remove script
373
- if (fs.existsSync(SCRIPT_DEST)) {
374
- fs.unlinkSync(SCRIPT_DEST);
375
- success(`Removed ~/.claude/statusline-command.sh`);
376
- } else {
377
- warn(`statusline-command.sh not found`);
378
- }
379
-
380
- // Remove config
381
- if (fs.existsSync(CONFIG_PATH)) {
382
- fs.unlinkSync(CONFIG_PATH);
383
- success(`Removed ~/.claude/statusline-config.json`);
384
- }
385
-
386
- // Remove from settings.json
387
- if (fs.existsSync(SETTINGS_PATH)) {
388
- try {
389
- const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
390
- if (settings.statusLine) {
391
- delete settings.statusLine;
392
- writeSettings(settings);
393
- success(`Removed statusLine from settings.json`);
394
- }
395
- } catch (e) {
396
- warn(`Could not parse settings.json`);
397
- }
398
- }
399
-
400
- // Remove CLAUDE.md section
401
- if (uninstallClaudeMd()) {
402
- success(`Removed statusline section from CLAUDE.md`);
403
- }
404
-
405
- blank();
406
- log(` ${GRAY}\u2502${R} ${GRN}${B}Done.${R} Restart Claude Code to apply.`);
407
-
408
- footer();
409
- }
410
-
411
- function update() {
412
- header();
413
- blank();
414
- info(`${B}Updating statusline scripts${R} (preserving config)`);
415
- blank();
416
-
417
- installFiles();
418
- success(`Scripts updated to v${VERSION}`);
419
-
420
- const config = readConfig();
421
- success(`Config preserved: theme=${CYN}${config.theme}${R}, layout=${CYN}${config.layout}${R}`);
422
-
423
- // Refresh CLAUDE.md instructions
424
- installClaudeMd();
425
- success(`CLAUDE.md refreshed`);
426
-
427
- blank();
428
- log(` ${GRAY}\u2502${R} ${GRN}${B}Done.${R} Restart Claude Code to apply.`);
429
-
430
- footer();
431
- }
432
-
433
- function preview() {
434
- const themeName = args.includes('--theme') ? args[args.indexOf('--theme') + 1] : null;
435
- const layoutName = args.includes('--layout') ? args[args.indexOf('--layout') + 1] : null;
436
-
437
- const sampleJson = JSON.stringify({
438
- cwd: process.cwd(),
439
- session_id: 'preview-session',
440
- version: '2.0.0',
441
- model: { id: 'claude-opus-4-6', display_name: 'Opus' },
442
- workspace: { current_dir: process.cwd(), project_dir: process.cwd() },
443
- cost: { total_cost_usd: 1.23, total_duration_ms: 754000, total_api_duration_ms: 23400, total_lines_added: 156, total_lines_removed: 23 },
444
- context_window: {
445
- total_input_tokens: 125234, total_output_tokens: 34521,
446
- context_window_size: 200000, used_percentage: 47, remaining_percentage: 53,
447
- current_usage: { input_tokens: 85000, output_tokens: 12000, cache_creation_input_tokens: 5000, cache_read_input_tokens: 2000 }
448
- },
449
- vim: { mode: 'NORMAL' },
450
- agent: { name: 'code-reviewer' }
451
- });
452
-
453
- // Check if v2 engine is installed
454
- const coreFile = path.join(SL_DIR, 'core.sh');
455
- let scriptPath;
456
- if (fs.existsSync(coreFile)) {
457
- scriptPath = coreFile;
458
- } else if (fs.existsSync(SCRIPT_DEST)) {
459
- scriptPath = SCRIPT_DEST;
460
- } else {
461
- // Use the package's own script
462
- scriptPath = path.join(PKG_DIR, 'lib', 'core.sh');
463
- }
464
-
465
- const env = { ...process.env };
466
- if (themeName) env.STATUSLINE_THEME_OVERRIDE = themeName;
467
- if (layoutName) env.STATUSLINE_LAYOUT_OVERRIDE = layoutName;
468
-
469
- // For preview with package's own files, set STATUSLINE_DIR
470
- if (!fs.existsSync(path.join(SL_DIR, 'core.sh'))) {
471
- // Point to package's own lib directory structure
472
- env.HOME = PKG_DIR;
473
- }
474
-
475
- try {
476
- const escaped = sampleJson.replace(/'/g, "'\\''");
477
- const result = execSync(`printf '%s' '${escaped}' | bash "${scriptPath.replace(/\\/g, '/')}"`, {
478
- encoding: 'utf8',
479
- env,
480
- timeout: 5000
481
- });
482
- log('');
483
- log(result);
484
- log('');
485
- } catch (e) {
486
- warn(`Preview failed: ${e.message}`);
487
- }
488
- }
489
-
490
- function themeCmd() {
491
- const config = readConfig();
492
-
493
- if (subcommand === 'set') {
494
- const name = args[2];
495
- if (!name || !THEMES.includes(name)) {
496
- header();
497
- blank();
498
- fail(`Unknown theme: ${name || '(none)'}`);
499
- blank();
500
- info(`Available: ${THEMES.join(', ')}`);
501
- footer();
502
- process.exit(1);
503
- }
504
- config.theme = name;
505
- writeConfig(config);
506
- header();
507
- blank();
508
- success(`Theme set to ${CYN}${B}${name}${R}`);
509
- blank();
510
- log(` ${GRAY}\u2502${R} Restart Claude Code to apply.`);
511
- footer();
512
- return;
513
- }
514
-
515
- // List themes
516
- header();
517
- blank();
518
- info(`${B}Themes${R}`);
519
- blank();
520
- THEMES.forEach(t => {
521
- const marker = t === config.theme ? ` ${GRN}\u2190 current${R}` : '';
522
- log(` ${GRAY}\u2502${R} ${CYN}${t}${R}${marker}`);
523
- });
524
- blank();
525
- bar(`Set theme: ${R}${CYN}ccsl theme set <name>${R}`);
526
- bar(`Preview: ${R}${CYN}ccsl preview --theme <name>${R}`);
527
- footer();
528
- }
529
-
530
- function layoutCmd() {
531
- const config = readConfig();
532
-
533
- if (subcommand === 'set') {
534
- const name = args[2];
535
- if (!name || !LAYOUTS.includes(name)) {
536
- header();
537
- blank();
538
- fail(`Unknown layout: ${name || '(none)'}`);
539
- blank();
540
- info(`Available: ${LAYOUTS.join(', ')}`);
541
- footer();
542
- process.exit(1);
543
- }
544
- config.layout = name;
545
- writeConfig(config);
546
- header();
547
- blank();
548
- success(`Layout set to ${CYN}${B}${name}${R}`);
549
- blank();
550
- log(` ${GRAY}\u2502${R} Restart Claude Code to apply.`);
551
- footer();
552
- return;
553
- }
554
-
555
- // List layouts
556
- header();
557
- blank();
558
- info(`${B}Layouts${R}`);
559
- blank();
560
- const descriptions = { compact: '2 rows \u2014 minimal', standard: '4 rows \u2014 balanced', full: '6 rows \u2014 everything' };
561
- LAYOUTS.forEach(l => {
562
- const marker = l === config.layout ? ` ${GRN}\u2190 current${R}` : '';
563
- log(` ${GRAY}\u2502${R} ${CYN}${l}${R} ${D}(${descriptions[l]})${R}${marker}`);
564
- });
565
- blank();
566
- bar(`Set layout: ${R}${CYN}ccsl layout set <name>${R}`);
567
- bar(`Preview: ${R}${CYN}ccsl preview --layout <name>${R}`);
568
- footer();
569
- }
570
-
571
- function configCmd() {
572
- const config = readConfig();
573
-
574
- if (subcommand === 'set') {
575
- const key = args[2];
576
- const value = args[3];
577
- if (!key || value === undefined) {
578
- header();
579
- blank();
580
- fail(`Usage: ccsl config set <key> <value>`);
581
- blank();
582
- info(`Keys: compaction_warning_threshold, bar_width, cache_ttl_seconds,`);
583
- info(` show_burn_rate, show_vim_mode, show_agent_name`);
584
- footer();
585
- process.exit(1);
586
- }
587
- if (!config.options) config.options = {};
588
- // Parse booleans and numbers
589
- if (value === 'true') config.options[key] = true;
590
- else if (value === 'false') config.options[key] = false;
591
- else if (!isNaN(value)) config.options[key] = Number(value);
592
- else config.options[key] = value;
593
-
594
- writeConfig(config);
595
- header();
596
- blank();
597
- success(`Set ${CYN}${key}${R} = ${CYN}${value}${R}`);
598
- footer();
599
- return;
600
- }
601
-
602
- // Show config
603
- header();
604
- blank();
605
- info(`${B}Current configuration${R}`);
606
- blank();
607
- log(` ${GRAY}\u2502${R} ${WHT}Theme:${R} ${CYN}${config.theme}${R}`);
608
- log(` ${GRAY}\u2502${R} ${WHT}Layout:${R} ${CYN}${config.layout}${R}`);
609
- if (config.options && Object.keys(config.options).length > 0) {
610
- blank();
611
- info(`${B}Options${R}`);
612
- blank();
613
- for (const [k, v] of Object.entries(config.options)) {
614
- log(` ${GRAY}\u2502${R} ${D}${k}:${R} ${CYN}${v}${R}`);
615
- }
616
- }
617
- blank();
618
- bar(`File: ${R}${CYN}~/.claude/statusline-config.json${R}`);
619
- footer();
620
- }
621
-
622
- function doctor() {
623
- header();
624
- blank();
625
- info(`${B}Diagnostic check${R}`);
626
- blank();
627
-
628
- let issues = 0;
629
-
630
- // 1. Bash
631
- try {
632
- const bashVer = execSync('bash --version 2>&1', { encoding: 'utf8' }).split('\n')[0];
633
- success(`bash: ${D}${bashVer.substring(0, 60)}${R}`);
634
- } catch (e) {
635
- fail(`bash not found`);
636
- issues++;
637
- }
638
-
639
- // 2. Git
640
- try {
641
- execSync('git --version', { encoding: 'utf8' });
642
- success(`git available`);
643
- } catch (e) {
644
- warn(`git not found (GitHub field will show "no-git")`);
645
- }
646
-
647
- // 3. settings.json
648
- if (fs.existsSync(SETTINGS_PATH)) {
649
- try {
650
- const settings = JSON.parse(fs.readFileSync(SETTINGS_PATH, 'utf8'));
651
- if (settings.statusLine && settings.statusLine.command) {
652
- success(`settings.json has statusLine config`);
653
- } else {
654
- fail(`settings.json missing statusLine entry`);
655
- issues++;
656
- }
657
- } catch (e) {
658
- fail(`settings.json is invalid JSON`);
659
- issues++;
660
- }
661
- } else {
662
- fail(`~/.claude/settings.json not found`);
663
- issues++;
664
- }
665
-
666
- // 4. Entry point script
667
- if (fs.existsSync(SCRIPT_DEST)) {
668
- success(`statusline-command.sh exists`);
669
- } else {
670
- fail(`~/.claude/statusline-command.sh not found`);
671
- issues++;
672
- }
673
-
674
- // 5. v2 engine
675
- const coreFile = path.join(SL_DIR, 'core.sh');
676
- if (fs.existsSync(coreFile)) {
677
- success(`v2 engine installed (statusline/core.sh)`);
678
-
679
- // Check theme file
680
- const config = readConfig();
681
- const themeFile = path.join(SL_DIR, 'themes', `${config.theme}.sh`);
682
- if (fs.existsSync(themeFile)) {
683
- success(`Theme "${config.theme}" found`);
684
- } else {
685
- fail(`Theme "${config.theme}" not found at ${themeFile}`);
686
- issues++;
687
- }
688
-
689
- // Check layout file
690
- const layoutFile = path.join(SL_DIR, 'layouts', `${config.layout}.sh`);
691
- if (fs.existsSync(layoutFile)) {
692
- success(`Layout "${config.layout}" found`);
693
- } else {
694
- fail(`Layout "${config.layout}" not found at ${layoutFile}`);
695
- issues++;
696
- }
697
- } else {
698
- warn(`v2 engine not installed (running v1 fallback)`);
699
- }
700
-
701
- // 6. CLAUDE.md agent redirect
702
- if (fs.existsSync(CLAUDE_MD_PATH)) {
703
- const mdContent = fs.readFileSync(CLAUDE_MD_PATH, 'utf8');
704
- if (mdContent.includes(CLAUDE_MD_START)) {
705
- success(`CLAUDE.md has statusline agent redirect`);
706
- } else {
707
- warn(`CLAUDE.md exists but missing statusline section`);
708
- info(`Run ${CYN}ccsl install${R} or ${CYN}ccsl update${R} to add it`);
709
- }
710
- } else {
711
- warn(`No ~/.claude/CLAUDE.md (built-in statusline agent may interfere)`);
712
- info(`Run ${CYN}ccsl install${R} or ${CYN}ccsl update${R} to fix`);
713
- }
714
-
715
- // 7. Config file
716
- if (fs.existsSync(CONFIG_PATH)) {
717
- try {
718
- JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf8'));
719
- success(`statusline-config.json is valid`);
720
- } catch (e) {
721
- fail(`statusline-config.json is invalid JSON`);
722
- issues++;
723
- }
724
- } else {
725
- warn(`No config file (using defaults)`);
726
- }
727
-
728
- // 8. Performance benchmark
729
- blank();
730
- info(`${B}Performance benchmark${R}`);
731
- blank();
732
- try {
733
- const sampleJson = '{"model":{"id":"claude-opus-4-6","display_name":"Opus"},"workspace":{"current_dir":"/tmp"},"cost":{"total_cost_usd":0.5},"context_window":{"context_window_size":200000,"used_percentage":50,"current_usage":{"input_tokens":90000,"output_tokens":10000,"cache_creation_input_tokens":0,"cache_read_input_tokens":0}}}';
734
- const target = fs.existsSync(coreFile) ? coreFile : SCRIPT_DEST;
735
- if (target && fs.existsSync(target)) {
736
- const start = Date.now();
737
- execSync(`printf '%s' '${sampleJson}' | bash "${target.replace(/\\/g, '/')}"`, {
738
- encoding: 'utf8',
739
- timeout: 10000
740
- });
741
- const elapsed = Date.now() - start;
742
- const color = elapsed < 50 ? GRN : elapsed < 100 ? YLW : RED;
743
- const label = elapsed < 50 ? 'excellent' : elapsed < 100 ? 'good' : 'slow';
744
- log(` ${GRAY}\u2502${R} ${color}\u25CF${R} Execution: ${color}${B}${elapsed}ms${R} (${label}, target: <50ms)`);
745
- }
746
- } catch (e) {
747
- fail(`Benchmark failed: ${e.message.substring(0, 50)}`);
748
- issues++;
749
- }
750
-
751
- blank();
752
- if (issues === 0) {
753
- log(` ${GRAY}\u2502${R} ${GRN}${B}All checks passed.${R}`);
754
- } else {
755
- log(` ${GRAY}\u2502${R} ${RED}${B}${issues} issue(s) found.${R} Run ${CYN}ccsl install${R} to fix.`);
756
- }
757
-
758
- footer();
759
- }
760
-
761
- function showVersion() {
762
- log(`skill-statusline v${VERSION}`);
763
- }
764
-
765
- function showHelp() {
766
- header();
767
- blank();
768
- log(` ${GRAY}\u2502${R} ${WHT}${B}Commands:${R}`);
769
- blank();
770
- log(` ${GRAY}\u2502${R} ${CYN}ccsl install${R} Install with theme/layout wizard`);
771
- log(` ${GRAY}\u2502${R} ${CYN}ccsl install --quick${R} Install with defaults`);
772
- log(` ${GRAY}\u2502${R} ${CYN}ccsl uninstall${R} Remove statusline`);
773
- log(` ${GRAY}\u2502${R} ${CYN}ccsl update${R} Update scripts (keep config)`);
774
- blank();
775
- log(` ${GRAY}\u2502${R} ${CYN}ccsl theme${R} List themes`);
776
- log(` ${GRAY}\u2502${R} ${CYN}ccsl theme set <name>${R} Set active theme`);
777
- log(` ${GRAY}\u2502${R} ${CYN}ccsl layout${R} List layouts`);
778
- log(` ${GRAY}\u2502${R} ${CYN}ccsl layout set <name>${R} Set active layout`);
779
- blank();
780
- log(` ${GRAY}\u2502${R} ${CYN}ccsl preview${R} Preview with sample data`);
781
- log(` ${GRAY}\u2502${R} ${CYN}ccsl preview --theme x${R} Preview a specific theme`);
782
- log(` ${GRAY}\u2502${R} ${CYN}ccsl preview --layout x${R} Preview a specific layout`);
783
- blank();
784
- log(` ${GRAY}\u2502${R} ${CYN}ccsl config${R} Show current config`);
785
- log(` ${GRAY}\u2502${R} ${CYN}ccsl config set k v${R} Set config option`);
786
- log(` ${GRAY}\u2502${R} ${CYN}ccsl doctor${R} Run diagnostics`);
787
- log(` ${GRAY}\u2502${R} ${CYN}ccsl version${R} Show version`);
788
- blank();
789
- log(` ${GRAY}\u2502${R} ${WHT}${B}Themes:${R} ${THEMES.join(', ')}`);
790
- log(` ${GRAY}\u2502${R} ${WHT}${B}Layouts:${R} ${LAYOUTS.join(', ')}`);
791
- blank();
792
- log(` ${GRAY}\u2502${R} ${WHT}${B}What it shows:${R}`);
793
- blank();
794
- log(` ${GRAY}\u2502${R} ${PINK}Skill${R} Last tool used (Read, Write, Terminal, Agent...)`);
795
- log(` ${GRAY}\u2502${R} ${PURPLE}Model${R} Active model name and version`);
796
- log(` ${GRAY}\u2502${R} ${WHT}GitHub${R} user/repo/branch with dirty indicators`);
797
- log(` ${GRAY}\u2502${R} ${TEAL}Dir${R} Last 3 segments of working directory`);
798
- log(` ${GRAY}\u2502${R} ${YLW}Tokens${R} Current window: input + output`);
799
- log(` ${GRAY}\u2502${R} ${GRN}Cost${R} Session cost in USD`);
800
- log(` ${GRAY}\u2502${R} ${WHT}Context${R} Accurate progress bar with compaction warning`);
801
- log(` ${GRAY}\u2502${R} ${D}+ Session tokens, duration, lines, cache, vim, agent (full layout)${R}`);
802
- blank();
803
- bar(`Docs ${R}${TEAL}https://skills.thinqmesh.com${R}`);
804
- bar(`GitHub ${R}${PURPLE}https://github.com/AnitChaudhry/skill-statusline${R}`);
805
-
806
- footer();
807
- }
808
-
809
- // ── Main ──
810
-
811
- if (command === 'install' || command === 'init') {
812
- install();
813
- } else if (command === 'uninstall' || command === 'remove') {
814
- uninstall();
815
- } else if (command === 'update' || command === 'upgrade') {
816
- update();
817
- } else if (command === 'preview') {
818
- preview();
819
- } else if (command === 'theme') {
820
- themeCmd();
821
- } else if (command === 'layout') {
822
- layoutCmd();
823
- } else if (command === 'config') {
824
- configCmd();
825
- } else if (command === 'doctor' || command === 'check') {
826
- doctor();
827
- } else if (command === 'version' || command === '--version' || command === '-v') {
828
- showVersion();
829
- } else if (command === 'help' || command === '--help' || command === '-h') {
830
- showHelp();
831
- } else {
832
- if (command) {
833
- log('');
834
- warn(`Unknown command: ${command}`);
835
- }
836
- showHelp();
837
- }