@nutthead/cc-statusline 0.1.3 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,33 +1,22 @@
1
1
  # Claude Code Status Line
2
2
 
3
- To install dependencies:
3
+ ## Install
4
4
 
5
5
  ```bash
6
- bun install
6
+ bunx @nutthead/cc-statusline install
7
7
  ```
8
8
 
9
- To build:
9
+ Use `--overwrite` to replace an existing installation.
10
10
 
11
- ```bash
12
- bun run build:binary
13
- ```
14
-
15
- To copy to `~/.claude`:
16
-
17
- ```bash
18
- bun run install:binary
19
- ```
20
-
21
- ## Configure your Claude Code's statusline
11
+ ## Configure
22
12
 
23
- Edit your `~/.claude/settings.json` file to include:
13
+ Add to `~/.claude/settings.json`:
24
14
 
25
15
  ```json
26
16
  {
27
17
  "statusLine": {
28
18
  "type": "command",
29
- "command": "/path/to/.claude/statusline",
30
- "padding": 0
19
+ "command": "~/.claude/statusline"
31
20
  }
32
21
  }
33
22
  ```
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { execSync } from "node:child_process";
5
+ import { existsSync, mkdirSync, copyFileSync, unlinkSync } from "node:fs";
6
+ import { homedir } from "node:os";
7
+ import { join } from "node:path";
8
+ var BINARY_NAME = "statusline";
9
+ var CLAUDE_DIR = join(homedir(), ".claude");
10
+ var TARGET_PATH = join(CLAUDE_DIR, BINARY_NAME);
11
+ function build() {
12
+ console.log("Building statusline binary...");
13
+ execSync("mkdir -p target && bun build --compile ./index.ts --outfile target/statusline", {
14
+ stdio: "inherit"
15
+ });
16
+ console.log("Build complete.");
17
+ }
18
+ function install(overwrite) {
19
+ build();
20
+ if (!existsSync(CLAUDE_DIR)) {
21
+ mkdirSync(CLAUDE_DIR, { recursive: true });
22
+ }
23
+ if (existsSync(TARGET_PATH)) {
24
+ if (!overwrite) {
25
+ console.error(`Error: ${TARGET_PATH} already exists.`);
26
+ console.error("Use --overwrite to replace the existing file.");
27
+ process.exit(1);
28
+ }
29
+ console.log(`Overwriting existing file at ${TARGET_PATH}...`);
30
+ unlinkSync(TARGET_PATH);
31
+ }
32
+ const sourcePath = join(process.cwd(), "target", BINARY_NAME);
33
+ copyFileSync(sourcePath, TARGET_PATH);
34
+ console.log(`Installed statusline to ${TARGET_PATH}`);
35
+ }
36
+ function printUsage() {
37
+ console.log(`Usage: cc-statusline <command> [options]
38
+
39
+ Commands:
40
+ install Build and install statusline to ~/.claude/
41
+
42
+ Options:
43
+ --overwrite Overwrite existing file if it exists
44
+ --help, -h Show this help message`);
45
+ }
46
+ function main() {
47
+ const args = process.argv.slice(2);
48
+ const command = args.find((arg) => !arg.startsWith("-"));
49
+ const flags = new Set(args.filter((arg) => arg.startsWith("-")));
50
+ if (flags.has("--help") || flags.has("-h") || args.length === 0) {
51
+ printUsage();
52
+ process.exit(0);
53
+ }
54
+ switch (command) {
55
+ case "install":
56
+ install(flags.has("--overwrite"));
57
+ break;
58
+ default:
59
+ console.error(`Unknown command: ${command}`);
60
+ printUsage();
61
+ process.exit(1);
62
+ }
63
+ }
64
+ main();
package/index.ts CHANGED
@@ -6,31 +6,37 @@ import {
6
6
  currentDirStatus,
7
7
  currentGitStatus,
8
8
  currentModelStatus,
9
+ currentSessionId,
9
10
  } from "./src/utils";
10
11
 
11
12
  import c from "ansi-colors";
12
13
 
13
14
  await configure(logtapeConfig);
14
15
 
16
+ let statusLine = null;
15
17
  const input = await Bun.stdin.stream().json();
16
- log.debug("stdin: {input}", input);
18
+ log.debug("input: {input}", input);
17
19
 
18
- const result = statusSchema.safeParse(input);
20
+ if (input) {
21
+ const result = statusSchema.safeParse(input);
19
22
 
20
- let statusLine = null;
21
- if (result.success) {
22
- const status = result.data;
23
- const dirStatus = c.blue(currentDirStatus(status));
24
- const gitStatus = c.green(await currentGitStatus());
25
- const modelStatus = c.magenta(currentModelStatus(status));
26
- const separator = c.bold.gray("⋮");
23
+ if (result.success) {
24
+ const status = result.data;
25
+ const dirStatus = c.blue(currentDirStatus(status));
26
+ const gitStatus = c.green(await currentGitStatus());
27
+ const modelStatus = c.magenta(currentModelStatus(status));
28
+ const sessionId = c.blue(currentSessionId(status));
29
+ const separator = c.bold.gray("⋮");
27
30
 
28
- statusLine = `${dirStatus} ${separator} ${gitStatus} ${separator} ${modelStatus}`;
31
+ statusLine = `${dirStatus} ${separator} ${gitStatus}\n${modelStatus} ${separator} ${sessionId}`;
32
+ } else {
33
+ log.error("Failed to parse input: {error}", {
34
+ error: JSON.stringify(z.treeifyError(result.error)),
35
+ });
36
+ statusLine = `[malformed status]`;
37
+ }
29
38
  } else {
30
- log.error("Failed to parse input: {error}", {
31
- error: JSON.stringify(z.treeifyError(result.error)),
32
- });
33
- statusLine = `[]`;
39
+ statusLine = `[no status]`;
34
40
  }
35
41
 
36
42
  console.log(statusLine);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nutthead/cc-statusline",
3
3
  "description": "Status Line for Claude Code",
4
- "version": "0.1.3",
4
+ "version": "0.1.8",
5
5
  "exports": {
6
6
  ".": {
7
7
  "import": "./index.ts",
@@ -9,6 +9,9 @@
9
9
  }
10
10
  },
11
11
  "type": "module",
12
+ "bin": {
13
+ "cc-statusline": "bin/cc-statusline.js"
14
+ },
12
15
  "private": false,
13
16
  "license": "MIT",
14
17
  "author": {
@@ -31,6 +34,8 @@
31
34
  ],
32
35
  "scripts": {
33
36
  "build:binary": "mkdir -p target && bun build --compile ./index.ts --outfile target/statusline",
37
+ "build:cli": "bun build ./src/cli.ts --outfile ./bin/cc-statusline.js --target node",
38
+ "prepublishOnly": "bun run build:cli",
34
39
  "install:binary": "cp target/statusline ~/.claude/"
35
40
  },
36
41
  "devDependencies": {
@@ -0,0 +1,310 @@
1
+ # Theming System: Render Function Approach
2
+
3
+ ## Selected Approach
4
+
5
+ **Render Function** with support for custom components. Theme authors export a function that receives context and returns the formatted statusline string.
6
+
7
+ ---
8
+
9
+ ## Theme API Design
10
+
11
+ ### Basic Theme Structure
12
+
13
+ ```typescript
14
+ // ~/.config/statusline/themes/my-theme.ts
15
+ import type { ThemeContext } from 'statusline/theme';
16
+
17
+ export default async function render(ctx: ThemeContext): Promise<string> {
18
+ const { status, helpers, colors: c } = ctx;
19
+
20
+ const dir = c.blue(`🗂️ ${helpers.abbreviatePath(status.workspace.current_dir)}`);
21
+ const git = c.green(`🌿 ${await helpers.gitBranch()}`);
22
+ const model = c.magenta(`⏣ ${helpers.modelName()}`);
23
+
24
+ return `${dir} │ ${git}\n${model}`;
25
+ }
26
+ ```
27
+
28
+ ### ThemeContext Interface
29
+
30
+ ```typescript
31
+ interface ThemeContext {
32
+ // Raw status data from Claude Code
33
+ status: StatusLineInput;
34
+
35
+ // ansi-colors instance for styling
36
+ colors: typeof import('ansi-colors');
37
+
38
+ // Helper functions (async where needed)
39
+ helpers: {
40
+ // Path utilities
41
+ abbreviatePath(path: string): string;
42
+ homePath(): string;
43
+
44
+ // Git utilities (async)
45
+ gitBranch(): Promise<string>;
46
+ gitStatus(): Promise<GitStatusResult>;
47
+ isGitRepo(): Promise<boolean>;
48
+
49
+ // Model utilities
50
+ modelName(): string; // "opus-4.5"
51
+ modelDisplayName(): string; // "Claude Opus 4.5"
52
+
53
+ // Session utilities
54
+ sessionId(): string;
55
+
56
+ // Cost/metrics utilities
57
+ formatCost(): string; // "$0.42"
58
+ formatDuration(): string; // "2m 34s"
59
+
60
+ // Generic formatting
61
+ truncate(str: string, maxLen: number): string;
62
+ };
63
+ }
64
+ ```
65
+
66
+ ---
67
+
68
+ ## Implementation Plan
69
+
70
+ ### Files to Create/Modify
71
+
72
+ 1. **`src/theme.ts`** (NEW) - Theme types, loader, and context builder
73
+ 2. **`src/defaultTheme.ts`** (NEW) - Built-in default theme
74
+ 3. **`index.ts`** - Add CLI argument parsing for `--theme`
75
+ 4. **`src/utils.ts`** - Export helpers for theme context
76
+
77
+ ### Step 1: Define Theme Types (`src/theme.ts`)
78
+
79
+ ```typescript
80
+ import type { StatusLineInput } from './statusLineSchema';
81
+ import type c from 'ansi-colors';
82
+
83
+ export interface ThemeHelpers {
84
+ abbreviatePath(path: string): string;
85
+ gitBranch(): Promise<string>;
86
+ gitStatus(): Promise<GitStatusResult>;
87
+ isGitRepo(): Promise<boolean>;
88
+ modelName(): string;
89
+ modelDisplayName(): string;
90
+ sessionId(): string;
91
+ formatCost(): string;
92
+ formatDuration(): string;
93
+ truncate(str: string, maxLen: number): string;
94
+ homePath(): string;
95
+ }
96
+
97
+ export interface ThemeContext {
98
+ status: StatusLineInput;
99
+ colors: typeof c;
100
+ helpers: ThemeHelpers;
101
+ }
102
+
103
+ export type ThemeFunction = (ctx: ThemeContext) => string | Promise<string>;
104
+
105
+ export interface ThemeModule {
106
+ default: ThemeFunction;
107
+ }
108
+ ```
109
+
110
+ ### Step 2: Theme Loader (`src/theme.ts`)
111
+
112
+ ```typescript
113
+ export async function loadTheme(themePath: string): Promise<ThemeFunction> {
114
+ // Resolve path (handle ~ expansion, relative paths)
115
+ const resolvedPath = resolvePath(themePath);
116
+
117
+ // Dynamic import (Bun handles .ts automatically)
118
+ const module = await import(resolvedPath) as ThemeModule;
119
+
120
+ if (typeof module.default !== 'function') {
121
+ throw new Error(`Theme must export a default function`);
122
+ }
123
+
124
+ return module.default;
125
+ }
126
+
127
+ export function createThemeContext(status: StatusLineInput): ThemeContext {
128
+ return {
129
+ status,
130
+ colors: c,
131
+ helpers: buildHelpers(status),
132
+ };
133
+ }
134
+ ```
135
+
136
+ ### Step 3: Default Theme (`src/defaultTheme.ts`)
137
+
138
+ ```typescript
139
+ import type { ThemeContext } from './theme';
140
+
141
+ export default async function render(ctx: ThemeContext): Promise<string> {
142
+ const { status, helpers, colors: c } = ctx;
143
+
144
+ const dirStatus = c.blue(formatDirectory(status, helpers));
145
+ const gitStatus = c.green(await formatGit(helpers));
146
+ const modelStatus = c.magenta(formatModel(helpers));
147
+ const sessionId = c.blue(formatSession(helpers));
148
+ const separator = c.bold.gray('⋮');
149
+
150
+ return `${dirStatus} ${separator} ${gitStatus}\n${modelStatus} ${separator} ${sessionId}`;
151
+ }
152
+ ```
153
+
154
+ ### Step 4: CLI Argument Parsing (`index.ts`)
155
+
156
+ ```typescript
157
+ // Parse --theme argument
158
+ const args = process.argv.slice(2);
159
+ const themeIndex = args.indexOf('--theme');
160
+ const themePath = themeIndex !== -1 ? args[themeIndex + 1] : null;
161
+
162
+ // Load theme (default or custom)
163
+ const renderTheme = themePath
164
+ ? await loadTheme(themePath)
165
+ : (await import('./src/defaultTheme')).default;
166
+
167
+ // Create context and render
168
+ const ctx = createThemeContext(status);
169
+ const statusLine = await renderTheme(ctx);
170
+ ```
171
+
172
+ ### Step 5: Refactor Helpers (`src/utils.ts`)
173
+
174
+ Export existing functions and add new helpers:
175
+
176
+ ```typescript
177
+ // Existing (keep)
178
+ export function abbreviatePath(path: string): string { ... }
179
+ export async function currentBranchName(cwd?: string): Promise<BranchResult> { ... }
180
+
181
+ // New helpers
182
+ export function formatCost(cost: number): string {
183
+ return `$${cost.toFixed(2)}`;
184
+ }
185
+
186
+ export function formatDuration(ms: number): string {
187
+ const seconds = Math.floor(ms / 1000);
188
+ const minutes = Math.floor(seconds / 60);
189
+ if (minutes > 0) {
190
+ return `${minutes}m ${seconds % 60}s`;
191
+ }
192
+ return `${seconds}s`;
193
+ }
194
+
195
+ export function truncate(str: string, maxLen: number): string {
196
+ if (str.length <= maxLen) return str;
197
+ return str.slice(0, maxLen - 1) + '…';
198
+ }
199
+ ```
200
+
201
+ ---
202
+
203
+ ## CLI Usage
204
+
205
+ ```bash
206
+ # Use default theme
207
+ statusline
208
+
209
+ # Use custom theme (absolute path)
210
+ statusline --theme /path/to/my-theme.ts
211
+
212
+ # Use custom theme (relative to home)
213
+ statusline --theme ~/.config/statusline/themes/minimal.ts
214
+
215
+ # Environment variable (fallback)
216
+ STATUSLINE_THEME=~/.config/statusline/themes/nerd.ts statusline
217
+ ```
218
+
219
+ ---
220
+
221
+ ## Example Themes
222
+
223
+ ### Minimal Single-Line Theme
224
+
225
+ ```typescript
226
+ // minimal.ts
227
+ import type { ThemeContext } from 'statusline/theme';
228
+
229
+ export default async function render({ status, helpers, colors: c }: ThemeContext): Promise<string> {
230
+ const dir = helpers.abbreviatePath(status.workspace.current_dir);
231
+ const branch = await helpers.gitBranch();
232
+ return c.dim(`${dir} (${branch})`);
233
+ }
234
+ ```
235
+
236
+ ### Powerline-Style Theme
237
+
238
+ ```typescript
239
+ // powerline.ts
240
+ import type { ThemeContext } from 'statusline/theme';
241
+
242
+ export default async function render({ status, helpers, colors: c }: ThemeContext): Promise<string> {
243
+ const dir = c.bgBlue.white(` ${helpers.abbreviatePath(status.workspace.current_dir)} `);
244
+ const git = c.bgGreen.black(` ${await helpers.gitBranch()} `);
245
+ const model = c.bgMagenta.white(` ${helpers.modelName()} `);
246
+
247
+ return `${dir}${git}${model}`;
248
+ }
249
+ ```
250
+
251
+ ### Theme with Custom Component (Time)
252
+
253
+ ```typescript
254
+ // with-clock.ts
255
+ import type { ThemeContext } from 'statusline/theme';
256
+
257
+ export default async function render({ status, helpers, colors: c }: ThemeContext): Promise<string> {
258
+ const time = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
259
+
260
+ const dir = c.blue(`🗂️ ${helpers.abbreviatePath(status.workspace.current_dir)}`);
261
+ const git = c.green(`🌿 ${await helpers.gitBranch()}`);
262
+ const clock = c.yellow(`🕐 ${time}`);
263
+
264
+ return `${dir} │ ${git} │ ${clock}`;
265
+ }
266
+ ```
267
+
268
+ ---
269
+
270
+ ## Error Handling
271
+
272
+ 1. **Theme load failure**: Fall back to default theme, log warning
273
+ 2. **Theme render error**: Catch, show error message in statusline, log details
274
+ 3. **Invalid export**: Validate `module.default` is a function
275
+
276
+ ```typescript
277
+ try {
278
+ const renderTheme = themePath ? await loadTheme(themePath) : defaultTheme;
279
+ statusLine = await renderTheme(ctx);
280
+ } catch (error) {
281
+ logger.error('Theme error', { error, themePath });
282
+ statusLine = c.red(`Theme error: ${error.message}`);
283
+ }
284
+ ```
285
+
286
+ ---
287
+
288
+ ## Type Distribution
289
+
290
+ For theme authors to get type hints, provide types via:
291
+
292
+ **Option 1**: Export types from main package
293
+ ```typescript
294
+ // Theme authors can reference
295
+ /// <reference path="./node_modules/statusline/theme.d.ts" />
296
+ ```
297
+
298
+ **Option 2**: Publish `@statusline/types` package (if this grows)
299
+
300
+ **Option 3**: Include `.d.ts` alongside binary (simplest for now)
301
+
302
+ ---
303
+
304
+ ## Verification
305
+
306
+ 1. Run with default theme: `echo '{}' | bun run index.ts`
307
+ 2. Run with custom theme: `echo '{}' | bun run index.ts --theme ./examples/minimal.ts`
308
+ 3. Test error handling: Use invalid theme path, theme with syntax error
309
+ 4. Test async helpers: Verify git operations work in themes
310
+ 5. Run existing tests: `bun test`
package/src/cli.ts ADDED
@@ -0,0 +1,78 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execSync } from "node:child_process";
4
+ import { existsSync, mkdirSync, copyFileSync, unlinkSync } from "node:fs";
5
+ import { homedir } from "node:os";
6
+ import { join } from "node:path";
7
+
8
+ const BINARY_NAME = "statusline";
9
+ const CLAUDE_DIR = join(homedir(), ".claude");
10
+ const TARGET_PATH = join(CLAUDE_DIR, BINARY_NAME);
11
+
12
+ function build(): void {
13
+ console.log("Building statusline binary...");
14
+ execSync("mkdir -p target && bun build --compile ./index.ts --outfile target/statusline", {
15
+ stdio: "inherit",
16
+ });
17
+ console.log("Build complete.");
18
+ }
19
+
20
+ function install(overwrite: boolean): void {
21
+ // Build first
22
+ build();
23
+
24
+ // Ensure ~/.claude directory exists
25
+ if (!existsSync(CLAUDE_DIR)) {
26
+ mkdirSync(CLAUDE_DIR, { recursive: true });
27
+ }
28
+
29
+ // Check if target file exists
30
+ if (existsSync(TARGET_PATH)) {
31
+ if (!overwrite) {
32
+ console.error(`Error: ${TARGET_PATH} already exists.`);
33
+ console.error("Use --overwrite to replace the existing file.");
34
+ process.exit(1);
35
+ }
36
+ console.log(`Overwriting existing file at ${TARGET_PATH}...`);
37
+ unlinkSync(TARGET_PATH); // Remove first to avoid ETXTBSY if binary is running
38
+ }
39
+
40
+ // Copy binary to destination
41
+ const sourcePath = join(process.cwd(), "target", BINARY_NAME);
42
+ copyFileSync(sourcePath, TARGET_PATH);
43
+ console.log(`Installed statusline to ${TARGET_PATH}`);
44
+ }
45
+
46
+ function printUsage(): void {
47
+ console.log(`Usage: cc-statusline <command> [options]
48
+
49
+ Commands:
50
+ install Build and install statusline to ~/.claude/
51
+
52
+ Options:
53
+ --overwrite Overwrite existing file if it exists
54
+ --help, -h Show this help message`);
55
+ }
56
+
57
+ function main(): void {
58
+ const args = process.argv.slice(2);
59
+ const command = args.find((arg) => !arg.startsWith("-"));
60
+ const flags = new Set(args.filter((arg) => arg.startsWith("-")));
61
+
62
+ if (flags.has("--help") || flags.has("-h") || args.length === 0) {
63
+ printUsage();
64
+ process.exit(0);
65
+ }
66
+
67
+ switch (command) {
68
+ case "install":
69
+ install(flags.has("--overwrite"));
70
+ break;
71
+ default:
72
+ console.error(`Unknown command: ${command}`);
73
+ printUsage();
74
+ process.exit(1);
75
+ }
76
+ }
77
+
78
+ main();
package/src/utils.ts CHANGED
@@ -156,6 +156,10 @@ function currentDirStatus(status: Status) {
156
156
  return dirStatus;
157
157
  }
158
158
 
159
+ function currentSessionId(status: Status) {
160
+ return `📝 ${status.session_id}`;
161
+ }
162
+
159
163
  export {
160
164
  abbreviateModelId,
161
165
  abbreviatePath,
@@ -163,6 +167,7 @@ export {
163
167
  currentDirStatus,
164
168
  currentGitStatus,
165
169
  currentModelStatus,
170
+ currentSessionId,
166
171
  };
167
172
 
168
173
  export type { BranchResult };