cc-cream 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,28 @@ All notable changes to cc-cream are documented here. Format follows
4
4
  [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versions follow
5
5
  [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
6
 
7
+ ## [0.1.2] — 2026-05-29
8
+
9
+ ### Changed
10
+ - **No install-time lifecycle scripts in the published package.** The git-hook
11
+ registration moved off the `prepare` lifecycle to an opt-in `npm run hooks`,
12
+ so `npm install`/`npx cc-cream` runs nothing automatically. Improves the
13
+ supply-chain posture (and Socket score) with no change to the runtime.
14
+
15
+ ## [0.1.1] — 2026-05-29
16
+
17
+ ### Added
18
+ - **Uninstall** — `node src/install.js --uninstall` (and the `/cc-cream:uninstall`
19
+ plugin command) removes cc-cream's `statusLine` block, but only when it is
20
+ cc-cream's; a foreign statusLine is left untouched. Offers to delete the copied
21
+ runtime and session-state files, and keeps `~/.claude/cc-cream.json` unless
22
+ `--purge` is passed.
23
+
24
+ ### Fixed
25
+ - **Never overwrite a malformed `settings.json`** — the installer now refuses to
26
+ write when `settings.json` exists but fails to parse (or is not a JSON object),
27
+ instead of silently starting fresh and erasing the user's other settings.
28
+
7
29
  ## [0.1.0] — 2026-05-29
8
30
 
9
31
  Initial public release. cc-cream reads the JSON Claude Code pipes to its status
@@ -37,4 +59,6 @@ line and prints a colored ≤3-row bar — zero tokens, the model never sees it.
37
59
  - Supports **macOS and Linux**; Windows is a planned fast-follow.
38
60
  - Requires Claude Code **2.1.132+** (`effort` / `thinking` need 2.1.145+).
39
61
 
62
+ [0.1.2]: https://github.com/bart-turczynski/cc-cream/compare/v0.1.1...v0.1.2
63
+ [0.1.1]: https://github.com/bart-turczynski/cc-cream/compare/v0.1.0...v0.1.1
40
64
  [0.1.0]: https://github.com/bart-turczynski/cc-cream/releases/tag/v0.1.0
package/README.md CHANGED
@@ -109,6 +109,26 @@ The installer:
109
109
  After install, Claude Code must be **trusted** for the directory (if prompted),
110
110
  and you may need to **restart** it for the bar to appear.
111
111
 
112
+ ### Uninstall
113
+
114
+ Plugin users:
115
+ ```
116
+ /cc-cream:uninstall
117
+ /plugin uninstall cc-cream
118
+ ```
119
+
120
+ npm / manual users:
121
+ ```bash
122
+ node $(npm root -g)/cc-cream/src/install.js --uninstall # npm
123
+ node cc-cream/src/install.js --uninstall # manual clone
124
+ ```
125
+
126
+ Uninstall removes the `statusLine` block **only if it is cc-cream's** — a
127
+ statusLine you wired for something else is left untouched. It then offers to
128
+ delete the copied runtime and session-state files, and **keeps your
129
+ `~/.claude/cc-cream.json` config** unless you add `--purge`. Restart Claude Code
130
+ to clear the bar.
131
+
112
132
  ## Configuration
113
133
 
114
134
  Every display decision is read from `~/.claude/cc-cream.json`. Edit it by hand
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-cream",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Claude Code cache/context/cost status-line tool",
5
5
  "directories": {
6
6
  "doc": "docs"
@@ -14,7 +14,7 @@
14
14
  "test:manual": "cucumber-js --profile manual",
15
15
  "coverage": "c8 cucumber-js",
16
16
  "watch": "cucumber-js --watch",
17
- "prepare": "simple-git-hooks",
17
+ "hooks": "simple-git-hooks",
18
18
  "prepublishOnly": "npm test"
19
19
  },
20
20
  "simple-git-hooks": {
package/src/install.js CHANGED
@@ -42,6 +42,44 @@ function isInstalled(existing, command) {
42
42
  );
43
43
  }
44
44
 
45
+ // True if an existing statusLine belongs to cc-cream under ANY install strategy
46
+ // (dev repo, copied home runtime, or the plugin cache-glob) — every command
47
+ // references the cc-cream entrypoint. Used by uninstall so we never touch a
48
+ // statusLine the user wired for something else.
49
+ function isCcCreamStatusLine(existing) {
50
+ return (
51
+ !!existing &&
52
+ typeof existing === 'object' &&
53
+ existing.type === 'command' &&
54
+ typeof existing.command === 'string' &&
55
+ existing.command.includes('cc-cream')
56
+ );
57
+ }
58
+
59
+ // Decide what an uninstall should do. Pure: returns { settings, changed, messages }.
60
+ // Removes ONLY a cc-cream statusLine; a foreign statusLine is left verbatim.
61
+ export function planUninstall(settings) {
62
+ const s = settings && typeof settings === 'object' ? settings : {};
63
+ const existing = s.statusLine;
64
+ const messages = [];
65
+
66
+ if (!isCcCreamStatusLine(existing)) {
67
+ if (existing && typeof existing === 'object') {
68
+ messages.push(
69
+ `Your statusLine is not cc-cream's — leaving it untouched:\n ${JSON.stringify(existing)}`,
70
+ );
71
+ } else {
72
+ messages.push('No cc-cream statusLine found in settings.json — nothing to remove.');
73
+ }
74
+ return { settings: s, changed: false, messages };
75
+ }
76
+
77
+ messages.push(`Removing cc-cream statusLine:\n ${JSON.stringify(existing)}`);
78
+ const next = { ...s };
79
+ delete next.statusLine;
80
+ return { settings: next, changed: true, messages };
81
+ }
82
+
45
83
  // Decide what to do. Returns { settings, changed, messages, needsConsent }.
46
84
  // `consent` is the user's yes/no when an existing statusLine must be replaced.
47
85
  //
@@ -96,6 +134,32 @@ function destinationPath() {
96
134
  return path.join(os.homedir(), '.claude', 'cc-cream', 'cc-cream.js');
97
135
  }
98
136
 
137
+ // Read settings.json safely. A MISSING or empty file -> {} (fresh start, nothing
138
+ // to lose). A file that exists with content but fails to parse, or parses to a
139
+ // non-object, is REFUSED: we exit rather than overwrite and erase the user's
140
+ // other settings (permissions, hooks, plugins...). This guards the one path
141
+ // where a blind write would be destructive.
142
+ function readSettings(file) {
143
+ if (!fs.existsSync(file)) return {};
144
+ const raw = fs.readFileSync(file, 'utf8');
145
+ if (raw.trim() === '') return {};
146
+ let parsed;
147
+ try {
148
+ parsed = JSON.parse(raw);
149
+ } catch {
150
+ console.error(`Error: ${file} is not valid JSON.`);
151
+ console.error('Refusing to write it — that would erase your other settings.');
152
+ console.error('Fix the JSON (or move the file aside) and re-run.');
153
+ process.exit(1);
154
+ }
155
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
156
+ console.error(`Error: ${file} does not contain a JSON object.`);
157
+ console.error('Refusing to overwrite it. Move it aside and re-run if intended.');
158
+ process.exit(1);
159
+ }
160
+ return parsed;
161
+ }
162
+
99
163
  function runtimeFiles(sourceFile) {
100
164
  const sourceDir = path.dirname(sourceFile);
101
165
  return fs.readdirSync(sourceDir)
@@ -143,19 +207,55 @@ function ask(question) {
143
207
  }));
144
208
  }
145
209
 
210
+ // Remove the cc-cream wiring (and, with consent, its install artifacts). Keeps
211
+ // the user's config (~/.claude/cc-cream.json) unless `--purge` is passed.
212
+ async function uninstall({ purge }) {
213
+ const file = settingsPath();
214
+ const settings = readSettings(file);
215
+ const result = planUninstall(settings);
216
+ for (const m of result.messages) console.log(m);
217
+ if (result.changed) {
218
+ fs.writeFileSync(file, `${JSON.stringify(result.settings, null, 2)}\n`);
219
+ console.log(`\nUpdated ${file}.`);
220
+ }
221
+
222
+ const home = path.join(os.homedir(), '.claude');
223
+ const runtimeDir = path.join(home, 'cc-cream');
224
+ const stateFile = path.join(home, 'cc-cream-state.json');
225
+ const configFile = path.join(home, 'cc-cream.json');
226
+
227
+ const artifacts = [runtimeDir, stateFile].filter((p) => fs.existsSync(p));
228
+ if (artifacts.length) {
229
+ const remove = purge || (await ask(`Also delete the copied runtime and session state?\n ${artifacts.join('\n ')}`));
230
+ if (remove) {
231
+ for (const p of artifacts) fs.rmSync(p, { recursive: true, force: true });
232
+ console.log('Removed runtime and state files.');
233
+ } else {
234
+ console.log('Left runtime and state files in place.');
235
+ }
236
+ }
237
+ if (purge && fs.existsSync(configFile)) {
238
+ fs.rmSync(configFile, { force: true });
239
+ console.log(`Removed config ${configFile}.`);
240
+ } else if (fs.existsSync(configFile)) {
241
+ console.log(`Kept your config ${configFile} (pass --purge to remove it too).`);
242
+ }
243
+
244
+ console.log('\nRestart Claude Code to drop the bar.');
245
+ }
246
+
146
247
  async function main() {
147
248
  const args = process.argv.slice(2);
249
+ if (args.includes('--uninstall')) {
250
+ await uninstall({ purge: args.includes('--purge') });
251
+ return;
252
+ }
148
253
  const plugin = args.includes('--plugin');
149
254
  // First non-flag arg is an optional local source path (manual mode only).
150
255
  const positional = args.filter((a) => !a.startsWith('--'));
151
256
 
152
257
  const file = settingsPath();
153
- let settings = {};
154
- try {
155
- settings = JSON.parse(fs.readFileSync(file, 'utf8')) || {};
156
- } catch {
157
- settings = {}; // missing or malformed -> start fresh, don't clobber blindly below
158
- }
258
+ const settings = readSettings(file);
159
259
 
160
260
  // planOpts holds whatever the chosen strategy needs to build its command.
161
261
  let planOpts;