lumira 1.8.0 → 1.8.2
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 +8 -4
- package/dist/installer.js +98 -19
- package/dist/parsers/gsd.js +137 -16
- package/dist/render/line4.js +13 -4
- package/dist/types.js +5 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,7 @@ Interactive wizard — preset, theme, icons — previewed live before write.
|
|
|
26
26
|
|
|
27
27
|
> 3,400+ monthly downloads, zero marketing. Try it for one session — `npx lumira install`.
|
|
28
28
|
|
|
29
|
-
> **What's new in v1.8.
|
|
29
|
+
> **What's new in v1.8.2:** the installer now writes a fast per-render command — it runs the compiled `lumira` binary directly (~10× faster than `npx lumira@latest`, which hit the npm registry on every render) and migrates existing setups automatically. v1.8.1 brought the GSD widget to parity with get-shit-done (GSD)'s own statusline — phase/milestone lifecycle, a milestone progress bar, and `⬆ /gsd:update` / `⚠ stale hooks` indicators that show in any project (on by default, self-gating). Earlier releases added the compaction counter `⊙ N` (v1.8.0), added-dirs badge + worktree breadcrumb (v1.7.0), [`lumira stats` CLI](#stats-cli) (v1.5), `API N%` latency widget (v1.4.0), 7-day quota projection (v1.3.0), and the auto-compact proximity glyph ⚠ (v1.4.1).
|
|
30
30
|
|
|
31
31
|
## Table of contents
|
|
32
32
|
|
|
@@ -103,6 +103,8 @@ npx lumira install
|
|
|
103
103
|
|
|
104
104
|
The installer walks you through three choices — **preset** (`full` / `balanced` / `minimal`), **theme**, and **icons** — showing a live preview at each step. Press `Esc` to abort without writing anything. In non-interactive shells (piped stdin, CI), the installer skips the wizard and writes sensible defaults (`preset: balanced`, `icons: nerd`). If Qwen Code is detected (`~/.qwen/` exists), the `/lumira` skill is installed for both CLIs.
|
|
105
105
|
|
|
106
|
+
For the fastest statusline (the command runs on **every** render), the installer offers to install lumira globally so it can invoke the compiled binary directly (`lumira`, ~60ms) instead of `npx` (which is ~10× slower). It also migrates older `npx lumira@latest` setups to the faster form automatically.
|
|
107
|
+
|
|
106
108
|
Or install globally:
|
|
107
109
|
|
|
108
110
|
```bash
|
|
@@ -120,19 +122,19 @@ Your preferences are saved to `~/.config/lumira/config.json` — hand-edited key
|
|
|
120
122
|
|
|
121
123
|
### Manual setup
|
|
122
124
|
|
|
123
|
-
|
|
125
|
+
The `statusLine.command` runs on every render, so prefer the **direct binary**. Install globally (`npm install -g lumira`), then add to `~/.claude/settings.json`:
|
|
124
126
|
|
|
125
127
|
```json
|
|
126
128
|
{
|
|
127
129
|
"statusLine": {
|
|
128
130
|
"type": "command",
|
|
129
|
-
"command": "
|
|
131
|
+
"command": "lumira",
|
|
130
132
|
"padding": 0
|
|
131
133
|
}
|
|
132
134
|
}
|
|
133
135
|
```
|
|
134
136
|
|
|
135
|
-
If installed from source:
|
|
137
|
+
If installed from source, point at the compiled entry:
|
|
136
138
|
|
|
137
139
|
```json
|
|
138
140
|
{
|
|
@@ -144,6 +146,8 @@ If installed from source:
|
|
|
144
146
|
}
|
|
145
147
|
```
|
|
146
148
|
|
|
149
|
+
> Without a global install you can use `"command": "npx lumira"` — it works, but resolves through npx on every render (~10× slower). Avoid `npx lumira@latest`: the `@latest` hits the npm registry on every render.
|
|
150
|
+
|
|
147
151
|
## Display
|
|
148
152
|
|
|
149
153
|
### Custom Mode (default, >=70 columns)
|
package/dist/installer.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync, copyFileSync, unlinkSync, mkdirSync, rmdirSync, renameSync, openSync, writeSync, fsyncSync, closeSync } from 'node:fs';
|
|
2
2
|
import { join, dirname, resolve } from 'node:path';
|
|
3
3
|
import { homedir } from 'node:os';
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
4
5
|
import { sanitizeTermString } from './normalize.js';
|
|
5
6
|
import { fileURLToPath } from 'node:url';
|
|
6
7
|
import { createInterface } from 'node:readline';
|
|
@@ -17,11 +18,67 @@ const ok = (msg) => `${GREEN}✓${RST} ${msg}`;
|
|
|
17
18
|
const warn = (msg) => `${YELLOW}⚠${RST} ${msg}`;
|
|
18
19
|
const header = () => `\n${CYAN} lumira installer${RST}\n`;
|
|
19
20
|
// ── StatusLine value ────────────────────────────────────────────────
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
// The per-render command. `lumira` (a real global bin) runs the compiled
|
|
22
|
+
// binary directly (~60ms). `npx lumira` resolves from cache (~150-300ms).
|
|
23
|
+
// `npx lumira@latest` hits the npm registry EVERY render (~600ms) — never
|
|
24
|
+
// write that form; it's the perf bug this installer migrates away from.
|
|
25
|
+
function makeStatusLine(command) {
|
|
26
|
+
return { type: 'command', command, padding: 0 };
|
|
27
|
+
}
|
|
28
|
+
// Rank a statusLine command by per-render speed (higher = faster).
|
|
29
|
+
// 2 = direct binary (`lumira`, `node …/dist/index.js`, ${CLAUDE_PLUGIN_ROOT})
|
|
30
|
+
// 1 = npx, cached (`npx lumira`)
|
|
31
|
+
// 0 = npx, registry (`npx lumira@latest` / any pinned `@version`)
|
|
32
|
+
// Used to decide migration: only ever rewrite TOWARD a faster form.
|
|
33
|
+
export function commandSpeed(command) {
|
|
34
|
+
const c = command.trim();
|
|
35
|
+
// `npx` as a bare word or a path basename (e.g. /usr/local/bin/npx, …\npx).
|
|
36
|
+
if (/(^|[\s/\\])npx(\s|$)/.test(c)) {
|
|
37
|
+
return /@(latest|\d)/.test(c) ? 0 : 1;
|
|
38
|
+
}
|
|
39
|
+
return 2;
|
|
40
|
+
}
|
|
41
|
+
// Is `lumira` resolvable as a global bin on PATH?
|
|
42
|
+
function defaultHasGlobalBin() {
|
|
43
|
+
const probe = process.platform === 'win32' ? 'where' : 'which';
|
|
44
|
+
try {
|
|
45
|
+
execFileSync(probe, ['lumira'], { stdio: 'ignore', timeout: 3000 });
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Install lumira globally so the per-render command can invoke it directly.
|
|
53
|
+
function defaultInstallGlobal() {
|
|
54
|
+
try {
|
|
55
|
+
execFileSync('npm', ['install', '-g', 'lumira'], { stdio: 'inherit', timeout: 120000 });
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Resolve the fastest statusLine command available in this environment.
|
|
63
|
+
// In a TTY with no global bin, offer to `npm i -g lumira` (confirmed) so the
|
|
64
|
+
// command can be the direct `lumira`; otherwise fall back to cached `npx lumira`.
|
|
65
|
+
async function resolveStatusLineCommand(ctx) {
|
|
66
|
+
if (ctx.hasGlobalBin())
|
|
67
|
+
return 'lumira';
|
|
68
|
+
if (ctx.isTTY) {
|
|
69
|
+
const accepted = await ctx.confirm('Install lumira globally for ~10× faster rendering (npm i -g lumira)?');
|
|
70
|
+
if (accepted) {
|
|
71
|
+
if (ctx.installGlobal()) {
|
|
72
|
+
ctx.lines.push(ok('Installed lumira globally — statusline runs the compiled binary directly'));
|
|
73
|
+
return 'lumira';
|
|
74
|
+
}
|
|
75
|
+
ctx.lines.push(warn('Global install failed — using npx for now (run npm i -g lumira later for full speed)'));
|
|
76
|
+
return 'npx lumira';
|
|
77
|
+
}
|
|
78
|
+
ctx.lines.push(` ${DIM}Tip: npm i -g lumira for ~10× faster rendering${RST}`);
|
|
79
|
+
}
|
|
80
|
+
return 'npx lumira';
|
|
81
|
+
}
|
|
25
82
|
function defaultSettingsPath() {
|
|
26
83
|
return join(homedir(), '.claude', 'settings.json');
|
|
27
84
|
}
|
|
@@ -95,6 +152,8 @@ export async function install(opts = {}) {
|
|
|
95
152
|
const confirm = opts.confirm ?? promptYN;
|
|
96
153
|
const stdin = opts.stdin ?? process.stdin;
|
|
97
154
|
const stdout = opts.stdout ?? process.stdout;
|
|
155
|
+
const hasGlobalBin = opts.hasGlobalBin ?? defaultHasGlobalBin;
|
|
156
|
+
const installGlobal = opts.installGlobal ?? defaultInstallGlobal;
|
|
98
157
|
const lines = [];
|
|
99
158
|
// Build banner prelude (shown on each wizard frame so it survives screen clears)
|
|
100
159
|
let prelude = '';
|
|
@@ -159,20 +218,41 @@ export async function install(opts = {}) {
|
|
|
159
218
|
wizard = { preset: 'balanced', icons: 'nerd' };
|
|
160
219
|
lines.push(ok('Non-interactive mode — using defaults (preset: balanced, icons: nerd)'));
|
|
161
220
|
}
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
221
|
+
// Save config + emit footer + render output. Shared by every exit below.
|
|
222
|
+
const finalize = () => {
|
|
223
|
+
saveConfig(wizard, configPath);
|
|
224
|
+
lines.push(ok(`Saved config → ${DIM}${configPath}${RST}`));
|
|
225
|
+
emitFooter(lines, opts.homeOverride);
|
|
226
|
+
return lines.join('\n') + '\n';
|
|
227
|
+
};
|
|
228
|
+
// ── settings.json replace / backup / migrate ───────────────────
|
|
229
|
+
const existingIsLumira = !!settings.statusLine && isLumira(settings.statusLine);
|
|
230
|
+
const existingCmd = existingIsLumira
|
|
231
|
+
? String(settings.statusLine.command ?? '')
|
|
232
|
+
: '';
|
|
233
|
+
// Already on the fastest form (direct binary) — nothing to rewrite.
|
|
234
|
+
if (existingIsLumira && commandSpeed(existingCmd) >= 2) {
|
|
235
|
+
lines.push(ok('lumira is already configured (optimal command)'));
|
|
236
|
+
return finalize();
|
|
237
|
+
}
|
|
238
|
+
// Resolve the fastest per-render command this environment can offer.
|
|
239
|
+
const resolvedCmd = await resolveStatusLineCommand({
|
|
240
|
+
isTTY: !!stdin?.isTTY, confirm, hasGlobalBin, installGlobal, lines,
|
|
241
|
+
});
|
|
242
|
+
if (existingIsLumira) {
|
|
243
|
+
// Existing lumira command (npx form) — only rewrite if strictly faster,
|
|
244
|
+
// so we never downgrade a user's direct binary to npx.
|
|
245
|
+
if (commandSpeed(resolvedCmd) <= commandSpeed(existingCmd)) {
|
|
246
|
+
lines.push(ok('lumira is already configured'));
|
|
247
|
+
return finalize();
|
|
170
248
|
}
|
|
249
|
+
}
|
|
250
|
+
else if (settings.statusLine) {
|
|
171
251
|
// Foreign statusLine already confirmed above — back it up and replace.
|
|
172
252
|
copyFileSync(settingsPath, backupPath);
|
|
173
253
|
lines.push(ok(`Backed up existing settings → ${DIM}settings.json.lumira.bak${RST}`));
|
|
174
254
|
}
|
|
175
|
-
settings.statusLine =
|
|
255
|
+
settings.statusLine = makeStatusLine(resolvedCmd);
|
|
176
256
|
mkdirSync(dirname(settingsPath), { recursive: true });
|
|
177
257
|
const tmp = `${settingsPath}.${process.pid}.${Date.now()}.lumira.tmp`;
|
|
178
258
|
try {
|
|
@@ -189,11 +269,10 @@ export async function install(opts = {}) {
|
|
|
189
269
|
catch { }
|
|
190
270
|
throw e;
|
|
191
271
|
}
|
|
192
|
-
lines.push(ok(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return lines.join('\n') + '\n';
|
|
272
|
+
lines.push(ok(existingIsLumira
|
|
273
|
+
? `Upgraded statusline command → ${DIM}${resolvedCmd}${RST} (faster)`
|
|
274
|
+
: 'Configured lumira as statusline'));
|
|
275
|
+
return finalize();
|
|
197
276
|
}
|
|
198
277
|
// ── Uninstall ───────────────────────────────────────────────────────
|
|
199
278
|
export function uninstall(opts = {}) {
|
package/dist/parsers/gsd.js
CHANGED
|
@@ -14,13 +14,15 @@ export function parseStateMd(content) {
|
|
|
14
14
|
const state = {};
|
|
15
15
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
16
16
|
if (fmMatch) {
|
|
17
|
-
|
|
17
|
+
const fmText = fmMatch[1];
|
|
18
|
+
// Parse simple scalar fields
|
|
19
|
+
for (const line of fmText.split('\n')) {
|
|
18
20
|
const m = line.match(/^(\w+):\s*(.+)/);
|
|
19
21
|
if (!m)
|
|
20
22
|
continue;
|
|
21
23
|
const [, key, val] = m;
|
|
22
24
|
const v = val.trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
23
|
-
if (v === 'null')
|
|
25
|
+
if (v === 'null' || v === '')
|
|
24
26
|
continue;
|
|
25
27
|
if (key === 'status')
|
|
26
28
|
state.status = v;
|
|
@@ -28,6 +30,51 @@ export function parseStateMd(content) {
|
|
|
28
30
|
state.milestone = v;
|
|
29
31
|
else if (key === 'milestone_name')
|
|
30
32
|
state.milestoneName = v;
|
|
33
|
+
else if (key === 'active_phase')
|
|
34
|
+
state.activePhase = v;
|
|
35
|
+
else if (key === 'next_action')
|
|
36
|
+
state.nextAction = v;
|
|
37
|
+
}
|
|
38
|
+
// Parse next_phases: flow form [a, b]
|
|
39
|
+
const flowMatch = fmText.match(/^next_phases:\s*\[([^\]]*)\]/m);
|
|
40
|
+
if (flowMatch && flowMatch[1]) {
|
|
41
|
+
const items = flowMatch[1].split(',').map(s => {
|
|
42
|
+
const trimmed = s.trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
43
|
+
return trimmed;
|
|
44
|
+
}).filter(s => s.length > 0);
|
|
45
|
+
if (items.length > 0)
|
|
46
|
+
state.nextPhases = items;
|
|
47
|
+
}
|
|
48
|
+
// Parse next_phases: block-list form
|
|
49
|
+
if (!state.nextPhases) {
|
|
50
|
+
const blockMatch = fmText.match(/^next_phases:\s*\n((?:[ \t]*-[ \t]*[^\n]+\n?)*)/m);
|
|
51
|
+
if (blockMatch && blockMatch[1]) {
|
|
52
|
+
const items = [];
|
|
53
|
+
for (const itemLine of blockMatch[1].split('\n')) {
|
|
54
|
+
const itemM = itemLine.match(/^[ \t]*-[ \t]*(.+)/);
|
|
55
|
+
if (itemM) {
|
|
56
|
+
const itemVal = itemM[1].trim().replace(/^(["'])(.*)\1$/, '$2');
|
|
57
|
+
if (itemVal)
|
|
58
|
+
items.push(itemVal);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (items.length > 0)
|
|
62
|
+
state.nextPhases = items;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Parse progress block
|
|
66
|
+
const progressMatch = fmText.match(/^progress:\s*\n((?:[ \t]+\w+:.+\n?)+)/m);
|
|
67
|
+
if (progressMatch && progressMatch[1]) {
|
|
68
|
+
const progressText = progressMatch[1];
|
|
69
|
+
const completedM = progressText.match(/completed_phases:\s*(\d+)/);
|
|
70
|
+
if (completedM)
|
|
71
|
+
state.completedPhases = completedM[1];
|
|
72
|
+
const totalM = progressText.match(/total_phases:\s*(\d+)/);
|
|
73
|
+
if (totalM)
|
|
74
|
+
state.totalPhases = totalM[1];
|
|
75
|
+
const percentM = progressText.match(/percent:\s*(\d+)/);
|
|
76
|
+
if (percentM)
|
|
77
|
+
state.percent = percentM[1];
|
|
31
78
|
}
|
|
32
79
|
}
|
|
33
80
|
const phaseMatch = content.match(/^Phase:\s*(\d+)\s+of\s+(\d+)(?:\s+\(([^)]+)\))?/m);
|
|
@@ -51,6 +98,24 @@ export function parseStateMd(content) {
|
|
|
51
98
|
}
|
|
52
99
|
return state;
|
|
53
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* Render a 10-segment progress bar: `█████░░░░░ 50%`.
|
|
103
|
+
*
|
|
104
|
+
* INTENTIONAL deviation from GSD's own statusline (gsd-statusline.js wraps the
|
|
105
|
+
* bar in brackets — `[█████░░░░░] 50%`). lumira drops the brackets so the
|
|
106
|
+
* milestone bar reads as distinct from the line-2 context bar. Do NOT re-add
|
|
107
|
+
* brackets when re-syncing GSD's format — this is a deliberate design choice.
|
|
108
|
+
*/
|
|
109
|
+
function renderProgressBar(percent) {
|
|
110
|
+
if (percent === undefined || percent === null)
|
|
111
|
+
return '';
|
|
112
|
+
const pct = Math.max(0, Math.min(100, parseInt(String(percent), 10)));
|
|
113
|
+
if (isNaN(pct))
|
|
114
|
+
return '';
|
|
115
|
+
const filled = Math.floor(pct / 10);
|
|
116
|
+
const bar = '█'.repeat(filled) + '░'.repeat(10 - filled);
|
|
117
|
+
return `${bar} ${pct}%`;
|
|
118
|
+
}
|
|
54
119
|
/** Walk up from `cwd` looking for `.planning/STATE.md`; stop at home or filesystem root. */
|
|
55
120
|
export function findStateMd(cwd) {
|
|
56
121
|
const home = homedir();
|
|
@@ -69,27 +134,64 @@ export function findStateMd(cwd) {
|
|
|
69
134
|
/** Format a GSD state into a compact status string: `milestone · status · phase`. */
|
|
70
135
|
function formatState(s) {
|
|
71
136
|
const parts = [];
|
|
137
|
+
// Milestone segment with optional progress bar
|
|
72
138
|
if (s.milestone || s.milestoneName) {
|
|
73
139
|
const ver = s.milestone ?? '';
|
|
74
140
|
const name = s.milestoneName && s.milestoneName !== 'milestone' ? s.milestoneName : '';
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
141
|
+
const bar = renderProgressBar(s.percent);
|
|
142
|
+
const msParts = [ver, name, bar].filter(Boolean);
|
|
143
|
+
if (msParts.length > 0)
|
|
144
|
+
parts.push(msParts.join(' '));
|
|
145
|
+
}
|
|
146
|
+
// Scene selection: activePhase → nextAction → milestone-complete → default
|
|
147
|
+
const phasesStr = s.nextPhases?.length ? s.nextPhases.join('/') : null;
|
|
148
|
+
if (s.activePhase) {
|
|
149
|
+
// Scene 1: activePhase (with optional status)
|
|
150
|
+
parts.push(s.status ? `Phase ${s.activePhase} ${s.status}` : `Phase ${s.activePhase}`);
|
|
78
151
|
}
|
|
79
|
-
if (s.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
152
|
+
else if (s.nextAction && phasesStr) {
|
|
153
|
+
// Scene 2: nextAction + phases when idle
|
|
154
|
+
parts.push(`next ${s.nextAction} ${phasesStr}`);
|
|
155
|
+
}
|
|
156
|
+
else if (Number(s.percent) === 100 || (s.completedPhases && s.totalPhases && s.completedPhases === s.totalPhases)) {
|
|
157
|
+
// Scene 3: milestone complete
|
|
158
|
+
parts.push('milestone complete');
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
// Scene 4 (default): preserve existing behavior
|
|
162
|
+
if (s.status)
|
|
163
|
+
parts.push(s.status);
|
|
164
|
+
if (s.phaseNum && s.phaseTotal) {
|
|
165
|
+
const phase = s.phaseName ? `${s.phaseName} (${s.phaseNum}/${s.phaseTotal})` : `ph ${s.phaseNum}/${s.phaseTotal}`;
|
|
166
|
+
parts.push(phase);
|
|
167
|
+
}
|
|
84
168
|
}
|
|
85
169
|
return parts.join(' · ');
|
|
86
170
|
}
|
|
171
|
+
/** Compare two semver versions. Returns 1 if a > b, -1 if a < b, 0 if equal. */
|
|
172
|
+
function semverCompare(a, b) {
|
|
173
|
+
const parseVer = (v) => {
|
|
174
|
+
const parts = v.replace(/^v/, '').split('.').map(p => parseInt(p, 10));
|
|
175
|
+
return { major: parts[0] ?? 0, minor: parts[1] ?? 0, patch: parts[2] ?? 0 };
|
|
176
|
+
};
|
|
177
|
+
const av = parseVer(a);
|
|
178
|
+
const bv = parseVer(b);
|
|
179
|
+
if (av.major !== bv.major)
|
|
180
|
+
return av.major > bv.major ? 1 : -1;
|
|
181
|
+
if (av.minor !== bv.minor)
|
|
182
|
+
return av.minor > bv.minor ? 1 : -1;
|
|
183
|
+
if (av.patch !== bv.patch)
|
|
184
|
+
return av.patch > bv.patch ? 1 : -1;
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
87
187
|
/**
|
|
88
188
|
* Read GSD update-check cache. Checks the shared tool-agnostic cache first
|
|
89
189
|
* (`~/.cache/gsd/`, introduced by GSD #1421), then falls back to the legacy
|
|
90
190
|
* per-runtime location (`~/.claude/cache/`) for older GSD installs.
|
|
191
|
+
* Returns update status, stale hooks flag, and dev install flag.
|
|
91
192
|
*/
|
|
92
193
|
function readUpdateCache(sharedCacheFile, legacyCacheFile) {
|
|
194
|
+
const result = { updateAvailable: false, staleHooks: false, devInstall: false };
|
|
93
195
|
const candidates = [
|
|
94
196
|
['shared', sharedCacheFile],
|
|
95
197
|
['legacy', legacyCacheFile],
|
|
@@ -100,19 +202,33 @@ function readUpdateCache(sharedCacheFile, legacyCacheFile) {
|
|
|
100
202
|
try {
|
|
101
203
|
const parsed = JSON.parse(readFileSync(file, 'utf8'));
|
|
102
204
|
if (parsed.update_available) {
|
|
205
|
+
result.updateAvailable = true;
|
|
103
206
|
log('update cache:', source, file);
|
|
104
|
-
return true;
|
|
105
207
|
}
|
|
208
|
+
if (Array.isArray(parsed.stale_hooks) && parsed.stale_hooks.length > 0) {
|
|
209
|
+
result.staleHooks = true;
|
|
210
|
+
}
|
|
211
|
+
// DevInstall: stale_hooks present AND installed is ahead of latest.
|
|
212
|
+
// Guard against an unknown/missing latest explicitly (mirrors GSD's own
|
|
213
|
+
// check) rather than relying on NaN comparison semantics in semverCompare.
|
|
214
|
+
if (result.staleHooks &&
|
|
215
|
+
parsed.installed &&
|
|
216
|
+
parsed.latest &&
|
|
217
|
+
parsed.latest !== 'unknown' &&
|
|
218
|
+
semverCompare(parsed.installed, parsed.latest) > 0) {
|
|
219
|
+
result.devInstall = true;
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
106
222
|
}
|
|
107
223
|
catch { /* ignore malformed */ }
|
|
108
224
|
}
|
|
109
|
-
return
|
|
225
|
+
return result;
|
|
110
226
|
}
|
|
111
227
|
export function getGsdInfo(cwd, opts = {}) {
|
|
112
228
|
const claudeDir = opts.claudeDir ?? process.env['CLAUDE_CONFIG_DIR'] ?? join(homedir(), '.claude');
|
|
113
229
|
const sharedCacheFile = opts.sharedCacheFile ?? join(homedir(), '.cache', 'gsd', 'gsd-update-check.json');
|
|
114
230
|
const legacyCacheFile = join(claudeDir, 'cache', 'gsd-update-check.json');
|
|
115
|
-
const
|
|
231
|
+
const cacheData = readUpdateCache(sharedCacheFile, legacyCacheFile);
|
|
116
232
|
let currentTask;
|
|
117
233
|
const stateFile = findStateMd(cwd || process.cwd());
|
|
118
234
|
if (stateFile) {
|
|
@@ -132,10 +248,15 @@ export function getGsdInfo(cwd, opts = {}) {
|
|
|
132
248
|
else {
|
|
133
249
|
log('no STATE.md found walking up from:', cwd || process.cwd());
|
|
134
250
|
}
|
|
135
|
-
if (!updateAvailable && !currentTask) {
|
|
136
|
-
log('no gsd signal — update=false, task=none (line4 will be empty)');
|
|
251
|
+
if (!cacheData.updateAvailable && !cacheData.staleHooks && !currentTask) {
|
|
252
|
+
log('no gsd signal — update=false, staleHooks=false, task=none (line4 will be empty)');
|
|
137
253
|
return null;
|
|
138
254
|
}
|
|
139
|
-
return {
|
|
255
|
+
return {
|
|
256
|
+
updateAvailable: cacheData.updateAvailable || undefined,
|
|
257
|
+
staleHooks: cacheData.staleHooks || undefined,
|
|
258
|
+
devInstall: cacheData.devInstall || undefined,
|
|
259
|
+
currentTask,
|
|
260
|
+
};
|
|
140
261
|
}
|
|
141
262
|
//# sourceMappingURL=gsd.js.map
|
package/dist/render/line4.js
CHANGED
|
@@ -3,14 +3,23 @@ import { getCustomCommandsForLine, renderCustomCommand } from './shared.js';
|
|
|
3
3
|
export function renderLine4(ctx, c) {
|
|
4
4
|
const { gsd, icons } = ctx;
|
|
5
5
|
const parts = [];
|
|
6
|
-
// GSD widget — only emit when GSD has something to display.
|
|
7
|
-
|
|
6
|
+
// GSD widget — only emit when GSD has something to display. Text and glyphs
|
|
7
|
+
// mirror GSD's own statusline (gsd-statusline.js) so the integration reads
|
|
8
|
+
// identically. The update/stale-hooks indicators render even without a
|
|
9
|
+
// current task, so a GSD update is visible in any project (gated only on the
|
|
10
|
+
// update-check cache, not on being inside a GSD project).
|
|
11
|
+
if (gsd && (gsd.currentTask || gsd.updateAvailable || gsd.staleHooks)) {
|
|
8
12
|
parts.push(c.dim('GSD'));
|
|
9
13
|
if (gsd.currentTask) {
|
|
10
|
-
parts.push(c.bold(`${icons.hammer} ${truncField(gsd.currentTask,
|
|
14
|
+
parts.push(c.bold(`${icons.hammer} ${truncField(gsd.currentTask, 60)}`));
|
|
11
15
|
}
|
|
12
16
|
if (gsd.updateAvailable) {
|
|
13
|
-
parts.push(c.yellow(
|
|
17
|
+
parts.push(c.yellow('⬆ /gsd:update'));
|
|
18
|
+
}
|
|
19
|
+
if (gsd.staleHooks) {
|
|
20
|
+
parts.push(gsd.devInstall
|
|
21
|
+
? c.yellow('⚠ dev install — re-run installer to sync hooks')
|
|
22
|
+
: c.red('⚠ stale hooks — run /gsd:update'));
|
|
14
23
|
}
|
|
15
24
|
}
|
|
16
25
|
// Custom commands (issue #143 phase 3) — line 4 is the lowest-priority line
|
package/dist/types.js
CHANGED
|
@@ -113,7 +113,11 @@ export const DEFAULT_DISPLAY = {
|
|
|
113
113
|
};
|
|
114
114
|
export const DEFAULT_CONFIG = {
|
|
115
115
|
layout: 'auto',
|
|
116
|
-
|
|
116
|
+
// GSD on by default, mirroring GSD's own always-on statusline. Self-gates to
|
|
117
|
+
// nothing when there's no .planning/STATE.md and no update-check cache, so
|
|
118
|
+
// non-GSD users see no extra line and pay only a few cheap existsSync checks.
|
|
119
|
+
// Minimal/singleline returns early (renderMinimal) and never reaches line 4.
|
|
120
|
+
gsd: true,
|
|
117
121
|
display: { ...DEFAULT_DISPLAY },
|
|
118
122
|
colors: { mode: 'auto' },
|
|
119
123
|
customCommands: { enabled: false, commands: [] },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lumira",
|
|
3
|
-
"version": "1.8.
|
|
3
|
+
"version": "1.8.2",
|
|
4
4
|
"description": "Real-time statusline HUD for Claude Code and Qwen Code. Includes session analytics CLI, API latency overhead widget, 7d quota projection, auto-compact proximity warnings, themes, and powerline. Zero deps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|