@nutthead/cc-statusline 0.1.9 → 0.2.0

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/index.ts CHANGED
@@ -1,9 +1,38 @@
1
+ import meow from "meow";
1
2
  import { configure } from "@logtape/logtape";
2
3
  import { log, logtapeConfig } from "./src/logging";
3
- import { defaultTheme } from "./src/theme/default";
4
+ import { defaultTheme } from "./src/defaultTheme";
5
+ import { loadTheme } from "./src/theme/loadTheme";
4
6
 
5
7
  await configure(logtapeConfig);
6
8
 
9
+ const cli = meow(
10
+ `
11
+ Usage
12
+ $ cc-statusline
13
+
14
+ Options
15
+ --theme, -t Use a custom theme
16
+
17
+ Examples
18
+ $ cc-statusline --rainbow
19
+ $ cc-statusline --theme ~/.config/cc-statusline/basic.js
20
+
21
+ `,
22
+ {
23
+ importMeta: import.meta, // This is required
24
+ flags: {
25
+ theme: {
26
+ type: "string",
27
+ shortFlag: "t",
28
+ isRequired: false,
29
+ },
30
+ },
31
+ },
32
+ );
33
+
34
+ const resolvedTheme = (cli.flags.theme && await loadTheme(cli.flags.theme)) || defaultTheme;
35
+
7
36
  const input = await Bun.stdin.stream().json();
8
37
  log.debug("input: {input}", input);
9
- console.log(await defaultTheme(input));
38
+ console.log(await resolvedTheme(input));
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.9",
4
+ "version": "0.2.0",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "cc-statusline": "bin/cc-statusline.js"
@@ -42,7 +42,9 @@
42
42
  "@logtape/file": "^1.3.6",
43
43
  "@logtape/logtape": "^1.3.6",
44
44
  "ansi-colors": "^4.1.3",
45
+ "meow": "^14.0.0",
45
46
  "simple-git": "^3.30.0",
47
+ "ts-pattern": "^5.9.0",
46
48
  "type-fest": "^5.3.1",
47
49
  "zod": "^4.3.5"
48
50
  },
package/src/cli.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import meow from "meow";
3
4
  import { execSync } from "node:child_process";
4
5
  import { existsSync, mkdirSync, copyFileSync, unlinkSync } from "node:fs";
5
6
  import { homedir } from "node:os";
@@ -9,70 +10,107 @@ const BINARY_NAME = "statusline";
9
10
  const CLAUDE_DIR = join(homedir(), ".claude");
10
11
  const TARGET_PATH = join(CLAUDE_DIR, BINARY_NAME);
11
12
 
12
- function build(): void {
13
+ const cli = meow(
14
+ `
15
+ Usage
16
+ $ cc-statusline <command>
17
+
18
+ Commands
19
+ install Build and install statusline to ~/.claude/
20
+
21
+ Options
22
+ --overwrite Overwrite existing file if it exists
23
+
24
+ Examples
25
+ $ cc-statusline install
26
+ $ cc-statusline install --overwrite
27
+ `,
28
+ {
29
+ importMeta: import.meta,
30
+ flags: {
31
+ overwrite: {
32
+ type: "boolean",
33
+ default: false,
34
+ },
35
+ },
36
+ },
37
+ );
38
+
39
+ export function build(): void {
13
40
  console.log("Building statusline binary...");
14
- execSync("mkdir -p target && bun build --compile ./index.ts --outfile target/statusline", {
15
- stdio: "inherit",
16
- });
41
+ execSync(
42
+ "mkdir -p target && bun build --compile ./index.ts --outfile target/statusline",
43
+ { stdio: "inherit" },
44
+ );
17
45
  console.log("Build complete.");
18
46
  }
19
47
 
20
- function install(overwrite: boolean): void {
21
- // Build first
22
- build();
48
+ export interface InstallDeps {
49
+ claudeDir: string;
50
+ targetPath: string;
51
+ sourcePath: string;
52
+ doBuild: () => void;
53
+ existsSync: (path: string) => boolean;
54
+ mkdirSync: (path: string, options?: { recursive?: boolean }) => void;
55
+ copyFileSync: (src: string, dest: string) => void;
56
+ unlinkSync: (path: string) => void;
57
+ }
58
+
59
+ const defaultDeps: InstallDeps = {
60
+ claudeDir: CLAUDE_DIR,
61
+ targetPath: TARGET_PATH,
62
+ sourcePath: join(process.cwd(), "target", BINARY_NAME),
63
+ doBuild: build,
64
+ existsSync,
65
+ mkdirSync,
66
+ copyFileSync,
67
+ unlinkSync,
68
+ };
69
+
70
+ export function installBinary(
71
+ overwrite: boolean,
72
+ deps: InstallDeps = defaultDeps,
73
+ ): void {
74
+ deps.doBuild();
23
75
 
24
- // Ensure ~/.claude directory exists
25
- if (!existsSync(CLAUDE_DIR)) {
26
- mkdirSync(CLAUDE_DIR, { recursive: true });
76
+ if (!deps.existsSync(deps.claudeDir)) {
77
+ deps.mkdirSync(deps.claudeDir, { recursive: true });
27
78
  }
28
79
 
29
- // Check if target file exists
30
- if (existsSync(TARGET_PATH)) {
80
+ if (deps.existsSync(deps.targetPath)) {
31
81
  if (!overwrite) {
32
- console.error(`Error: ${TARGET_PATH} already exists.`);
82
+ console.error(`Error: ${deps.targetPath} already exists.`);
33
83
  console.error("Use --overwrite to replace the existing file.");
34
84
  process.exit(1);
35
85
  }
36
- console.log(`Overwriting existing file at ${TARGET_PATH}...`);
37
- unlinkSync(TARGET_PATH); // Remove first to avoid ETXTBSY if binary is running
86
+ console.log(`Overwriting existing file at ${deps.targetPath}...`);
87
+ deps.unlinkSync(deps.targetPath);
38
88
  }
39
89
 
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}`);
90
+ deps.copyFileSync(deps.sourcePath, deps.targetPath);
91
+ console.log(`Installed statusline to ${deps.targetPath}`);
44
92
  }
45
93
 
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`);
94
+ export function install(overwrite: boolean): void {
95
+ installBinary(overwrite);
55
96
  }
56
97
 
57
98
  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
- }
99
+ const command = cli.input[0];
66
100
 
67
101
  switch (command) {
68
102
  case "install":
69
- install(flags.has("--overwrite"));
103
+ install(cli.flags.overwrite);
104
+ break;
105
+ case undefined:
106
+ cli.showHelp();
70
107
  break;
71
108
  default:
72
109
  console.error(`Unknown command: ${command}`);
73
- printUsage();
74
- process.exit(1);
110
+ cli.showHelp(1);
75
111
  }
76
112
  }
77
113
 
78
- main();
114
+ if (import.meta.main) {
115
+ main();
116
+ }
@@ -1,16 +1,16 @@
1
1
  import { z } from "zod";
2
- import { log } from "../logging";
3
- import { statusSchema } from "../statusLineSchema";
2
+ import { log } from "./logging";
3
+ import { statusSchema } from "./statusLineSchema";
4
4
  import {
5
5
  currentDirStatus,
6
6
  currentGitStatus,
7
7
  currentModelStatus,
8
8
  currentSessionId,
9
- } from "../utils";
9
+ } from "./utils";
10
10
 
11
11
  import c from "ansi-colors";
12
12
 
13
- async function defaultTheme(input?: string) {
13
+ async function defaultTheme(input?: string): Promise<string> {
14
14
  let statusLine = null;
15
15
 
16
16
  if (input) {
@@ -22,9 +22,14 @@ async function defaultTheme(input?: string) {
22
22
  const gitStatus = c.green(await currentGitStatus());
23
23
  const modelStatus = c.magenta(currentModelStatus(status));
24
24
  const sessionId = c.blue(currentSessionId(status));
25
- const separator = c.bold.gray("⋮");
25
+ const separator = c.bold.gray(" ");
26
26
 
27
- statusLine = `${dirStatus} ${separator} ${gitStatus}\n${modelStatus} ${separator} ${sessionId}`;
27
+ statusLine = [
28
+ [dirStatus, gitStatus],
29
+ [modelStatus, sessionId],
30
+ ]
31
+ .map((row) => row.join(separator))
32
+ .join("\n");
28
33
  } else {
29
34
  log.error("Failed to parse input: {error}", {
30
35
  error: JSON.stringify(z.treeifyError(result.error)),
@@ -0,0 +1,35 @@
1
+ import { join, resolve } from "node:path";
2
+ import { homedir } from "node:os";
3
+
4
+ type ThemeFunction = (input?: string) => Promise<string>;
5
+
6
+ interface ThemeModule {
7
+ default: ThemeFunction;
8
+ }
9
+
10
+ function expandTilde(path: string) {
11
+ if (path.startsWith("~")) {
12
+ return join(homedir(), path.slice(1));
13
+ }
14
+
15
+ return path;
16
+ }
17
+
18
+ function resolvePath(path: string) {
19
+ const expandedPath = expandTilde(path);
20
+ return resolve(expandedPath);
21
+ }
22
+
23
+ async function loadTheme(themePath: string): Promise<ThemeFunction | null> {
24
+ const resovedPath = resolvePath(themePath);
25
+
26
+ const module = (await import(resovedPath)) as ThemeModule;
27
+
28
+ if (typeof module.default !== `function`) {
29
+ return null;
30
+ }
31
+
32
+ return module.default;
33
+ }
34
+
35
+ export { type ThemeModule, type ThemeFunction, loadTheme };