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 +12 -8
- package/bin/cli.js +138 -56
- package/bin/statusline-node.js +17 -21
- package/package.json +2 -2
- package/bin/cli.js.bak +0 -837
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
|
-
|
|
127
|
+
Two rendering engines — the 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 #
|
|
136
|
+
statusline-command.sh # Bash entry point (macOS/Linux)
|
|
137
|
+
statusline-node.js # Node.js renderer (Windows)
|
|
132
138
|
statusline/
|
|
133
|
-
core.sh #
|
|
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
|
-
-
|
|
151
|
+
- Node.js >=16 (installer + Windows renderer)
|
|
148
152
|
- Git (for GitHub field)
|
|
149
|
-
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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' +
|
|
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
|
-
|
|
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
|
|
377
|
+
// Update settings.json
|
|
356
378
|
const settings = readSettings();
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
362
|
-
//
|
|
363
|
-
const
|
|
364
|
-
|
|
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
|
-
|
|
412
|
-
|
|
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
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
//
|
|
536
|
-
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
590
|
+
const env = { ...process.env };
|
|
591
|
+
if (themeName) env.STATUSLINE_THEME_OVERRIDE = themeName;
|
|
592
|
+
if (layoutName) env.STATUSLINE_LAYOUT_OVERRIDE = layoutName;
|
|
542
593
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
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.
|
|
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
|
-
|
|
826
|
-
|
|
887
|
+
|
|
888
|
+
// Benchmark Node.js renderer
|
|
889
|
+
if (fs.existsSync(nodeRendererPath)) {
|
|
827
890
|
const start = Date.now();
|
|
828
|
-
execSync(`
|
|
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}
|
|
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)}`);
|
package/bin/statusline-node.js
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// skill-statusline v2.
|
|
2
|
+
// skill-statusline v2.4 — Node.js statusline renderer
|
|
3
3
|
// Zero bash dependency — works on Windows, macOS, Linux
|
|
4
|
-
//
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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'
|
|
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.
|
|
4
|
-
"description": "Rich, themeable statusline for Claude Code — accurate context tracking, 5 themes, 3 layouts, token/cost/GitHub/skill display.
|
|
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
|
-
}
|