@hasna/statusline 0.0.1 → 0.0.3

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/dist/cli.js ADDED
@@ -0,0 +1,1303 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ function __accessProp(key) {
9
+ return this[key];
10
+ }
11
+ var __toESMCache_node;
12
+ var __toESMCache_esm;
13
+ var __toESM = (mod, isNodeMode, target) => {
14
+ var canCache = mod != null && typeof mod === "object";
15
+ if (canCache) {
16
+ var cache = isNodeMode ? __toESMCache_node ??= new WeakMap : __toESMCache_esm ??= new WeakMap;
17
+ var cached = cache.get(mod);
18
+ if (cached)
19
+ return cached;
20
+ }
21
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
22
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
23
+ for (let key of __getOwnPropNames(mod))
24
+ if (!__hasOwnProp.call(to, key))
25
+ __defProp(to, key, {
26
+ get: __accessProp.bind(mod, key),
27
+ enumerable: true
28
+ });
29
+ if (canCache)
30
+ cache.set(mod, to);
31
+ return to;
32
+ };
33
+ var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
34
+ var __returnValue = (v) => v;
35
+ function __exportSetter(name, newValue) {
36
+ this[name] = __returnValue.bind(null, newValue);
37
+ }
38
+ var __export = (target, all) => {
39
+ for (var name in all)
40
+ __defProp(target, name, {
41
+ get: all[name],
42
+ enumerable: true,
43
+ configurable: true,
44
+ set: __exportSetter.bind(all, name)
45
+ });
46
+ };
47
+
48
+ // src/config.ts
49
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
50
+ import { homedir } from "os";
51
+ import { dirname, join } from "path";
52
+ function defaultConfig() {
53
+ return {
54
+ separator: " \xB7 ",
55
+ colors: true,
56
+ segments: [
57
+ "machine",
58
+ "project",
59
+ "commit-age",
60
+ "loc",
61
+ "model-context",
62
+ "context-remaining",
63
+ "cost"
64
+ ]
65
+ };
66
+ }
67
+ function configPath(path) {
68
+ if (path)
69
+ return path;
70
+ if (process.env.STATUSLINE_CONFIG)
71
+ return process.env.STATUSLINE_CONFIG;
72
+ const base = process.env.XDG_CONFIG_HOME || join(homedir(), ".config");
73
+ return join(base, "statusline", "config.json");
74
+ }
75
+ function loadConfig(path = configPath()) {
76
+ if (!existsSync(path))
77
+ return defaultConfig();
78
+ try {
79
+ const raw = JSON.parse(readFileSync(path, "utf8"));
80
+ const fallback = defaultConfig();
81
+ return {
82
+ separator: typeof raw.separator === "string" ? raw.separator : fallback.separator,
83
+ segments: Array.isArray(raw.segments) ? raw.segments.filter((s) => typeof s === "string") : fallback.segments,
84
+ colors: typeof raw.colors === "boolean" ? raw.colors : fallback.colors
85
+ };
86
+ } catch {
87
+ return defaultConfig();
88
+ }
89
+ }
90
+ function saveConfig(cfg, path = configPath()) {
91
+ mkdirSync(dirname(path), { recursive: true });
92
+ writeFileSync(path, JSON.stringify(cfg, null, 2) + `
93
+ `);
94
+ }
95
+
96
+ // src/providers/claude.ts
97
+ function parseClaudeInput(input) {
98
+ const cwd = input?.cwd || input?.workspace?.current_dir || process.cwd();
99
+ return {
100
+ provider: "claude",
101
+ cwd,
102
+ projectDir: input?.workspace?.project_dir || undefined,
103
+ model: input?.model ? { id: input.model.id || undefined, displayName: input.model.display_name || undefined } : undefined,
104
+ cost: input?.cost ? {
105
+ totalCostUsd: numberOr(input.cost.total_cost_usd),
106
+ totalDurationMs: numberOr(input.cost.total_duration_ms),
107
+ totalLinesAdded: numberOr(input.cost.total_lines_added),
108
+ totalLinesRemoved: numberOr(input.cost.total_lines_removed)
109
+ } : undefined,
110
+ transcriptPath: input?.transcript_path || undefined,
111
+ sessionId: input?.session_id || undefined,
112
+ sessionName: input?.session_name || undefined,
113
+ version: input?.version || undefined,
114
+ outputStyle: input?.output_style?.name || undefined,
115
+ effort: input?.effort?.level || undefined,
116
+ thinking: typeof input?.thinking?.enabled === "boolean" ? input.thinking.enabled : undefined,
117
+ rateLimits: input?.rate_limits ? {
118
+ fiveHour: { usedPercentage: numberOr(input.rate_limits.five_hour?.used_percentage) },
119
+ sevenDay: { usedPercentage: numberOr(input.rate_limits.seven_day?.used_percentage) }
120
+ } : undefined
121
+ };
122
+ }
123
+ function numberOr(v) {
124
+ return typeof v === "number" && Number.isFinite(v) ? v : undefined;
125
+ }
126
+
127
+ // src/segments/index.ts
128
+ import { hostname } from "os";
129
+
130
+ // src/format.ts
131
+ function compactAge(seconds) {
132
+ if (seconds < 60)
133
+ return `${Math.floor(seconds)}s`;
134
+ if (seconds < 3600)
135
+ return `${Math.floor(seconds / 60)}m`;
136
+ if (seconds < 86400)
137
+ return `${Math.floor(seconds / 3600)}h`;
138
+ if (seconds < 604800)
139
+ return `${Math.floor(seconds / 86400)}d`;
140
+ if (seconds < 2592000)
141
+ return `${Math.floor(seconds / 604800)}w`;
142
+ if (seconds < 31536000)
143
+ return `${Math.floor(seconds / 2592000)}mo`;
144
+ return `${Math.floor(seconds / 31536000)}y`;
145
+ }
146
+ function trimmed(value) {
147
+ const fixed = value.toFixed(1);
148
+ return fixed.endsWith(".0") ? fixed.slice(0, -2) : fixed;
149
+ }
150
+ function compactNum(n) {
151
+ if (n >= 1e6)
152
+ return `${trimmed(n / 1e6)}m`;
153
+ if (n >= 1e4)
154
+ return `${Math.floor(n / 1000)}k`;
155
+ if (n >= 1000)
156
+ return `${trimmed(n / 1000)}k`;
157
+ return `${n}`;
158
+ }
159
+ function money(n) {
160
+ return `$${n.toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
161
+ }
162
+ function compactDuration(ms) {
163
+ const s = Math.floor(ms / 1000);
164
+ if (s < 60)
165
+ return `${s}s`;
166
+ if (s < 3600) {
167
+ const rem2 = s % 60;
168
+ return rem2 ? `${Math.floor(s / 60)}m${rem2}s` : `${Math.floor(s / 60)}m`;
169
+ }
170
+ const rem = Math.floor(s % 3600 / 60);
171
+ return rem ? `${Math.floor(s / 3600)}h${rem}m` : `${Math.floor(s / 3600)}h`;
172
+ }
173
+
174
+ // src/git.ts
175
+ function run(cwd, args) {
176
+ try {
177
+ const proc = Bun.spawnSync(["git", "-c", "gc.auto=0", ...args], {
178
+ cwd,
179
+ stdout: "pipe",
180
+ stderr: "ignore"
181
+ });
182
+ if (proc.exitCode !== 0)
183
+ return null;
184
+ const out = proc.stdout.toString().trim();
185
+ return out.length ? out : null;
186
+ } catch {
187
+ return null;
188
+ }
189
+ }
190
+ function gitRoot(cwd) {
191
+ return run(cwd, ["rev-parse", "--show-toplevel"]);
192
+ }
193
+ function gitBranch(cwd) {
194
+ return run(cwd, ["branch", "--show-current"]);
195
+ }
196
+ function gitProjectName(cwd) {
197
+ const root = gitRoot(cwd);
198
+ if (!root)
199
+ return null;
200
+ const remote = run(root, ["config", "--get", "remote.origin.url"]);
201
+ if (remote)
202
+ return remote.replace(/\.git$/, "").split("/").pop() || null;
203
+ return root.split("/").pop() || null;
204
+ }
205
+ function lastCommitEpoch(cwd) {
206
+ const out = run(cwd, ["log", "-1", "--format=%ct"]);
207
+ if (!out)
208
+ return null;
209
+ const ts = Number(out);
210
+ return Number.isFinite(ts) ? ts : null;
211
+ }
212
+ function trackedLineCount(cwd) {
213
+ const root = gitRoot(cwd);
214
+ if (!root)
215
+ return null;
216
+ try {
217
+ const proc = Bun.spawnSync(["bash", "-c", "git -c gc.auto=0 ls-files -z | xargs -0 wc -l 2>/dev/null | awk 'END {print $1+0}'"], { cwd: root, stdout: "pipe", stderr: "ignore" });
218
+ if (proc.exitCode !== 0)
219
+ return null;
220
+ const n = Number(proc.stdout.toString().trim());
221
+ return Number.isFinite(n) && n > 0 ? n : null;
222
+ } catch {
223
+ return null;
224
+ }
225
+ }
226
+
227
+ // src/context-window.ts
228
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
229
+ function contextUsage(ctx) {
230
+ const path = ctx.transcriptPath;
231
+ if (!path || !existsSync2(path))
232
+ return null;
233
+ let lines;
234
+ try {
235
+ lines = readFileSync2(path, "utf8").split(`
236
+ `);
237
+ } catch {
238
+ return null;
239
+ }
240
+ for (let i = lines.length - 1;i >= 0; i--) {
241
+ const line = lines[i];
242
+ if (!line || !line.includes('"usage"'))
243
+ continue;
244
+ try {
245
+ const entry = JSON.parse(line);
246
+ const usage = entry?.message?.usage;
247
+ if (!usage || typeof usage.input_tokens !== "number")
248
+ continue;
249
+ const used = usage.input_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
250
+ return {
251
+ used,
252
+ output: usage.output_tokens || 0,
253
+ window: windowSize(ctx)
254
+ };
255
+ } catch {
256
+ continue;
257
+ }
258
+ }
259
+ return null;
260
+ }
261
+ function windowSize(ctx) {
262
+ return ctx.model?.id?.includes("[1m]") ? 1e6 : 200000;
263
+ }
264
+
265
+ // src/accounts.ts
266
+ import { existsSync as existsSync3, readFileSync as readFileSync3, realpathSync } from "fs";
267
+ import { homedir as homedir2 } from "os";
268
+ import { join as join2, resolve, sep } from "path";
269
+ var CONFIG_DIR_ENV = "CLAUDE_CONFIG_DIR";
270
+ var DEFAULT_CONFIG_SUBDIR = ".claude";
271
+ var STORE_PATH_ENV = "ACCOUNTS_STORE_PATH";
272
+ var ACCOUNTS_HOME_ENV = "ACCOUNTS_HOME";
273
+ var DEFAULT_ACCOUNTS_HOME = [".hasna", "accounts"];
274
+ var STORE_FILE = "accounts.json";
275
+ var PROFILES_SUBDIR = "profiles";
276
+ var STATE_FILE_PREFERRED = ".config.json";
277
+ var STATE_FILE_PREFIX = ".claude";
278
+ var STATE_FILE_SUFFIX = ".json";
279
+ var CUSTOM_OAUTH_ENV = "CLAUDE_CODE_CUSTOM_OAUTH_URL";
280
+ var OAUTH_VARIANTS = ["", "-custom-oauth", "-local-oauth", "-staging-oauth"];
281
+ function str(value) {
282
+ return typeof value === "string" && value.trim() ? value.trim() : null;
283
+ }
284
+ function home(env) {
285
+ return str(env.HOME) ?? homedir2();
286
+ }
287
+ function expandHome(path, env) {
288
+ return path === "~" || path.startsWith(`~${sep}`) ? join2(home(env), path.slice(1)) : path;
289
+ }
290
+ function canonical(path, env) {
291
+ const abs = resolve(expandHome(path, env));
292
+ try {
293
+ return realpathSync(abs);
294
+ } catch {
295
+ return abs;
296
+ }
297
+ }
298
+ function readJson(path) {
299
+ try {
300
+ return JSON.parse(readFileSync3(path, "utf8"));
301
+ } catch {
302
+ return null;
303
+ }
304
+ }
305
+ var ACCOUNTS_STORAGE_MODULE = "@hasna/accounts/storage";
306
+ var storagePromise;
307
+ function accountsStorage() {
308
+ storagePromise ??= import(ACCOUNTS_STORAGE_MODULE).then((mod) => typeof mod?.storePath === "function" && typeof mod?.profilesDir === "function" ? mod : null, () => null);
309
+ return storagePromise;
310
+ }
311
+ async function accountsPaths(env) {
312
+ const homeOverride = str(env[ACCOUNTS_HOME_ENV]);
313
+ const storeOverride = str(env[STORE_PATH_ENV]);
314
+ if (!homeOverride && !storeOverride) {
315
+ const storage = await accountsStorage();
316
+ try {
317
+ if (storage)
318
+ return { store: storage.storePath(), profiles: storage.profilesDir() };
319
+ } catch {}
320
+ }
321
+ const base = homeOverride ?? join2(home(env), ...DEFAULT_ACCOUNTS_HOME);
322
+ return {
323
+ store: storeOverride ?? join2(base, STORE_FILE),
324
+ profiles: join2(base, PROFILES_SUBDIR)
325
+ };
326
+ }
327
+ function registryEntry(store, configDir, env, tool) {
328
+ if (!existsSync3(store))
329
+ return null;
330
+ const entries = readJson(store)?.profiles;
331
+ if (!Array.isArray(entries))
332
+ return null;
333
+ let untyped = null;
334
+ let first = null;
335
+ for (const entry of entries) {
336
+ const dir = str(entry?.dir);
337
+ if (!dir || canonical(dir, env) !== configDir)
338
+ continue;
339
+ const entryTool = str(entry?.tool);
340
+ if (tool && entryTool === tool)
341
+ return entry;
342
+ if (!entryTool)
343
+ untyped ??= entry;
344
+ first ??= entry;
345
+ }
346
+ return tool ? untyped : first;
347
+ }
348
+ function layoutProfile(profilesRoot, configDir, env) {
349
+ if (!existsSync3(configDir))
350
+ return null;
351
+ const prefix = canonical(profilesRoot, env) + sep;
352
+ if (!configDir.startsWith(prefix))
353
+ return null;
354
+ const parts = configDir.slice(prefix.length).split(sep);
355
+ if (parts.length !== 2)
356
+ return null;
357
+ const [tool, profile] = parts;
358
+ if (!tool || !profile)
359
+ return null;
360
+ return { configDir, profile, tool, source: "layout" };
361
+ }
362
+ function sessionConfigDir(env = process.env) {
363
+ return canonical(str(env[CONFIG_DIR_ENV]) ?? join2(home(env), DEFAULT_CONFIG_SUBDIR), env);
364
+ }
365
+ function sessionStateFile(env = process.env) {
366
+ const base = canonical(str(env[CONFIG_DIR_ENV]) ?? home(env), env);
367
+ const preferred = join2(base, STATE_FILE_PREFERRED);
368
+ if (existsSync3(preferred))
369
+ return preferred;
370
+ const variants = str(env[CUSTOM_OAUTH_ENV]) ? ["-custom-oauth", ...OAUTH_VARIANTS] : OAUTH_VARIANTS;
371
+ for (const variant of variants) {
372
+ const candidate = join2(base, `${STATE_FILE_PREFIX}${variant}${STATE_FILE_SUFFIX}`);
373
+ if (existsSync3(candidate))
374
+ return candidate;
375
+ }
376
+ return null;
377
+ }
378
+ async function sessionAccount(env = process.env, tool) {
379
+ const configDir = sessionConfigDir(env);
380
+ const unresolved = { configDir, profile: null, tool: null, source: null };
381
+ try {
382
+ const paths = await accountsPaths(env);
383
+ const entry = registryEntry(paths.store, configDir, env, tool);
384
+ const name = str(entry?.name);
385
+ if (name)
386
+ return { configDir, profile: name, tool: str(entry?.tool), source: "registry" };
387
+ return layoutProfile(paths.profiles, configDir, env) ?? unresolved;
388
+ } catch {
389
+ return unresolved;
390
+ }
391
+ }
392
+ async function sessionAccountEmail(env = process.env, tool) {
393
+ try {
394
+ const stateFile = sessionStateFile(env);
395
+ const live = stateFile ? str(readJson(stateFile)?.oauthAccount?.emailAddress) : null;
396
+ if (live)
397
+ return live;
398
+ const paths = await accountsPaths(env);
399
+ return str(registryEntry(paths.store, sessionConfigDir(env), env, tool)?.email);
400
+ } catch {
401
+ return null;
402
+ }
403
+ }
404
+
405
+ // src/segments/index.ts
406
+ function friendlyModel(id) {
407
+ let tag = null;
408
+ let mid = id;
409
+ const tagMatch = mid.match(/\[([^\]]+)\]$/);
410
+ if (tagMatch) {
411
+ tag = `[${tagMatch[1]}]`;
412
+ mid = mid.slice(0, mid.length - tagMatch[0].length);
413
+ }
414
+ mid = mid.replace(/^claude-/, "").replace(/-\d{8}$/, "");
415
+ const tokens = mid.split("-").filter(Boolean);
416
+ const words = tokens.filter((t) => !/^\d+$/.test(t));
417
+ const nums = tokens.filter((t) => /^\d+$/.test(t));
418
+ const name = [words.join(" "), nums.join(".")].filter(Boolean).join(" ");
419
+ return { name: name || mid, tag };
420
+ }
421
+ var dir = (ctx) => ctx.cwd || process.cwd();
422
+ function modelName(ctx) {
423
+ if (ctx.model?.displayName)
424
+ return ctx.model.displayName;
425
+ return ctx.model?.id ? friendlyModel(ctx.model.id).name : null;
426
+ }
427
+ function limitPercent(pct) {
428
+ return typeof pct === "number" ? pct : 0;
429
+ }
430
+ var segments = [
431
+ {
432
+ id: "machine",
433
+ description: "Machine hostname (short)",
434
+ defaultEnabled: true,
435
+ render: () => hostname().split(".")[0] || null
436
+ },
437
+ {
438
+ id: "project",
439
+ description: "Project name with current Git branch",
440
+ defaultEnabled: true,
441
+ render(ctx) {
442
+ const name = gitProjectName(dir(ctx)) || dir(ctx).split("/").pop() || null;
443
+ if (!name)
444
+ return null;
445
+ const branch = gitBranch(dir(ctx));
446
+ return branch ? `${name} (${branch})` : name;
447
+ }
448
+ },
449
+ {
450
+ id: "project-name",
451
+ description: "Project name (omitted when unavailable)",
452
+ defaultEnabled: false,
453
+ render: (ctx) => gitProjectName(dir(ctx)) || dir(ctx).split("/").pop() || null
454
+ },
455
+ {
456
+ id: "git-branch",
457
+ description: "Current Git branch (omitted when unavailable)",
458
+ defaultEnabled: false,
459
+ render: (ctx) => gitBranch(dir(ctx))
460
+ },
461
+ {
462
+ id: "commit-age",
463
+ description: "Time since last commit (compact)",
464
+ defaultEnabled: true,
465
+ render(ctx) {
466
+ const ts = lastCommitEpoch(dir(ctx));
467
+ if (!ts)
468
+ return null;
469
+ return compactAge(Date.now() / 1000 - ts);
470
+ }
471
+ },
472
+ {
473
+ id: "loc",
474
+ description: "Lines of code tracked by Git (compact)",
475
+ defaultEnabled: true,
476
+ render(ctx) {
477
+ const n = trackedLineCount(dir(ctx));
478
+ return n ? compactNum(n) : null;
479
+ }
480
+ },
481
+ {
482
+ id: "current-dir",
483
+ description: "Current working directory (basename)",
484
+ defaultEnabled: false,
485
+ render: (ctx) => dir(ctx).split("/").pop() || null
486
+ },
487
+ {
488
+ id: "model",
489
+ description: "Current model name",
490
+ defaultEnabled: false,
491
+ render(ctx) {
492
+ if (!ctx.model?.id)
493
+ return null;
494
+ return friendlyModel(ctx.model.id).name;
495
+ }
496
+ },
497
+ {
498
+ id: "model-context",
499
+ description: "Model name with context-size tag (e.g. [1m])",
500
+ defaultEnabled: true,
501
+ render(ctx) {
502
+ if (!ctx.model?.id)
503
+ return null;
504
+ const { name, tag } = friendlyModel(ctx.model.id);
505
+ return tag ? `${name} ${tag}` : name;
506
+ }
507
+ },
508
+ {
509
+ id: "model-with-reasoning",
510
+ description: "Model name with its reasoning effort (or thinking, when on)",
511
+ defaultEnabled: false,
512
+ color: "cyan",
513
+ render(ctx) {
514
+ const name = modelName(ctx);
515
+ if (!name)
516
+ return null;
517
+ if (ctx.effort)
518
+ return `${name} (${ctx.effort})`;
519
+ if (ctx.thinking)
520
+ return `${name} (thinking)`;
521
+ return name;
522
+ }
523
+ },
524
+ {
525
+ id: "auth-profile",
526
+ description: "Account profile this session runs as (omitted when unmanaged)",
527
+ defaultEnabled: false,
528
+ color: "magenta",
529
+ async render(ctx) {
530
+ const account = await sessionAccount(process.env, ctx.provider);
531
+ return account.profile ?? await sessionAccountEmail(process.env, ctx.provider);
532
+ }
533
+ },
534
+ {
535
+ id: "auth-email",
536
+ description: "Account email this session is logged in as (omitted when unknown)",
537
+ defaultEnabled: false,
538
+ color: "magenta",
539
+ render: (ctx) => sessionAccountEmail(process.env, ctx.provider)
540
+ },
541
+ {
542
+ id: "five-hour-limit",
543
+ description: "Percentage of the 5-hour rate limit used (omitted when unreported)",
544
+ defaultEnabled: false,
545
+ color: (ctx) => limitPercent(ctx.rateLimits?.fiveHour?.usedPercentage) >= 80 ? "red" : "yellow",
546
+ render(ctx) {
547
+ const pct = ctx.rateLimits?.fiveHour?.usedPercentage;
548
+ return typeof pct === "number" ? `5h:${Math.round(pct)}%` : null;
549
+ }
550
+ },
551
+ {
552
+ id: "seven-day-limit",
553
+ description: "Percentage of the 7-day rate limit used (omitted when unreported)",
554
+ defaultEnabled: false,
555
+ color: (ctx) => limitPercent(ctx.rateLimits?.sevenDay?.usedPercentage) >= 80 ? "red" : "yellow",
556
+ render(ctx) {
557
+ const pct = ctx.rateLimits?.sevenDay?.usedPercentage;
558
+ return typeof pct === "number" ? `7d:${Math.round(pct)}%` : null;
559
+ }
560
+ },
561
+ {
562
+ id: "thread-title",
563
+ description: "Thread title set with /rename (omitted until named)",
564
+ defaultEnabled: false,
565
+ color: "blue",
566
+ render: (ctx) => ctx.sessionName || null
567
+ },
568
+ {
569
+ id: "context-used",
570
+ description: "Percentage of context window used (omitted when unknown)",
571
+ defaultEnabled: false,
572
+ render(ctx) {
573
+ const u = contextUsage(ctx);
574
+ if (!u)
575
+ return null;
576
+ return `${Math.min(100, Math.round(u.used / u.window * 100))}%`;
577
+ }
578
+ },
579
+ {
580
+ id: "context-remaining",
581
+ description: "Percentage of context window remaining (omitted when unknown)",
582
+ defaultEnabled: true,
583
+ render(ctx) {
584
+ const u = contextUsage(ctx);
585
+ if (!u)
586
+ return null;
587
+ return `${Math.max(0, 100 - Math.round(u.used / u.window * 100))}% left`;
588
+ }
589
+ },
590
+ {
591
+ id: "used-tokens",
592
+ description: "Total tokens in the context window (omitted when unknown)",
593
+ defaultEnabled: false,
594
+ render(ctx) {
595
+ const u = contextUsage(ctx);
596
+ if (!u)
597
+ return null;
598
+ return `${compactNum(u.used + u.output)} tok`;
599
+ }
600
+ },
601
+ {
602
+ id: "cost",
603
+ description: "Session cost in USD (omitted when zero)",
604
+ defaultEnabled: true,
605
+ render(ctx) {
606
+ const c = ctx.cost?.totalCostUsd;
607
+ return c ? money(c) : null;
608
+ }
609
+ },
610
+ {
611
+ id: "duration",
612
+ description: "Session wall-clock duration",
613
+ defaultEnabled: false,
614
+ render(ctx) {
615
+ const ms = ctx.cost?.totalDurationMs;
616
+ return ms ? compactDuration(ms) : null;
617
+ }
618
+ },
619
+ {
620
+ id: "lines-changed",
621
+ description: "Lines added/removed this session (omitted when zero)",
622
+ defaultEnabled: false,
623
+ render(ctx) {
624
+ const a = ctx.cost?.totalLinesAdded || 0;
625
+ const r = ctx.cost?.totalLinesRemoved || 0;
626
+ if (!a && !r)
627
+ return null;
628
+ return `+${a}/-${r}`;
629
+ }
630
+ },
631
+ {
632
+ id: "output-style",
633
+ description: "Active output style (omitted when default)",
634
+ defaultEnabled: false,
635
+ render(ctx) {
636
+ const s = ctx.outputStyle;
637
+ return s && s !== "default" ? s : null;
638
+ }
639
+ },
640
+ {
641
+ id: "agent-version",
642
+ description: "Host agent version (e.g. Claude Code version)",
643
+ defaultEnabled: false,
644
+ render: (ctx) => ctx.version ? `v${ctx.version}` : null
645
+ },
646
+ {
647
+ id: "session-id",
648
+ description: "Session identifier (short)",
649
+ defaultEnabled: false,
650
+ render: (ctx) => ctx.sessionId ? ctx.sessionId.split("-")[0] : null
651
+ }
652
+ ];
653
+ function getSegment(id) {
654
+ return segments.find((s) => s.id === id);
655
+ }
656
+
657
+ // src/render.ts
658
+ var ANSI = {
659
+ cyan: "\x1B[36m",
660
+ magenta: "\x1B[35m",
661
+ blue: "\x1B[34m",
662
+ green: "\x1B[32m",
663
+ yellow: "\x1B[33m",
664
+ red: "\x1B[31m",
665
+ bold: "\x1B[1m",
666
+ dim: "\x1B[2m"
667
+ };
668
+ var RESET = "\x1B[0m";
669
+ function colorsEnabled(cfg, env = process.env) {
670
+ if (env.NO_COLOR)
671
+ return false;
672
+ return cfg.colors !== false;
673
+ }
674
+ function paint(segment, text, ctx) {
675
+ const color = typeof segment.color === "function" ? segment.color(ctx) : segment.color;
676
+ const code = color ? ANSI[color] : undefined;
677
+ return code ? `${code}${text}${RESET}` : text;
678
+ }
679
+ async function renderLine(ctx, cfg) {
680
+ const colorize = colorsEnabled(cfg);
681
+ const parts = [];
682
+ for (const id of cfg.segments) {
683
+ const segment = getSegment(id);
684
+ if (!segment)
685
+ continue;
686
+ try {
687
+ const out = await segment.render(ctx);
688
+ if (out)
689
+ parts.push(colorize ? paint(segment, out, ctx) : out);
690
+ } catch {}
691
+ }
692
+ return parts.join(cfg.separator);
693
+ }
694
+ // package.json
695
+ var package_default = {
696
+ name: "@hasna/statusline",
697
+ version: "0.0.3",
698
+ description: "Tiny composable statusline CLI for AI coding agents \u2014 pre-built toggleable segments for Claude Code, with a provider model for others",
699
+ license: "Apache-2.0",
700
+ type: "module",
701
+ main: "./dist/index.js",
702
+ types: "./dist/index.d.ts",
703
+ bin: {
704
+ statusline: "./dist/cli.js",
705
+ "statusline-mcp": "./dist/mcp/index.js"
706
+ },
707
+ exports: {
708
+ ".": {
709
+ types: "./dist/index.d.ts",
710
+ import: "./dist/index.js"
711
+ },
712
+ "./cli": {
713
+ types: "./dist/cli.d.ts",
714
+ import: "./dist/cli.js"
715
+ },
716
+ "./mcp": {
717
+ types: "./dist/mcp/index.d.ts",
718
+ import: "./dist/mcp/index.js"
719
+ }
720
+ },
721
+ files: [
722
+ "dist",
723
+ "LICENSE",
724
+ "README.md"
725
+ ],
726
+ scripts: {
727
+ build: "rm -rf dist && bun build src/index.ts src/cli.ts src/mcp/index.ts --outdir dist --target bun && tsc -p tsconfig.build.json && chmod +x dist/cli.js dist/mcp/index.js",
728
+ typecheck: "tsc --noEmit",
729
+ test: "bun test",
730
+ check: "bun run typecheck && bun test && bun run build",
731
+ prepack: "bun run build",
732
+ prepublishOnly: "bun run typecheck && bun test && bun run build"
733
+ },
734
+ repository: {
735
+ type: "git",
736
+ url: "git+https://github.com/hasna/statusline.git"
737
+ },
738
+ homepage: "https://github.com/hasna/statusline#readme",
739
+ bugs: {
740
+ url: "https://github.com/hasna/statusline/issues"
741
+ },
742
+ keywords: [
743
+ "statusline",
744
+ "status-bar",
745
+ "claude-code",
746
+ "cli",
747
+ "bun"
748
+ ],
749
+ publishConfig: {
750
+ registry: "https://registry.npmjs.org",
751
+ access: "public"
752
+ },
753
+ engines: {
754
+ bun: ">=1.0.0"
755
+ },
756
+ dependencies: {
757
+ "@modelcontextprotocol/sdk": "^1.12.1",
758
+ zod: "^3.25.76"
759
+ },
760
+ devDependencies: {
761
+ "@types/bun": "^1.2.4",
762
+ typescript: "^5.7.3"
763
+ }
764
+ };
765
+ // src/install.ts
766
+ import { copyFileSync, existsSync as existsSync4, mkdirSync as mkdirSync2, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
767
+ import { homedir as homedir3 } from "os";
768
+ import { dirname as dirname2, join as join3, resolve as resolve2 } from "path";
769
+ function defaultSettingsPath(env) {
770
+ return join3(env.HOME || homedir3(), ".claude", "settings.json");
771
+ }
772
+ function claudeSettingsPath(env = process.env) {
773
+ const home2 = env.HOME || homedir3();
774
+ const raw = env.CLAUDE_CONFIG_DIR?.trim();
775
+ if (!raw)
776
+ return defaultSettingsPath(env);
777
+ const configDir = raw === "~" || raw.startsWith("~/") ? join3(home2, raw.slice(1)) : raw;
778
+ return join3(configDir, "settings.json");
779
+ }
780
+ function installClaude(settingsPath, env = process.env) {
781
+ const target = settingsPath ?? claudeSettingsPath(env);
782
+ const binary = Bun.which("statusline") || "statusline";
783
+ let settings = {};
784
+ if (existsSync4(target)) {
785
+ try {
786
+ settings = JSON.parse(readFileSync4(target, "utf8"));
787
+ } catch (error) {
788
+ throw new Error(`${target} is not valid JSON \u2014 fix or move it first`);
789
+ }
790
+ copyFileSync(target, `${target}.bak-statusline`);
791
+ }
792
+ settings.statusLine = { ...settings.statusLine, type: "command", command: `${binary} render` };
793
+ const configDir = dirname2(target);
794
+ if (!existsSync4(configDir)) {
795
+ if (resolve2(target) !== resolve2(defaultSettingsPath(env))) {
796
+ throw new Error(`config dir ${configDir} does not exist \u2014 create the profile first`);
797
+ }
798
+ mkdirSync2(configDir, { recursive: true });
799
+ }
800
+ writeFileSync2(target, JSON.stringify(settings, null, 2) + `
801
+ `);
802
+ return target;
803
+ }
804
+
805
+ // src/index.ts
806
+ var statuslineVersion = package_default.version;
807
+ function listSegments(config = loadConfig()) {
808
+ const enabled = new Set(config.segments);
809
+ return segments.map((segment) => ({
810
+ id: segment.id,
811
+ description: segment.description,
812
+ defaultEnabled: segment.defaultEnabled,
813
+ enabled: enabled.has(segment.id)
814
+ }));
815
+ }
816
+ function requireKnownSegments(ids) {
817
+ const unknown = ids.filter((id) => !getSegment(id));
818
+ if (unknown.length) {
819
+ throw new Error(`unknown segment(s): ${unknown.join(", ")}`);
820
+ }
821
+ return ids;
822
+ }
823
+ function enableSegments(ids, config = loadConfig()) {
824
+ requireKnownSegments(ids);
825
+ const next = { ...config, segments: [...config.segments] };
826
+ const changed = [];
827
+ for (const id of ids) {
828
+ if (!next.segments.includes(id)) {
829
+ next.segments.push(id);
830
+ changed.push(id);
831
+ }
832
+ }
833
+ return { config: next, changed };
834
+ }
835
+ function disableSegments(ids, config = loadConfig()) {
836
+ requireKnownSegments(ids);
837
+ const nextSegments = config.segments.filter((id) => !ids.includes(id));
838
+ return {
839
+ config: { ...config, segments: nextSegments },
840
+ changed: config.segments.filter((id) => ids.includes(id))
841
+ };
842
+ }
843
+ function orderSegments(ids, config = loadConfig()) {
844
+ requireKnownSegments(ids);
845
+ if (ids.length === 0)
846
+ throw new Error("at least one segment id is required");
847
+ return { config: { ...config, segments: [...ids] }, changed: [...ids] };
848
+ }
849
+ function setSeparator(separator, config = loadConfig()) {
850
+ if (!separator)
851
+ throw new Error("separator must be a non-empty string");
852
+ return { config: { ...config, separator }, changed: ["separator"] };
853
+ }
854
+ function setColors(colors, config = loadConfig()) {
855
+ return { config: { ...config, colors }, changed: ["colors"] };
856
+ }
857
+ function resetStatuslineConfig() {
858
+ return defaultConfig();
859
+ }
860
+ async function renderStatusline(input = {}, config = loadConfig()) {
861
+ return renderLine(parseClaudeInput(input), config);
862
+ }
863
+ function sampleClaudeInput(cwd = process.cwd()) {
864
+ return {
865
+ cwd,
866
+ model: { id: "claude-fable-5[1m]", display_name: "Fable" },
867
+ cost: { total_cost_usd: 12.34, total_duration_ms: 5400000, total_lines_added: 42, total_lines_removed: 7 },
868
+ version: statuslineVersion,
869
+ session_id: "preview0-0000-0000-0000-000000000000",
870
+ session_name: "preview thread",
871
+ effort: { level: "high" },
872
+ thinking: { enabled: true },
873
+ rate_limits: { five_hour: { used_percentage: 42 } }
874
+ };
875
+ }
876
+ async function previewStatusline(config = loadConfig(), cwd = process.cwd()) {
877
+ return renderStatusline(sampleClaudeInput(cwd), config);
878
+ }
879
+ function saveUpdatedConfig(result, path = configPath()) {
880
+ saveConfig(result.config, path);
881
+ return result.config;
882
+ }
883
+
884
+ // src/cli.ts
885
+ var DEFAULT_LIST_LIMIT = 12;
886
+ async function readStdinJson() {
887
+ try {
888
+ const text = await Bun.stdin.text();
889
+ return text.trim() ? JSON.parse(text) : {};
890
+ } catch {
891
+ return {};
892
+ }
893
+ }
894
+ function printError(error) {
895
+ console.error(error instanceof Error ? error.message : String(error));
896
+ process.exitCode = 1;
897
+ }
898
+ function printSegmentError(error) {
899
+ const message = error instanceof Error ? error.message : String(error);
900
+ if (message.startsWith("unknown segment")) {
901
+ console.error(`${message}
902
+ run \`statusline list\` to see available segments`);
903
+ } else {
904
+ console.error(message);
905
+ }
906
+ process.exitCode = 1;
907
+ }
908
+ function help() {
909
+ return `statusline ${statuslineVersion} \u2014 composable statusline for AI coding agents
910
+
911
+ usage:
912
+ statusline render read agent JSON on stdin, print the statusline (default)
913
+ statusline list compact segment list (default limit: ${DEFAULT_LIST_LIMIT})
914
+ statusline list --verbose include descriptions
915
+ statusline list --all show all rows
916
+ statusline list --json machine-readable segment list
917
+ statusline list --limit N cap rows explicitly
918
+ statusline list --enabled show only enabled segments
919
+ statusline list --disabled show only disabled segments
920
+ statusline list --search text filter by id or description
921
+ statusline search <text> alias for list --search
922
+ statusline show <id> show one segment in detail
923
+ statusline show <id> --json machine-readable segment detail
924
+ statusline inspect <id> alias for show
925
+ statusline enable <id...> enable segments (appended to the end)
926
+ statusline disable <id...> disable segments
927
+ statusline order <id...> set the exact segment order (also defines enabled set)
928
+ statusline separator <str> set the separator (default " \xB7 ")
929
+ statusline colors <on|off> toggle segment colours (default on)
930
+ statusline preview render with a sample payload from the current directory
931
+ statusline reset restore default config
932
+ statusline install [claude] wire into Claude Code (~/.claude/settings.json)
933
+ statusline version print version
934
+
935
+ config: ${configPath()}`;
936
+ }
937
+ function fail(message) {
938
+ console.error(message);
939
+ process.exit(1);
940
+ }
941
+ function printListHelp() {
942
+ console.log(`usage: statusline list [options] [search text]
943
+
944
+ options:
945
+ --all show all matching rows
946
+ --enabled show only enabled segments
947
+ --disabled show only disabled segments
948
+ --json, -j output structured JSON
949
+ --limit <n> cap rows
950
+ --search, -s <q> filter by id or description
951
+ --verbose include descriptions
952
+ --help, -h show this help`);
953
+ process.exit(0);
954
+ }
955
+ function printSearchHelp() {
956
+ console.log(`usage: statusline search <text> [options]
957
+
958
+ options:
959
+ --all show all matching rows
960
+ --enabled show only enabled segments
961
+ --disabled show only disabled segments
962
+ --json, -j output structured JSON
963
+ --limit <n> cap rows
964
+ --verbose include descriptions
965
+ --help, -h show this help`);
966
+ process.exit(0);
967
+ }
968
+ function printShowHelp() {
969
+ console.log(`usage: statusline show <segment> [options]
970
+
971
+ options:
972
+ --json, -j output structured JSON
973
+ --help, -h show this help`);
974
+ process.exit(0);
975
+ }
976
+ function readFlagValue(raw, index, flag) {
977
+ const value = raw[index + 1];
978
+ if (!value || value.startsWith("-"))
979
+ fail(`missing value for ${flag}`);
980
+ return value;
981
+ }
982
+ function parsePositiveInt(value, flag) {
983
+ if (!value)
984
+ fail(`missing value for ${flag}`);
985
+ const n = Number(value);
986
+ if (!Number.isInteger(n) || n < 1)
987
+ fail(`${flag} must be a positive integer`);
988
+ return n;
989
+ }
990
+ function parseListArgs(raw) {
991
+ const opts = {
992
+ all: false,
993
+ disabledOnly: false,
994
+ enabledOnly: false,
995
+ json: false,
996
+ limit: null,
997
+ query: null,
998
+ verbose: false
999
+ };
1000
+ const queryParts = [];
1001
+ for (let i = 0;i < raw.length; i++) {
1002
+ const arg = raw[i];
1003
+ switch (arg) {
1004
+ case "--all":
1005
+ opts.all = true;
1006
+ break;
1007
+ case "--disabled":
1008
+ opts.disabledOnly = true;
1009
+ break;
1010
+ case "--enabled":
1011
+ opts.enabledOnly = true;
1012
+ break;
1013
+ case "--json":
1014
+ case "-j":
1015
+ opts.json = true;
1016
+ break;
1017
+ case "--limit":
1018
+ opts.limit = parsePositiveInt(readFlagValue(raw, i, "--limit"), "--limit");
1019
+ i++;
1020
+ break;
1021
+ case "--search":
1022
+ case "-s":
1023
+ queryParts.push(readFlagValue(raw, i, arg));
1024
+ i++;
1025
+ break;
1026
+ case "--verbose":
1027
+ opts.verbose = true;
1028
+ break;
1029
+ case "--help":
1030
+ case "-h":
1031
+ printListHelp();
1032
+ default:
1033
+ if (arg.startsWith("-"))
1034
+ fail(`unknown option for list: ${arg}`);
1035
+ queryParts.push(arg);
1036
+ }
1037
+ }
1038
+ if (opts.enabledOnly && opts.disabledOnly)
1039
+ fail("use only one of --enabled or --disabled");
1040
+ if (opts.all && opts.limit !== null)
1041
+ fail("use only one of --all or --limit");
1042
+ opts.query = queryParts.length ? queryParts.join(" ").toLowerCase() : null;
1043
+ return opts;
1044
+ }
1045
+ function parseShowArgs(raw) {
1046
+ let id = null;
1047
+ let json = false;
1048
+ for (const arg of raw) {
1049
+ if (arg === "--json" || arg === "-j") {
1050
+ json = true;
1051
+ } else if (arg === "--help" || arg === "-h") {
1052
+ printShowHelp();
1053
+ } else if (arg.startsWith("-")) {
1054
+ fail(`unknown option for show: ${arg}`);
1055
+ } else if (!id) {
1056
+ id = arg;
1057
+ } else {
1058
+ fail("usage: statusline show <segment> [--json]");
1059
+ }
1060
+ }
1061
+ return { id, json };
1062
+ }
1063
+ function searchArgsForList(raw) {
1064
+ const queryParts = [];
1065
+ const forwarded = [];
1066
+ for (let i = 0;i < raw.length; i++) {
1067
+ const arg = raw[i];
1068
+ switch (arg) {
1069
+ case "--all":
1070
+ case "--disabled":
1071
+ case "--enabled":
1072
+ case "--json":
1073
+ case "-j":
1074
+ case "--verbose":
1075
+ forwarded.push(arg);
1076
+ break;
1077
+ case "--help":
1078
+ case "-h":
1079
+ printSearchHelp();
1080
+ case "--limit":
1081
+ forwarded.push(arg, readFlagValue(raw, i, "--limit"));
1082
+ i++;
1083
+ break;
1084
+ default:
1085
+ if (arg.startsWith("-"))
1086
+ fail(`unknown option for search: ${arg}`);
1087
+ queryParts.push(arg);
1088
+ }
1089
+ }
1090
+ if (!queryParts.length)
1091
+ fail("usage: statusline search <text> [--json] [--verbose] [--limit N]");
1092
+ return ["--search", queryParts.join(" "), ...forwarded];
1093
+ }
1094
+ function filteredSegments(all, opts) {
1095
+ return all.filter((s) => {
1096
+ if (opts.enabledOnly && !s.enabled)
1097
+ return false;
1098
+ if (opts.disabledOnly && s.enabled)
1099
+ return false;
1100
+ if (!opts.query)
1101
+ return true;
1102
+ return `${s.id} ${s.description}`.toLowerCase().includes(opts.query);
1103
+ });
1104
+ }
1105
+ function prioritizeEnabled(rows) {
1106
+ return rows.map((segment, index) => ({ segment, index })).sort((a, b) => Number(b.segment.enabled) - Number(a.segment.enabled) || a.index - b.index).map((entry) => entry.segment);
1107
+ }
1108
+ function truncate(value, width) {
1109
+ return value.length > width ? `${value.slice(0, width - 1)}...` : value;
1110
+ }
1111
+ function printList(raw) {
1112
+ const opts = parseListArgs(raw);
1113
+ const all = listSegments(loadConfig());
1114
+ const allRows = filteredSegments(all, opts);
1115
+ const displayRows = opts.json ? allRows : prioritizeEnabled(allRows);
1116
+ const showAll = opts.all || opts.limit === null && opts.json;
1117
+ const limit = opts.limit ?? (showAll ? allRows.length : DEFAULT_LIST_LIMIT);
1118
+ const rows = displayRows.slice(0, limit);
1119
+ const enabledCount = all.filter((s) => s.enabled).length;
1120
+ const total = all.length;
1121
+ const limited = rows.length < allRows.length;
1122
+ if (opts.json) {
1123
+ console.log(JSON.stringify({
1124
+ total,
1125
+ matching: allRows.length,
1126
+ showing: rows.length,
1127
+ limited,
1128
+ segments: rows,
1129
+ next: limited ? { all: true, limit: allRows.length } : null
1130
+ }, null, 2));
1131
+ return;
1132
+ }
1133
+ const filterLabel = opts.query ? `, query "${opts.query}"` : "";
1134
+ console.log(`Segments: ${enabledCount} enabled / ${total} total (showing ${rows.length} of ${allRows.length}${filterLabel})`);
1135
+ if (!rows.length) {
1136
+ console.log("No matching segments.");
1137
+ console.log("Try `statusline list --all` or `statusline list --search <text>`.");
1138
+ return;
1139
+ }
1140
+ const idWidth = Math.max(2, ...rows.map((s) => s.id.length));
1141
+ if (opts.verbose) {
1142
+ console.log(`state default ${"id".padEnd(idWidth)} description`);
1143
+ for (const s of rows) {
1144
+ const state = s.enabled ? "on " : "off";
1145
+ const def = s.defaultEnabled ? "yes" : "no ";
1146
+ console.log(`${state.padEnd(5)} ${def.padEnd(7)} ${s.id.padEnd(idWidth)} ${truncate(s.description, 72)}`);
1147
+ }
1148
+ } else {
1149
+ console.log(`state default ${"id".padEnd(idWidth)}`);
1150
+ for (const s of rows) {
1151
+ const state = s.enabled ? "on " : "off";
1152
+ const def = s.defaultEnabled ? "yes" : "no ";
1153
+ console.log(`${state.padEnd(5)} ${def.padEnd(7)} ${s.id.padEnd(idWidth)}`);
1154
+ }
1155
+ }
1156
+ const hints = ["`statusline show <id>` for details", "`--verbose` for descriptions", "`--json` for machines"];
1157
+ if (limited)
1158
+ hints.unshift("`--all` for all matching rows");
1159
+ console.log(`Hint: use ${hints.join(", ")}.`);
1160
+ }
1161
+ function printShow(raw) {
1162
+ const { id, json } = parseShowArgs(raw);
1163
+ if (!id)
1164
+ fail("usage: statusline show <segment> [--json]");
1165
+ const info = listSegments(loadConfig()).find((s) => s.id === id);
1166
+ if (!info) {
1167
+ console.error(`unknown segment: ${id}`);
1168
+ console.error("run `statusline list --all` to see available segments");
1169
+ process.exit(1);
1170
+ }
1171
+ if (json) {
1172
+ console.log(JSON.stringify(info, null, 2));
1173
+ return;
1174
+ }
1175
+ console.log(`Segment: ${info.id}`);
1176
+ console.log(`State: ${info.enabled ? "enabled" : "disabled"}`);
1177
+ console.log(`Default: ${info.defaultEnabled ? "enabled" : "disabled"}`);
1178
+ console.log(`Description: ${info.description}`);
1179
+ console.log(`Commands: statusline ${info.enabled ? "disable" : "enable"} ${info.id} | statusline list --search ${info.id}`);
1180
+ }
1181
+ async function runCli(argv = process.argv.slice(2)) {
1182
+ const [command = "render", ...args] = argv;
1183
+ try {
1184
+ switch (command) {
1185
+ case "render": {
1186
+ console.log(await renderStatusline(await readStdinJson(), loadConfig()));
1187
+ return;
1188
+ }
1189
+ case "list": {
1190
+ printList(args);
1191
+ return;
1192
+ }
1193
+ case "search": {
1194
+ printList(searchArgsForList(args));
1195
+ return;
1196
+ }
1197
+ case "show":
1198
+ case "inspect": {
1199
+ printShow(args);
1200
+ return;
1201
+ }
1202
+ case "enable": {
1203
+ let result;
1204
+ try {
1205
+ result = enableSegments(args, loadConfig());
1206
+ } catch (error) {
1207
+ printSegmentError(error);
1208
+ return;
1209
+ }
1210
+ saveConfig(result.config);
1211
+ console.log(`enabled: ${args.join(", ")}
1212
+ order: ${result.config.segments.join(", ")}`);
1213
+ return;
1214
+ }
1215
+ case "disable": {
1216
+ let result;
1217
+ try {
1218
+ result = disableSegments(args, loadConfig());
1219
+ } catch (error) {
1220
+ printSegmentError(error);
1221
+ return;
1222
+ }
1223
+ saveConfig(result.config);
1224
+ console.log(`disabled: ${args.join(", ")}
1225
+ order: ${result.config.segments.join(", ") || "(none)"}`);
1226
+ return;
1227
+ }
1228
+ case "order": {
1229
+ if (!args.length)
1230
+ throw new Error("usage: statusline order <segment> [segment...]");
1231
+ let result;
1232
+ try {
1233
+ result = orderSegments(args, loadConfig());
1234
+ } catch (error) {
1235
+ printSegmentError(error);
1236
+ return;
1237
+ }
1238
+ saveConfig(result.config);
1239
+ console.log(`order: ${result.config.segments.join(", ")}`);
1240
+ return;
1241
+ }
1242
+ case "separator": {
1243
+ if (!args.length)
1244
+ throw new Error('usage: statusline separator " \xB7 "');
1245
+ const result = setSeparator(args.join(" "), loadConfig());
1246
+ saveConfig(result.config);
1247
+ console.log(`separator: "${result.config.separator}"`);
1248
+ return;
1249
+ }
1250
+ case "colors": {
1251
+ const value = args[0];
1252
+ if (value !== "on" && value !== "off")
1253
+ throw new Error("usage: statusline colors <on|off>");
1254
+ const result = setColors(value === "on", loadConfig());
1255
+ saveConfig(result.config);
1256
+ console.log(`colors: ${value}`);
1257
+ return;
1258
+ }
1259
+ case "reset": {
1260
+ saveConfig(defaultConfig());
1261
+ console.log(`reset to defaults (${configPath()})`);
1262
+ return;
1263
+ }
1264
+ case "preview": {
1265
+ console.log(await previewStatusline(loadConfig(), process.cwd()));
1266
+ return;
1267
+ }
1268
+ case "install": {
1269
+ const target = args[0] || "claude";
1270
+ if (target !== "claude") {
1271
+ throw new Error(`unsupported target "${target}" \u2014 only "claude" is supported today (OpenCode has no statusline hook yet)`);
1272
+ }
1273
+ const path = installClaude();
1274
+ console.log(`installed into ${path} \u2014 Claude Code picks it up on the next status refresh`);
1275
+ return;
1276
+ }
1277
+ case "--version":
1278
+ case "-v":
1279
+ case "version": {
1280
+ console.log(statuslineVersion);
1281
+ return;
1282
+ }
1283
+ case "help":
1284
+ case "--help":
1285
+ case "-h": {
1286
+ console.log(help());
1287
+ return;
1288
+ }
1289
+ default: {
1290
+ console.log(help());
1291
+ process.exitCode = 1;
1292
+ }
1293
+ }
1294
+ } catch (error) {
1295
+ printError(error);
1296
+ }
1297
+ }
1298
+ if (import.meta.main) {
1299
+ await runCli();
1300
+ }
1301
+ export {
1302
+ runCli
1303
+ };