@jv-k/claude-gauge 0.0.1 → 1.1.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/README.md CHANGED
@@ -1,5 +1,193 @@
1
- # @jv-k/claude-gauge
1
+ <h1 align="center">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/jv-k/claude-gauge/main/docs/media/wordmark-dark.png">
4
+ <img src="https://raw.githubusercontent.com/jv-k/claude-gauge/main/docs/media/wordmark-light.png" alt="claude-gauge" width="360">
5
+ </picture>
6
+ </h1>
2
7
 
3
- This version is a placeholder. It holds the package name so that releases can publish from GitHub Actions through npm trusted publishing. It contains no code.
8
+ <div align="center">
9
+ <img src="https://raw.githubusercontent.com/jv-k/claude-gauge/main/docs/media/hero.png" alt="The bottom of a Claude Code session: the prompt, and under it the claude-gauge status line with context, 5-hour and weekly usage bars, then the time, session length, repository, branch, model and effort.">
10
+ <p>
11
+ <a href="https://www.npmjs.com/package/@jv-k/claude-gauge"><img src="https://img.shields.io/npm/v/%40jv-k%2Fclaude-gauge" alt="npm version"></a>
12
+ <a href="https://github.com/jv-k/claude-gauge/actions/workflows/test.yml"><img src="https://github.com/jv-k/claude-gauge/actions/workflows/test.yml/badge.svg" alt="Test status"></a>
13
+ <a href="LICENSE"><img src="https://img.shields.io/badge/licence-MIT-blue.svg" alt="MIT licence"></a>
14
+ </p>
15
+ <p>
16
+ <a href="#how-it-looks"><b>How it looks</b></a> &nbsp;◦&nbsp;
17
+ <a href="#getting-started"><b>Getting started</b></a> &nbsp;◦&nbsp;
18
+ <a href="https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md"><b>Full reference</b></a> &nbsp;◦&nbsp;
19
+ <a href="#upgrading-from-claude-hud"><b>Upgrading from claude-hud</b></a>
20
+ </p>
21
+ </div>
4
22
 
5
- Source, docs and releases: https://github.com/jv-k/claude-gauge
23
+ **claude-gauge** is a compact status line and token line for [Claude Code](https://code.claude.com), in the terminal, the VS Code extension and the desktop app.
24
+
25
+ ## How it looks
26
+
27
+ In the terminal, the status line sits under the prompt:
28
+
29
+ ```text
30
+ ctx 43% ▓▓░░░ 86.0k │ 5h 9% ░░┃░░ → 14:10 │ 7d 41% ▓▓░┃░ → 3d
31
+ 11:10 │ 1h12m │ jv-k/claude-gauge │ ⎇ main* ↑1 │ Opus 5.5 │ effort high
32
+ ```
33
+
34
+ The token line shows at the end of each turn:
35
+
36
+ ```text
37
+ 11:10 │ 4 req │ out 3.4k (1.2k think) │ cache w6.5k r1.69M │ ctx 43% ▓▓░░░ 427k
38
+ ```
39
+
40
+ In VS Code and the desktop app, Claude ends each reply with the same bars:
41
+
42
+ ```text
43
+ ctx 43% ▓▓░░░ 86.0k │ 5h 9% ░░┃░░ → 14:10 │ 7d 41% ▓▓░┃░ → 3d
44
+ 11:10 │ 1h12m │ jv-k/claude-gauge │ ⎇ main* ↑1 │ Opus 5.5 │ effort high
45
+ 4 req │ out 3.4k (1.2k think) │ cache w6.5k r1.69M │ ctx 43% ▓▓░░░ 427k
46
+ ```
47
+
48
+ ## Getting started
49
+
50
+ You need Claude Code and Node.js 18 or later.
51
+
52
+ ### Install the plugin
53
+
54
+ In Claude Code, run these three commands:
55
+
56
+ ```text
57
+ /plugin marketplace add jv-k/claude-gauge
58
+ /plugin install claude-gauge@claude-gauge
59
+ /claude-gauge:setup
60
+ ```
61
+
62
+ If Claude Code does not find `/claude-gauge:setup`, run `/reload-plugins` first. Setup asks which bars you want. Then it backs up `~/.claude/settings.json` and adds the bars to it. Start a new session to see them.
63
+
64
+ ### Or install from npm
65
+
66
+ In a terminal, run:
67
+
68
+ ```sh
69
+ npx @jv-k/claude-gauge setup
70
+ ```
71
+
72
+ Setup shows the status line and asks which parts you want. It draws the status line again after each answer:
73
+
74
+ ![claude-gauge setup in a terminal: it shows the default rows, then draws them again as the answers change the second row, the bar size and the theme.](https://raw.githubusercontent.com/jv-k/claude-gauge/main/docs/media/demo.gif)
75
+
76
+ ### VS Code and the desktop app
77
+
78
+ The VS Code extension and the desktop app show no custom status line and no token line. To see the bars there, add one SessionStart hook for each bar. In those two apps, the hook tells Claude to end each reply with the bars. In the terminal, it does nothing. First install claude-gauge with the plugin or with npm, as above. Then add the hooks in one of two ways.
79
+
80
+ #### Ask Claude
81
+
82
+ Paste this prompt into a Claude Code session:
83
+
84
+ ```text
85
+ Set up claude-gauge for the VS Code extension and the desktop app.
86
+ Read ~/.claude/settings.json, or $CLAUDE_CONFIG_DIR/settings.json when that variable is set.
87
+ Make a backup copy of the file first.
88
+ Find the statusLine command that runs claude-gauge's statusline.js, and the Stop hook that runs its tokenline.js.
89
+ Add two entries to hooks.SessionStart and keep every entry that is there already:
90
+ one runs the same statusline.js with --instruct, and one runs the same tokenline.js with --instruct.
91
+ Put each bar's existing switches after --instruct.
92
+ The status line already shows the time, so give the token line hook a --show without time, such as --show req,out,cache,ctx.
93
+ Test each new command with CLAUDE_CODE_ENTRYPOINT=claude-vscode. It must print an instruction.
94
+ Then tell me to start a new session.
95
+ ```
96
+
97
+ #### Add the hooks by hand
98
+
99
+ 1. Open `~/.claude/settings.json` and find the `statusLine` command that setup wrote. Its folder is `~/.claude/claude-gauge/runtime/` after an npm install, or `~/.claude/claude-gauge/launcher/` after a plugin install.
100
+ 2. Add these two entries to the `SessionStart` array under `hooks`, with the folder from step 1. Keep the entries that are there already.
101
+
102
+ ```json
103
+ { "hooks": [ { "type": "command", "command": "node ~/.claude/claude-gauge/runtime/statusline.js --instruct" } ] },
104
+ { "hooks": [ { "type": "command", "command": "node ~/.claude/claude-gauge/runtime/tokenline.js --instruct --show req,out,cache,ctx" } ] }
105
+ ```
106
+
107
+ 3. Start a new session.
108
+
109
+ #### The result
110
+
111
+ Each reply then ends with the bars, as in [How it looks](#how-it-looks). The token line leaves out its time there, because the status line shows it. To change a bar in the replies, put its switches after `--instruct`, for example `--show 5h,7d`. On a 1M-context model, add `--window 1m` to both hooks. Each reply needs one or two more short tool calls. Uninstall also removes these hooks.
112
+
113
+ ### Change, update or remove the bars
114
+
115
+ | Plugin | npm | Effect |
116
+ | --- | --- | --- |
117
+ | `/claude-gauge:configure` | `npx @jv-k/claude-gauge configure` | Changes the parts and switches of the bars. |
118
+ | `/plugin`, then **Marketplaces** | `npx @jv-k/claude-gauge@latest update` | Updates claude-gauge and keeps your switches. |
119
+ | `/claude-gauge:uninstall` | `npx @jv-k/claude-gauge uninstall` | Removes claude-gauge and puts back the status line it replaced. |
120
+
121
+ ## Upgrading from claude-hud
122
+
123
+ If you use [claude-hud](https://github.com/jarrodwatts/claude-hud), run setup as in [Getting started](#getting-started). Setup finds claude-hud's status line, shows it, and asks before it replaces it. It saves claude-hud's command, so uninstall puts claude-hud back. Keep the claude-hud plugin installed until you are sure, because that command needs it.
124
+
125
+ claude-gauge has no configuration file. Each claude-hud option becomes a part or a switch in the bar's command. The reference [maps each option](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#upgrading-from-claude-hud) to claude-gauge.
126
+
127
+ ## What the bars show
128
+
129
+ The status line shows these parts by default:
130
+
131
+ | Part | Example | Shows |
132
+ | --- | --- | --- |
133
+ | `ctx` | `ctx 43% ▓▓░░░ 86.0k` | How much of the context window is in use. |
134
+ | `5h` | `5h 9% ░░┃░░ → 14:10` | Your 5-hour usage, and the time the window resets. |
135
+ | `7d` | `7d 41% ▓▓░┃░ → 3d` | Your weekly usage, and the days until it resets. |
136
+ | `time` | `11:10` | The local time. |
137
+ | `duration` | `1h12m` | How long the session has run. |
138
+ | `repo` | `jv-k/claude-gauge` | The repository, or the folder name. |
139
+ | `branch` | `⎇ main* ↑1` | The git branch. `*` means changes, and `↑1` means one commit ahead of the remote. |
140
+ | `model` | `Opus 5.5` | The model. |
141
+ | `effort` | `effort high` | The reasoning effort. |
142
+
143
+ The pace marker `┃` shows how much of the window has passed. Its colour shows where your usage goes at the current pace. Green stays well under the limit. Red and purple go over it.
144
+
145
+ Other parts show the tools and subagents in use, todos, git changes, pull requests, the cost per day and week, memory use and more.
146
+
147
+ > [!TIP]
148
+ > The reference describes [all 40 parts](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#what-the-bars-show), with what each one shows and when.
149
+
150
+ ## Customise
151
+
152
+ Each bar takes switches in its `command` in your settings. `configure` writes them for you. Two examples:
153
+
154
+ ```text
155
+ --show 5h,7d
156
+
157
+ 5h 9% ░░┃░░ → 14:10 │ 7d 41% ▓▓░┃░ → 3d
158
+ ```
159
+
160
+ ```text
161
+ --show ctx,5h,7d --show repo,branch,pr,lines --show model,effort,cost,cache
162
+
163
+ ctx 43% ▓▓░░░ 86.0k │ 5h 9% ░░┃░░ → 14:10 │ 7d 41% ▓▓░┃░ → 3d
164
+ jv-k/claude-gauge │ ⎇ main │ #12 approved │ +156 −23
165
+ Opus 5.5 │ effort high │ $1.23 │ cache 91% warm
166
+ ```
167
+
168
+ Each `--show` is one row.
169
+
170
+ > [!TIP]
171
+ > The reference lists [every switch](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#options), the four [themes](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#themes) and [more examples](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#examples).
172
+
173
+ ## Full reference
174
+
175
+ [docs/reference.md](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md) has everything this page leaves out:
176
+
177
+ - [Every status line part](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#what-the-bars-show), and the [token line's parts](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#token-line)
178
+ - [Every switch](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#options), the [themes](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#themes), [colours and bar characters](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#colours-and-bar-characters), and [examples](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#examples)
179
+ - [Other ways to install](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#install): with a Claude prompt, or by hand from a clone
180
+ - [Update](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#update) and [uninstall](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#uninstall) for each install route
181
+ - [The claude-hud option map](https://github.com/jv-k/claude-gauge/blob/main/docs/reference.md#upgrading-from-claude-hud)
182
+
183
+ ## Contributing
184
+
185
+ [CONTRIBUTING.md](CONTRIBUTING.md) gives the setup, the tests and the rules for a change. [RELEASING.md](RELEASING.md) gives the release steps.
186
+
187
+ ## Star history
188
+
189
+ [![Star history of jv-k/claude-gauge](https://api.star-history.com/svg?repos=jv-k/claude-gauge&type=Date)](https://star-history.com/#jv-k/claude-gauge&Date)
190
+
191
+ ## License
192
+
193
+ [MIT](LICENSE)
package/dist/cli.js ADDED
@@ -0,0 +1,408 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ // The claude-gauge command: sets the bars up in Claude Code's settings,
4
+ // changes their switches, takes them out again, and updates the copy of the
5
+ // scripts that the settings run.
6
+ //
7
+ // claude-gauge setup
8
+ // claude-gauge setup --yes
9
+ // claude-gauge setup --status-line "--show ctx,5h,7d --segments 10" --token-line "--window 1m"
10
+ // claude-gauge configure --no-token-line
11
+ // claude-gauge uninstall
12
+ // claude-gauge update
13
+ //
14
+ // setup and configure with no bar switches ask the questions themselves,
15
+ // with a preview of the status line after each answer (wizard.ts). With
16
+ // switches they ask nothing: the form the plugin's slash commands run once
17
+ // Claude has asked the questions.
18
+ //
19
+ // Everything it keeps lives in the state folder, claude-gauge/ in the Claude
20
+ // config folder ($CLAUDE_CONFIG_DIR, else ~/.claude): the copy of the
21
+ // scripts in runtime/, or, when it runs from the plugin, the launcher in
22
+ // launcher/, and the status line it replaced in .state/.
23
+ Object.defineProperty(exports, "__esModule", { value: true });
24
+ const fs = require("node:fs");
25
+ const os = require("node:os");
26
+ const path = require("node:path");
27
+ const readline = require("node:readline");
28
+ const node_child_process_1 = require("node:child_process");
29
+ const settings_1 = require("./settings");
30
+ const settings_file_1 = require("./settings-file");
31
+ const wizard_1 = require("./wizard");
32
+ const statusline_1 = require("./statusline");
33
+ const wordmark_1 = require("./wordmark");
34
+ const USAGE = `Usage: claude-gauge <setup | configure | uninstall | update> [switches]
35
+
36
+ setup add the status line and the token line to Claude Code's settings
37
+ configure change the switches of a bar that setup added, or take it out
38
+ uninstall take claude-gauge out, and put back the status line it replaced
39
+ update refresh the copy of claude-gauge that the settings run
40
+
41
+ Switches for setup and configure:
42
+ --status-line <switches> add the status line, run with these switches ("" for none)
43
+ --no-status-line leave the status line out
44
+ --token-line <switches> add the token line, run with these switches ("" for none)
45
+ --no-token-line leave the token line out
46
+ --replace replace a status line that is not claude-gauge's
47
+ --yes setup only: add each bar not chosen above, with no switches
48
+
49
+ With none of the bar switches above, setup and configure ask which bars and
50
+ parts you want, and show the status line after each answer.
51
+
52
+ The bars' switches are in README.md, under Options. The settings file is
53
+ $CLAUDE_CONFIG_DIR/settings.json, else ~/.claude/settings.json.
54
+ `;
55
+ // The wordmark and a blank line, in colour when `stream` takes colour. The
56
+ // usage and the questions start with it. Scripts and the plugin's slash
57
+ // commands run the commands with switches, and those print no wordmark.
58
+ const banner = (stream) => `${(0, wordmark_1.wordmark)((0, wordmark_1.colorEnabled)(stream))}\n`;
59
+ // The usage, under the wordmark, for `stream`.
60
+ const usage = (stream) => banner(stream) + USAGE;
61
+ // A mistake in the command line: exit 2, with the usage.
62
+ class UsageError extends Error {
63
+ }
64
+ const COMMANDS = ['setup', 'configure', 'uninstall', 'update'];
65
+ // Every switch: its names, the commands that take it, and what it sets.
66
+ // --help goes with any command.
67
+ const SWITCHES = [
68
+ { names: ['--status-line'], commands: ['setup', 'configure'], apply: (args, value) => { args.statusLine = value(); } },
69
+ { names: ['--no-status-line'], commands: ['setup', 'configure'], apply: (args) => { args.statusLine = null; } },
70
+ { names: ['--token-line'], commands: ['setup', 'configure'], apply: (args, value) => { args.tokenLine = value(); } },
71
+ { names: ['--no-token-line'], commands: ['setup', 'configure'], apply: (args) => { args.tokenLine = null; } },
72
+ { names: ['--replace'], commands: ['setup', 'configure'], apply: (args) => { args.replace = true; } },
73
+ { names: ['--yes', '-y'], commands: ['setup'], apply: (args) => { args.yes = true; } },
74
+ { names: ['--help', '-h'], commands: [...COMMANDS], apply: (args) => { args.help = true; } },
75
+ ];
76
+ // Unlike the two bars, which ignore what they do not know so that a typo
77
+ // never blanks the status line, the command refuses an unknown word: it
78
+ // writes the user's settings, and a typo there should stop it.
79
+ function parseArgs(argv) {
80
+ const args = { help: false, replace: false, yes: false };
81
+ const seen = [];
82
+ for (let i = 0; i < argv.length; i++) {
83
+ const [name, inline] = argv[i].split(/=(.*)/s);
84
+ if (!name.startsWith('-')) {
85
+ if (args.command)
86
+ throw new UsageError(`Unexpected word: ${name}`);
87
+ if (name === 'help')
88
+ args.help = true;
89
+ else if (COMMANDS.includes(name))
90
+ args.command = name;
91
+ else
92
+ throw new UsageError(`Unknown command: ${name}`);
93
+ continue;
94
+ }
95
+ const known = SWITCHES.find((s) => s.names.includes(name));
96
+ if (!known)
97
+ throw new UsageError(`Unknown switch: ${name}`);
98
+ // A value is the next word whatever it holds, since switches start with --.
99
+ known.apply(args, () => {
100
+ const v = inline ?? argv[++i];
101
+ if (v === undefined)
102
+ throw new UsageError(`${name} needs a value: the switches for that bar, or "" for none.`);
103
+ return v;
104
+ });
105
+ seen.push(known);
106
+ }
107
+ const { command } = args;
108
+ const stray = command && seen.find((s) => !s.commands.includes(command));
109
+ if (stray)
110
+ throw new UsageError(`${args.command} takes no ${stray.names[0]}`);
111
+ return args;
112
+ }
113
+ // The folders and files it works with.
114
+ const configDir = () => process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), '.claude');
115
+ const settingsFile = () => path.join(configDir(), 'settings.json');
116
+ const stateDir = () => path.join(configDir(), 'claude-gauge');
117
+ const savedStatusLineFile = () => path.join(stateDir(), '.state', 'previous-statusline.json');
118
+ // The package this command runs from: dist/.. of this file.
119
+ const packageRoot = path.resolve(__dirname, '..');
120
+ const RUNTIME = ['statusline.js', 'tokenline.js'];
121
+ // The native realpath also expands a Windows short name, such as RUNNER~1,
122
+ // which Bun has already expanded in __dirname and the JavaScript one keeps.
123
+ function sameFolder(a, b) {
124
+ try {
125
+ const [x, y] = [fs.realpathSync.native(a), fs.realpathSync.native(b)];
126
+ return process.platform === 'win32' ? x.toLowerCase() === y.toLowerCase() : x === y;
127
+ }
128
+ catch {
129
+ return false;
130
+ }
131
+ }
132
+ // The folder that holds the plugin's installed versions, when this command
133
+ // runs from one of them: Claude Code installs a plugin into
134
+ // <plugins>/cache/<marketplace>/<plugin>/<version>/, with its manifest.
135
+ function pluginVersions() {
136
+ const versions = path.dirname(packageRoot);
137
+ const cache = path.dirname(path.dirname(versions));
138
+ if (path.basename(cache) !== 'cache')
139
+ return undefined;
140
+ if (!fs.existsSync(path.join(packageRoot, '.claude-plugin', 'plugin.json')))
141
+ return undefined;
142
+ return versions;
143
+ }
144
+ function scripts() {
145
+ if (sameFolder(packageRoot, stateDir()))
146
+ return { route: 'clone', dir: path.join(stateDir(), 'dist') };
147
+ const versions = pluginVersions();
148
+ if (versions)
149
+ return { route: 'launcher', dir: path.join(stateDir(), 'launcher'), versions };
150
+ return { route: 'copy', dir: path.join(stateDir(), 'runtime') };
151
+ }
152
+ const version = () => {
153
+ try {
154
+ return JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8')).version ?? 'unknown';
155
+ }
156
+ catch {
157
+ return 'unknown';
158
+ }
159
+ };
160
+ // Copies the two scripts into `dir`, each replaced whole, so a status line
161
+ // that runs during the copy reads the old script or the new one. The
162
+ // package.json beside them keeps Node reading them as CommonJS, whatever a
163
+ // package.json further up says.
164
+ function copyRuntime(dir) {
165
+ for (const file of RUNTIME)
166
+ (0, settings_file_1.writeAtomic)(path.join(dir, file), fs.readFileSync(path.join(packageRoot, 'dist', file)));
167
+ writePackageJson(dir, 'claude-gauge-runtime');
168
+ }
169
+ function writePackageJson(dir, name) {
170
+ const pkg = { name, version: version(), private: true, type: 'commonjs' };
171
+ (0, settings_file_1.writeAtomic)(path.join(dir, 'package.json'), JSON.stringify(pkg, null, 2) + '\n');
172
+ }
173
+ // Writes the launcher into `dir`: launch.js, and one entry file per script
174
+ // that runs it from the newest version in `versions`. The entry files keep
175
+ // the scripts' names, so the settings name statusline.js and tokenline.js
176
+ // as on every other route, and claude-gauge knows them as its own.
177
+ function writeLauncher(dir, versions) {
178
+ (0, settings_file_1.writeAtomic)(path.join(dir, 'launch.js'), fs.readFileSync(path.join(packageRoot, 'dist', 'launcher.js')));
179
+ for (const file of RUNTIME) {
180
+ const entry = [
181
+ '#!/usr/bin/env node',
182
+ `// Written by claude-gauge setup: runs ${file} from the newest installed`,
183
+ '// claude-gauge plugin, so a plugin update needs no setup.',
184
+ `require('./launch.js').launch(${JSON.stringify(versions)}, ${JSON.stringify(file)});`,
185
+ '',
186
+ ].join('\n');
187
+ (0, settings_file_1.writeAtomic)(path.join(dir, file), entry);
188
+ }
189
+ writePackageJson(dir, 'claude-gauge-launcher');
190
+ }
191
+ // The status line claude-gauge replaced: undefined when none is saved, null
192
+ // when there was none to replace. Only a missing file means none is saved: a
193
+ // file it cannot read or parse stops the command, because going on would
194
+ // drop the only record of the status line to put back.
195
+ function savedStatusLine() {
196
+ const file = savedStatusLineFile();
197
+ let text;
198
+ try {
199
+ text = fs.readFileSync(file, 'utf8');
200
+ }
201
+ catch (err) {
202
+ if (err.code === 'ENOENT')
203
+ return undefined;
204
+ throw new Error(`Cannot read ${file}: ${err.message}. Fix or delete it by hand.`);
205
+ }
206
+ let saved;
207
+ try {
208
+ saved = JSON.parse(text);
209
+ }
210
+ catch {
211
+ saved = undefined;
212
+ }
213
+ const statusLine = saved?.statusLine;
214
+ if (typeof saved !== 'object' || saved === null || !(statusLine === null || (typeof statusLine === 'object' && !Array.isArray(statusLine)))) {
215
+ throw new Error(`${file} does not hold a saved status line, so claude-gauge cannot tell what to put back. Fix or delete it by hand.`);
216
+ }
217
+ return statusLine;
218
+ }
219
+ const say = (...lines) => process.stdout.write(lines.join('\n') + '\n');
220
+ // setup and configure: turns the bars' switches into commands, plans the
221
+ // settings, and writes them.
222
+ function apply({ statusLine, tokenLine }, replace) {
223
+ const file = settingsFile();
224
+ const { settings, text } = (0, settings_file_1.readSettings)(file);
225
+ const where = scripts();
226
+ const { dir } = where;
227
+ const command = (script, switches) => switches === null || switches === undefined ? switches : (0, settings_1.commandFor)(path.join(dir, script), switches);
228
+ const choices = {
229
+ statusLine: command('statusline.js', statusLine),
230
+ tokenLine: command('tokenline.js', tokenLine),
231
+ replace,
232
+ previous: savedStatusLine() ?? null,
233
+ };
234
+ const next = (0, settings_1.plan)(settings, choices);
235
+ if (next.blocked) {
236
+ const current = settings.statusLine.command ?? JSON.stringify(settings.statusLine);
237
+ const whose = next.found === 'claude-hud' ? "claude-hud's status line" : 'another status line';
238
+ throw new Error([
239
+ `${file} already runs ${whose}:`,
240
+ ` ${current}`,
241
+ 'Run again with --replace to replace it. claude-gauge saves it, and claude-gauge uninstall puts it back.',
242
+ ].join('\n'));
243
+ }
244
+ // The scripts go in first, so the settings never name a missing file.
245
+ if (typeof choices.statusLine === 'string' || typeof choices.tokenLine === 'string') {
246
+ if (where.route === 'copy')
247
+ copyRuntime(dir);
248
+ if (where.route === 'launcher')
249
+ writeLauncher(dir, where.versions);
250
+ }
251
+ // The status line to put back is saved before the settings change, so a
252
+ // failed save leaves nothing to restore wrongly.
253
+ if (next.backup)
254
+ (0, settings_file_1.writeAtomic)(savedStatusLineFile(), JSON.stringify(next.backup, null, 2) + '\n');
255
+ const settingsBackup = next.changed ? (0, settings_file_1.writeSettings)(file, next.settings, text) : null;
256
+ if (next.dropBackup)
257
+ fs.rmSync(savedStatusLineFile(), { force: true });
258
+ if (!next.changed) {
259
+ say(`${file} already holds these choices. Nothing to change.`);
260
+ return;
261
+ }
262
+ const ours = (0, settings_1.installed)(next.settings);
263
+ say(`Status line: ${ours.statusLine ?? 'not set up'}`, `Token line: ${ours.tokenLine ?? 'not set up'}`, ...(settingsBackup ? [`Backup of the previous settings: ${settingsBackup}`] : []), 'Start a new Claude Code session to pick up the changes.');
264
+ }
265
+ // The questions, asked on the terminal: each prompt on stdout, each answer a
266
+ // line of stdin. Lines are queued as they arrive, so answers piped in all at
267
+ // once each reach their question.
268
+ function terminalIo() {
269
+ const rl = readline.createInterface({ input: process.stdin, crlfDelay: Infinity });
270
+ const lines = rl[Symbol.asyncIterator]();
271
+ return {
272
+ ask: async (question) => {
273
+ process.stdout.write(question);
274
+ const next = await lines.next();
275
+ return next.done ? undefined : next.value;
276
+ },
277
+ write: (text) => process.stdout.write(text),
278
+ close: () => rl.close(),
279
+ };
280
+ }
281
+ // Whether gh runs here, and the call that stars the repo with it.
282
+ const hasGh = () => (0, node_child_process_1.spawnSync)('gh', ['--version'], { stdio: 'ignore' }).status === 0;
283
+ const starWithGh = () => (0, node_child_process_1.spawnSync)('gh', ['api', '--method', 'PUT', 'user/starred/jv-k/claude-gauge'], { stdio: ['ignore', 'ignore', 'inherit'] }).status === 0;
284
+ // setup or configure with no bar switches: asks before it replaces another
285
+ // status line, then asks for the bars, and writes them. configure starts the
286
+ // questions from the bars set up, setup from the defaults. setup ends with
287
+ // the star offer.
288
+ async function interactive(command, replace) {
289
+ const io = terminalIo();
290
+ try {
291
+ io.write(banner(process.stdout));
292
+ const file = settingsFile();
293
+ const { settings } = (0, settings_file_1.readSettings)(file);
294
+ const current = settings.statusLine;
295
+ const owner = (0, settings_1.ownerOf)(current);
296
+ if (!replace && (0, settings_1.isForeign)(owner)) {
297
+ io.write(`${file} already runs ${owner === 'claude-hud' ? "claude-hud's status line" : 'another status line'}:\n ${current?.command ?? JSON.stringify(current)}\n`);
298
+ if (!(await (0, wizard_1.confirm)(io, 'Replace it? claude-gauge saves it, and claude-gauge uninstall puts it back.', false))) {
299
+ say('Nothing changed.');
300
+ return;
301
+ }
302
+ replace = true;
303
+ }
304
+ const preview = (0, wizard_1.previewer)((0, wizard_1.loadPayload)((0, statusline_1.payloadFile)()));
305
+ const choices = await (0, wizard_1.runWizard)(io, { preview, installed: command === 'configure' ? (0, settings_1.installedSwitches)(settings) : undefined });
306
+ if (!choices) {
307
+ say('Nothing changed.');
308
+ return;
309
+ }
310
+ apply(choices, replace);
311
+ if (command === 'setup')
312
+ await (0, wizard_1.offerStar)(io, { hasGh, star: starWithGh });
313
+ }
314
+ finally {
315
+ io.close();
316
+ }
317
+ }
318
+ async function setup(args) {
319
+ const chosen = args.statusLine !== undefined || args.tokenLine !== undefined;
320
+ if (!chosen && !args.yes)
321
+ return interactive('setup', args.replace);
322
+ const orDefault = (v) => (v === undefined && args.yes ? '' : v);
323
+ apply({ statusLine: orDefault(args.statusLine), tokenLine: orDefault(args.tokenLine) }, args.replace);
324
+ }
325
+ async function configure(args) {
326
+ const file = settingsFile();
327
+ if (!(0, settings_1.installed)((0, settings_file_1.readSettings)(file).settings).any) {
328
+ throw new Error(`claude-gauge is not set up in ${file}. Run claude-gauge setup first.`);
329
+ }
330
+ if (args.statusLine === undefined && args.tokenLine === undefined)
331
+ return interactive('configure', args.replace);
332
+ apply(args, args.replace);
333
+ }
334
+ function uninstall() {
335
+ const file = settingsFile();
336
+ const { settings, text } = (0, settings_file_1.readSettings)(file);
337
+ const saved = savedStatusLine();
338
+ const next = (0, settings_1.plan)(settings, { uninstall: true, previous: saved ?? null });
339
+ // The saved status line goes only once the settings no longer need it, so
340
+ // a failed write leaves it for the next try.
341
+ const settingsBackup = next.changed ? (0, settings_file_1.writeSettings)(file, next.settings, text) : null;
342
+ fs.rmSync(savedStatusLineFile(), { force: true });
343
+ if (!next.changed) {
344
+ say(`claude-gauge is not in ${file}. Nothing to change.`);
345
+ return;
346
+ }
347
+ const restored = next.found === 'claude-gauge' && saved ? `Put back the previous status line: ${saved.command ?? JSON.stringify(saved)}` : undefined;
348
+ say(`Took claude-gauge out of ${file}.`, ...(restored ? [restored] : []), ...(settingsBackup ? [`Backup of the previous settings: ${settingsBackup}`] : []), `The scripts stay in ${stateDir()}. Delete that folder to remove them too.`, 'Start a new Claude Code session to pick up the changes.');
349
+ }
350
+ function update() {
351
+ const where = scripts();
352
+ const { dir } = where;
353
+ if (where.route === 'clone') {
354
+ say(`This claude-gauge is a git clone in ${stateDir()}. Update it with:`, ` git -C ${stateDir()} pull`);
355
+ return;
356
+ }
357
+ if (where.route === 'launcher') {
358
+ say('This claude-gauge is the Claude Code plugin. Update it with /plugin in Claude Code.', `The settings run ${dir}, which runs the newest installed version, so an update needs no setup.`);
359
+ return;
360
+ }
361
+ if (!fs.existsSync(path.join(dir, 'statusline.js'))) {
362
+ throw new Error(`There is no copy of claude-gauge in ${dir} to update. Run claude-gauge setup first.`);
363
+ }
364
+ copyRuntime(dir);
365
+ say(`Updated claude-gauge in ${dir} to ${version()}.`);
366
+ }
367
+ async function main(argv) {
368
+ try {
369
+ // A bare claude-gauge gets the usage alone, as a mistake: exit 2.
370
+ if (!argv.length) {
371
+ process.stderr.write(usage(process.stderr));
372
+ return 2;
373
+ }
374
+ const args = parseArgs(argv);
375
+ if (args.help) {
376
+ process.stdout.write(usage(process.stdout));
377
+ return 0;
378
+ }
379
+ if (!args.command)
380
+ throw new UsageError('Name a command.');
381
+ switch (args.command) {
382
+ case 'setup':
383
+ await setup(args);
384
+ break;
385
+ case 'configure':
386
+ await configure(args);
387
+ break;
388
+ case 'uninstall':
389
+ uninstall();
390
+ break;
391
+ case 'update':
392
+ update();
393
+ break;
394
+ }
395
+ return 0;
396
+ }
397
+ catch (err) {
398
+ if (err instanceof UsageError) {
399
+ process.stderr.write(`claude-gauge: ${err.message}\n\n${usage(process.stderr)}`);
400
+ return 2;
401
+ }
402
+ process.stderr.write(`claude-gauge: ${err.message}\n`);
403
+ return 1;
404
+ }
405
+ }
406
+ main(process.argv.slice(2)).then((code) => {
407
+ process.exitCode = code;
408
+ });