skill-statusline 2.3.1 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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.0';
13
13
 
14
14
  const PKG_DIR = path.resolve(__dirname, '..');
15
15
  const HOME = os.homedir();
@@ -271,10 +271,17 @@ function installFiles() {
271
271
  copyDir(layoutsSrc, path.join(SL_DIR, 'layouts'));
272
272
  }
273
273
 
274
- // Copy entry point
274
+ // Copy entry point (bash)
275
275
  const slSrc = path.join(PKG_DIR, 'bin', 'statusline.sh');
276
276
  fs.copyFileSync(slSrc, SCRIPT_DEST);
277
277
 
278
+ // Copy Node.js renderer (used on Windows for performance)
279
+ const nodeSrc = path.join(PKG_DIR, 'bin', 'statusline-node.js');
280
+ const nodeDest = path.join(CLAUDE_DIR, 'statusline-node.js');
281
+ if (fs.existsSync(nodeSrc)) {
282
+ fs.copyFileSync(nodeSrc, nodeDest);
283
+ }
284
+
278
285
  return true;
279
286
  }
280
287
 
@@ -352,23 +359,30 @@ async function install() {
352
359
  writeConfig(config);
353
360
  success(`Config: theme=${CYN}${config.theme}${R}, layout=${CYN}${config.layout}${R}`);
354
361
 
355
- // Update settings.json — use absolute paths on Windows to avoid WSL bash conflict
362
+ // Update settings.json
356
363
  const settings = readSettings();
357
- if (!settings.statusLine || settings.statusLine.command === 'bash ~/.claude/statusline-command.sh') {
358
- const isWin = process.platform === 'win32';
364
+ const isWin = process.platform === 'win32';
365
+ const prevCmd = settings.statusLine?.command || '';
366
+ const needsUpdate = !settings.statusLine
367
+ || prevCmd === 'bash ~/.claude/statusline-command.sh'
368
+ || (isWin && prevCmd.includes('bash.exe'))
369
+ || (isWin && prevCmd.includes('\\\\'));
370
+ if (needsUpdate) {
359
371
  let cmd;
360
372
  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}"`;
373
+ // Windows: use Node.js renderer directly avoids Git Bash/MSYS2 overhead
374
+ // (~50-100ms per subprocess spawn in Git Bash vs single Node.js process)
375
+ const nodeScript = path.join(CLAUDE_DIR, 'statusline-node.js');
376
+ cmd = `node "${nodeScript.replace(/\\/g, '/')}"`;
366
377
  } else {
367
378
  cmd = 'bash ~/.claude/statusline-command.sh';
368
379
  }
369
380
  settings.statusLine = { type: 'command', command: cmd };
370
381
  writeSettings(settings);
371
382
  success(`${B}statusLine${R} config added to settings.json`);
383
+ if (isWin) {
384
+ info(`Using Node.js renderer (fast on Windows)`);
385
+ }
372
386
  } else {
373
387
  success(`statusLine already configured in settings.json`);
374
388
  }
@@ -408,8 +422,12 @@ async function install() {
408
422
  }
409
423
 
410
424
  blank();
411
- bar(`Script: ${R}${CYN}~/.claude/statusline-command.sh${R}`);
412
- bar(`Engine: ${R}${CYN}~/.claude/statusline/core.sh${R}`);
425
+ if (isWin) {
426
+ bar(`Renderer: ${R}${CYN}~/.claude/statusline-node.js${R} ${D}(Node.js)${R}`);
427
+ } else {
428
+ bar(`Script: ${R}${CYN}~/.claude/statusline-command.sh${R}`);
429
+ bar(`Engine: ${R}${CYN}~/.claude/statusline/core.sh${R}`);
430
+ }
413
431
  bar(`Config: ${R}${CYN}~/.claude/statusline-config.json${R}`);
414
432
  bar(`Settings: ${R}${CYN}~/.claude/settings.json${R}`);
415
433
  blank();
@@ -431,13 +449,18 @@ function uninstall() {
431
449
  success(`Removed ~/.claude/statusline/`);
432
450
  }
433
451
 
434
- // Remove script
452
+ // Remove scripts
435
453
  if (fs.existsSync(SCRIPT_DEST)) {
436
454
  fs.unlinkSync(SCRIPT_DEST);
437
455
  success(`Removed ~/.claude/statusline-command.sh`);
438
456
  } else {
439
457
  warn(`statusline-command.sh not found`);
440
458
  }
459
+ const nodeScript = path.join(CLAUDE_DIR, 'statusline-node.js');
460
+ if (fs.existsSync(nodeScript)) {
461
+ fs.unlinkSync(nodeScript);
462
+ success(`Removed ~/.claude/statusline-node.js`);
463
+ }
441
464
 
442
465
  // Remove config
443
466
  if (fs.existsSync(CONFIG_PATH)) {
@@ -524,40 +547,53 @@ function preview() {
524
547
  agent: { name: 'code-reviewer' }
525
548
  });
526
549
 
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;
550
+ const isWin = process.platform === 'win32';
551
+
552
+ // On Windows, prefer Node.js renderer for speed
553
+ const nodeRendererInstalled = path.join(CLAUDE_DIR, 'statusline-node.js');
554
+ const nodeRendererPkg = path.join(PKG_DIR, 'bin', 'statusline-node.js');
555
+ const nodeRenderer = fs.existsSync(nodeRendererInstalled) ? nodeRendererInstalled : nodeRendererPkg;
556
+
557
+ if (isWin || !fs.existsSync(path.join(SL_DIR, 'core.sh'))) {
558
+ // Use Node.js renderer
559
+ try {
560
+ const result = execSync(`node "${nodeRenderer.replace(/\\/g, '/')}"`, {
561
+ input: sampleJson,
562
+ encoding: 'utf8',
563
+ timeout: 5000
564
+ });
565
+ log('');
566
+ log(result);
567
+ log('');
568
+ } catch (e) {
569
+ warn(`Preview failed: ${e.message}`);
570
+ }
534
571
  } else {
535
- // Use the package's own script
536
- scriptPath = path.join(PKG_DIR, 'lib', 'core.sh');
537
- }
572
+ // Unix: use bash engine with theme/layout overrides
573
+ const coreFile = path.join(SL_DIR, 'core.sh');
574
+ const scriptPath = fs.existsSync(coreFile) ? coreFile : SCRIPT_DEST;
538
575
 
539
- const env = { ...process.env };
540
- if (themeName) env.STATUSLINE_THEME_OVERRIDE = themeName;
541
- if (layoutName) env.STATUSLINE_LAYOUT_OVERRIDE = layoutName;
576
+ const env = { ...process.env };
577
+ if (themeName) env.STATUSLINE_THEME_OVERRIDE = themeName;
578
+ if (layoutName) env.STATUSLINE_LAYOUT_OVERRIDE = layoutName;
542
579
 
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
- }
580
+ if (!fs.existsSync(path.join(SL_DIR, 'core.sh'))) {
581
+ env.HOME = PKG_DIR;
582
+ }
548
583
 
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}`);
584
+ try {
585
+ const escaped = sampleJson.replace(/'/g, "'\\''");
586
+ const result = execSync(`printf '%s' '${escaped}' | bash "${scriptPath.replace(/\\/g, '/')}"`, {
587
+ encoding: 'utf8',
588
+ env,
589
+ timeout: 5000
590
+ });
591
+ log('');
592
+ log(result);
593
+ log('');
594
+ } catch (e) {
595
+ warn(`Preview failed: ${e.message}`);
596
+ }
561
597
  }
562
598
  }
563
599
 
@@ -816,23 +852,55 @@ function doctor() {
816
852
  warn(`No config file (using defaults)`);
817
853
  }
818
854
 
819
- // 9. Performance benchmark
855
+ // 9. Node.js renderer
856
+ const nodeRendererPath = path.join(CLAUDE_DIR, 'statusline-node.js');
857
+ if (fs.existsSync(nodeRendererPath)) {
858
+ success(`Node.js renderer installed (statusline-node.js)`);
859
+ } else if (process.platform === 'win32') {
860
+ fail(`Node.js renderer missing — required on Windows`);
861
+ info(`Run ${CYN}ccsl install${R} to fix`);
862
+ issues++;
863
+ } else {
864
+ bar(`Node.js renderer not installed (optional on Unix)`);
865
+ }
866
+
867
+ // 10. Performance benchmark
820
868
  blank();
821
869
  info(`${B}Performance benchmark${R}`);
822
870
  blank();
823
871
  try {
824
872
  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)) {
873
+
874
+ // Benchmark Node.js renderer
875
+ if (fs.existsSync(nodeRendererPath)) {
827
876
  const start = Date.now();
828
- execSync(`printf '%s' '${sampleJson}' | bash "${target.replace(/\\/g, '/')}"`, {
877
+ execSync(`node "${nodeRendererPath.replace(/\\/g, '/')}"`, {
878
+ input: sampleJson,
829
879
  encoding: 'utf8',
830
880
  timeout: 10000
831
881
  });
832
882
  const elapsed = Date.now() - start;
833
883
  const color = elapsed < 50 ? GRN : elapsed < 100 ? YLW : RED;
834
884
  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)`);
885
+ log(` ${GRAY}\u2502${R} ${color}\u25CF${R} Node.js renderer: ${color}${B}${elapsed}ms${R} (${label})`);
886
+ }
887
+
888
+ // Benchmark bash engine (skip on Windows — known slow)
889
+ if (process.platform !== 'win32') {
890
+ const target = fs.existsSync(coreFile) ? coreFile : SCRIPT_DEST;
891
+ if (target && fs.existsSync(target)) {
892
+ const start = Date.now();
893
+ execSync(`printf '%s' '${sampleJson}' | bash "${target.replace(/\\/g, '/')}"`, {
894
+ encoding: 'utf8',
895
+ timeout: 10000
896
+ });
897
+ const elapsed = Date.now() - start;
898
+ const color = elapsed < 50 ? GRN : elapsed < 100 ? YLW : RED;
899
+ const label = elapsed < 50 ? 'excellent' : elapsed < 100 ? 'good' : 'slow';
900
+ log(` ${GRAY}\u2502${R} ${color}\u25CF${R} Bash engine: ${color}${B}${elapsed}ms${R} (${label})`);
901
+ }
902
+ } else {
903
+ bar(`Bash benchmark skipped (slow on Windows, using Node.js)`);
836
904
  }
837
905
  } catch (e) {
838
906
  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.0",
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"