qualia-framework 2.1.5 → 2.1.6
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/bin/cli.js +14 -9
- package/framework/statusline-command.js +111 -0
- package/framework/statusline-command.sh +6 -5
- package/package.json +1 -1
package/bin/cli.js
CHANGED
|
@@ -139,7 +139,12 @@ function mergeSettings(existingPath, templatePath) {
|
|
|
139
139
|
// Overwrite hooks, permissions, statusLine from template (framework-managed)
|
|
140
140
|
merged.hooks = template.hooks;
|
|
141
141
|
merged.permissions = template.permissions;
|
|
142
|
-
|
|
142
|
+
// Use node-based statusline on Windows (no bash), bash on Unix
|
|
143
|
+
if (process.platform === 'win32') {
|
|
144
|
+
merged.statusLine = { type: 'command', command: 'node ~/.claude/statusline-command.js' };
|
|
145
|
+
} else {
|
|
146
|
+
merged.statusLine = template.statusLine;
|
|
147
|
+
}
|
|
143
148
|
|
|
144
149
|
// Preserve user's existing plugins and MCP servers
|
|
145
150
|
if (existing.enabledPlugins) {
|
|
@@ -233,8 +238,8 @@ async function runInstall() {
|
|
|
233
238
|
}
|
|
234
239
|
}
|
|
235
240
|
|
|
236
|
-
// Copy standalone files
|
|
237
|
-
for (const f of ['statusline-command.sh', 'askpass.sh']) {
|
|
241
|
+
// Copy standalone files (both .sh and .js for cross-platform)
|
|
242
|
+
for (const f of ['statusline-command.sh', 'statusline-command.js', 'askpass.sh']) {
|
|
238
243
|
const src = path.join(FRAMEWORK_DIR, f);
|
|
239
244
|
if (fs.existsSync(src)) {
|
|
240
245
|
fs.copyFileSync(src, path.join(CLAUDE_DIR, f));
|
|
@@ -251,9 +256,9 @@ async function runInstall() {
|
|
|
251
256
|
}
|
|
252
257
|
}
|
|
253
258
|
// Make standalone scripts executable
|
|
254
|
-
for (const f of ['statusline-command.sh', 'askpass.sh']) {
|
|
259
|
+
for (const f of ['statusline-command.sh', 'statusline-command.js', 'askpass.sh']) {
|
|
255
260
|
const p = path.join(CLAUDE_DIR, f);
|
|
256
|
-
if (fs.existsSync(p)) fs.chmodSync(p, 0o755);
|
|
261
|
+
if (fs.existsSync(p)) try { fs.chmodSync(p, 0o755); } catch {}
|
|
257
262
|
}
|
|
258
263
|
// Make qualia-engine bin executable
|
|
259
264
|
const engineBin = path.join(CLAUDE_DIR, 'qualia-engine', 'bin');
|
|
@@ -411,12 +416,12 @@ async function runUpdate() {
|
|
|
411
416
|
}
|
|
412
417
|
}
|
|
413
418
|
|
|
414
|
-
// Copy standalone files
|
|
415
|
-
for (const f of ['statusline-command.sh', 'askpass.sh']) {
|
|
419
|
+
// Copy standalone files (both .sh and .js for cross-platform)
|
|
420
|
+
for (const f of ['statusline-command.sh', 'statusline-command.js', 'askpass.sh']) {
|
|
416
421
|
const src = path.join(FRAMEWORK_DIR, f);
|
|
417
422
|
if (fs.existsSync(src)) {
|
|
418
423
|
fs.copyFileSync(src, path.join(CLAUDE_DIR, f));
|
|
419
|
-
fs.chmodSync(path.join(CLAUDE_DIR, f), 0o755);
|
|
424
|
+
try { fs.chmodSync(path.join(CLAUDE_DIR, f), 0o755); } catch {}
|
|
420
425
|
}
|
|
421
426
|
}
|
|
422
427
|
|
|
@@ -424,7 +429,7 @@ async function runUpdate() {
|
|
|
424
429
|
const hooksDir = path.join(CLAUDE_DIR, 'hooks');
|
|
425
430
|
if (fs.existsSync(hooksDir)) {
|
|
426
431
|
for (const f of fs.readdirSync(hooksDir)) {
|
|
427
|
-
if (f.endsWith('.sh')) fs.chmodSync(path.join(hooksDir, f), 0o755);
|
|
432
|
+
if (f.endsWith('.sh')) try { fs.chmodSync(path.join(hooksDir, f), 0o755); } catch {}
|
|
428
433
|
}
|
|
429
434
|
}
|
|
430
435
|
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Qualia statusline — cross-platform (Windows/Mac/Linux)
|
|
3
|
+
// Layout: ◆ dir branch [status] │ phase │ model ━━━━ 23%
|
|
4
|
+
|
|
5
|
+
const { execSync } = require('child_process');
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
|
|
9
|
+
let input = '';
|
|
10
|
+
process.stdin.on('data', c => input += c);
|
|
11
|
+
process.stdin.on('end', () => {
|
|
12
|
+
try {
|
|
13
|
+
const data = JSON.parse(input);
|
|
14
|
+
const usedPct = data.context_window?.used_percentage;
|
|
15
|
+
const model = data.model?.display_name || '';
|
|
16
|
+
const cwd = data.workspace?.current_dir || process.cwd();
|
|
17
|
+
|
|
18
|
+
if (!usedPct) process.exit(0);
|
|
19
|
+
|
|
20
|
+
const usedInt = Math.round(usedPct);
|
|
21
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
22
|
+
|
|
23
|
+
// Directory shortening
|
|
24
|
+
let dir = cwd.replace(home, '~').replace(/\\/g, '/');
|
|
25
|
+
const parts = dir.split('/');
|
|
26
|
+
if (parts.length > 3) dir = '…/' + parts.slice(-2).join('/');
|
|
27
|
+
|
|
28
|
+
// Colors
|
|
29
|
+
const T = '\x1b[38;2;0;188;175m';
|
|
30
|
+
const TB = '\x1b[38;2;45;226;210m';
|
|
31
|
+
const TD = '\x1b[38;2;0;120;112m';
|
|
32
|
+
const G = '\x1b[38;2;70;78;88m';
|
|
33
|
+
const Y = '\x1b[38;2;234;179;8m';
|
|
34
|
+
const R = '\x1b[38;2;239;68;68m';
|
|
35
|
+
const P = '\x1b[38;2;189;147;249m';
|
|
36
|
+
const X = '\x1b[0m';
|
|
37
|
+
|
|
38
|
+
const CB = usedInt >= 70 ? R : usedInt >= 50 ? Y : T;
|
|
39
|
+
|
|
40
|
+
// Project + phase detection
|
|
41
|
+
let project = '';
|
|
42
|
+
let phase = '';
|
|
43
|
+
const stateFile = path.join(cwd, '.planning', 'STATE.md');
|
|
44
|
+
if (fs.existsSync(stateFile)) {
|
|
45
|
+
const state = fs.readFileSync(stateFile, 'utf8');
|
|
46
|
+
const pm = state.match(/^Project:\s*(.+)/m);
|
|
47
|
+
if (pm) project = pm[1].trim();
|
|
48
|
+
const phm = state.match(/^Phase:\s*(.+)/m);
|
|
49
|
+
if (phm) {
|
|
50
|
+
const raw = phm[1].trim();
|
|
51
|
+
const nm = raw.match(/^(\d+ of \d+)/);
|
|
52
|
+
phase = nm ? nm[1] : raw.substring(0, 12);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (!project) {
|
|
57
|
+
const pkgFile = path.join(cwd, 'package.json');
|
|
58
|
+
if (fs.existsSync(pkgFile)) {
|
|
59
|
+
try { project = JSON.parse(fs.readFileSync(pkgFile, 'utf8')).name || ''; } catch {}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Git branch + status
|
|
64
|
+
let branch = '';
|
|
65
|
+
let gitStatus = '';
|
|
66
|
+
try {
|
|
67
|
+
branch = execSync('git branch --show-current', { cwd, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
68
|
+
if (branch) {
|
|
69
|
+
const raw = execSync('git status --porcelain', { cwd, stdio: ['pipe', 'pipe', 'pipe'] }).toString().trim();
|
|
70
|
+
if (raw) {
|
|
71
|
+
const lines = raw.split('\n');
|
|
72
|
+
const m = lines.filter(l => /^ M|^MM|^AM/.test(l)).length;
|
|
73
|
+
const s = lines.filter(l => /^[MARCD] /.test(l)).length;
|
|
74
|
+
const u = lines.filter(l => /^\?\?/.test(l)).length;
|
|
75
|
+
if (m > 0) gitStatus += `!${m}`;
|
|
76
|
+
if (s > 0) gitStatus += `+${s}`;
|
|
77
|
+
if (u > 0) gitStatus += `?${u}`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
} catch {}
|
|
81
|
+
|
|
82
|
+
// Context bar
|
|
83
|
+
const barW = 10;
|
|
84
|
+
const filled = Math.min(Math.round(usedInt * barW / 100), barW);
|
|
85
|
+
const empty = barW - filled;
|
|
86
|
+
const bar = '━'.repeat(filled);
|
|
87
|
+
const ebar = '─'.repeat(empty);
|
|
88
|
+
|
|
89
|
+
// Build output
|
|
90
|
+
let out = `${TB}◆${X} ${T}${dir}${X}`;
|
|
91
|
+
if (branch) {
|
|
92
|
+
out += ` ${P}${branch}${X}`;
|
|
93
|
+
if (gitStatus) out += `${Y}[${gitStatus}]${X}`;
|
|
94
|
+
}
|
|
95
|
+
out += ` ${G}│${X} `;
|
|
96
|
+
if (project) {
|
|
97
|
+
out += `${TB}${project}${X}`;
|
|
98
|
+
if (phase) out += ` ${TD}p${phase}${X}`;
|
|
99
|
+
} else if (phase) {
|
|
100
|
+
out += `${T}p${phase}${X}`;
|
|
101
|
+
} else {
|
|
102
|
+
out += `${TD}ad-hoc${X}`;
|
|
103
|
+
}
|
|
104
|
+
out += ` ${G}│${X} ${G}${model}${X}`;
|
|
105
|
+
out += ` ${CB}${bar}${G}${ebar}${X} ${CB}${usedInt}%${X}`;
|
|
106
|
+
|
|
107
|
+
process.stdout.write(out);
|
|
108
|
+
} catch {
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
|
|
5
5
|
input=$(cat)
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
# Cross-platform JSON parsing — use node instead of jq (Windows compat)
|
|
8
|
+
USED_PCT=$(echo "$input" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(d);process.stdout.write(String(j.context_window?.used_percentage||''))}catch(e){}})" 2>/dev/null)
|
|
9
|
+
MODEL=$(echo "$input" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(d);process.stdout.write(j.model?.display_name||'')}catch(e){}})" 2>/dev/null)
|
|
10
|
+
CWD=$(echo "$input" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>{try{const j=JSON.parse(d);process.stdout.write(j.workspace?.current_dir||'')}catch(e){}})" 2>/dev/null)
|
|
10
11
|
|
|
11
12
|
[ -z "$USED_PCT" ] && exit 0
|
|
12
13
|
|
|
@@ -47,8 +48,8 @@ if [ -f "$CWD/.planning/STATE.md" ]; then
|
|
|
47
48
|
# Get phase number only (e.g. "3 of 6") — not the full description
|
|
48
49
|
PHASE_RAW=$(grep -m1 "^Phase:" "$CWD/.planning/STATE.md" 2>/dev/null | sed 's/^Phase: *//')
|
|
49
50
|
if [ -n "$PHASE_RAW" ]; then
|
|
50
|
-
# Extract "N of M" pattern or just take first 12 chars
|
|
51
|
-
PHASE_SHORT=$(echo "$PHASE_RAW" |
|
|
51
|
+
# Extract "N of M" pattern or just take first 12 chars (no grep -P for Windows compat)
|
|
52
|
+
PHASE_SHORT=$(echo "$PHASE_RAW" | sed -n 's/^\([0-9]* of [0-9]*\).*/\1/p' 2>/dev/null)
|
|
52
53
|
[ -z "$PHASE_SHORT" ] && PHASE_SHORT=$(echo "$PHASE_RAW" | head -c 12)
|
|
53
54
|
PHASE="$PHASE_SHORT"
|
|
54
55
|
fi
|