cc-cream 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -4,6 +4,24 @@ 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
+ ## [Unreleased]
8
+
9
+ ## [0.3.0] — 2026-05-30
10
+
11
+ ### Added
12
+ - **`cc-cream-setup --status` — a read-only footprint report.** Because no Claude Code host removal path drops our `statusLine` or garbage-collects the version cache, users couldn't easily tell whether cc-cream had fully gone away. `--status` reports the whole footprint in one shot: the `statusLine` wiring (flagging a stale/ghost line whose entrypoint is missing), every cached plugin version, the marketplace clone + both registrations, the auto-wire marker, session state, config, and the manual runtime copy — with a "clean slate" verdict when nothing remains and removal guidance when something does (CREAM-zgdcbmfj).
13
+
14
+ ### Fixed
15
+ - **The status bar no longer zombies after the plugin is uninstalled.** No Claude Code host removal path deletes our `statusLine` *or* the version cache: `/plugin uninstall` is partial (it deregisters the plugin but leaves the cache tree and our `statusLine`), so the entrypoint still exists and the `[ -f … ] || exit 0` guard can never fire — the bar kept rendering every session, with no in-product way out (`/cc-cream:uninstall` deregisters with the plugin). The renderer now defends itself: when it detects it's running **from the plugin cache** while cc-cream is **absent from `~/.claude/plugins/installed_plugins.json`**, it exits 0 silently. The check costs one tiny read and runs *only* on the plugin-cache path — manual/npm installs skip it entirely. A corrupt/unreadable registry is treated as "still installed" so a transient glitch can't suppress a live bar (CREAM-uchemxln).
16
+ - **Non-interactive `--force` no longer prints a contradictory "Declined … then replaced" receipt.** The installer's consent path printed the detection-only first plan pass — including a speculative "Declined — your existing statusLine is unchanged." — and then replaced the line anyway. It now resolves consent first and prints a single coherent result (CREAM-hpjebzes).
17
+
18
+ ### Changed
19
+ - **Setup/uninstall copy now matches how the bar actually appears.** The status line shows on the **next message** of a new session — no restart needed; a restart only matters for an already-open session. The installer note, the `SessionStart` hook message, and the uninstall receipt were reworded accordingly (dropping the misleading "Restart Claude Code" framing) (CREAM-wvtiftfw).
20
+ - **`/cc-cream:uninstall` is now self-sufficient.** It auto-cleans the regenerable scratch (the copied runtime and `cc-cream-state.json`) with no prompt — the old interactive artifact prompt was dead code, since both the `!` bang runner and the slash commands run without a TTY. `--purge` additionally removes the user-authored `~/.claude/cc-cream.json`, and `commands/uninstall.md` now forwards `$ARGUMENTS` so `/cc-cream:uninstall --purge` actually reaches the script. The closing receipt enumerates the final state and the leftovers the host *doesn't* clean — the version cache (`rm -rf ~/.claude/plugins/cache/cc-cream`), `/plugin marketplace remove`, the slash commands that linger until restart, and the npm-free cache-path escape hatch (CREAM-lznfgrap, CREAM-wvtiftfw).
21
+
22
+ ### Internal
23
+ - **One-command releases (`npm run release <patch|minor|major>`).** A new `scripts/release.mjs` bumps every version location in lockstep — `package.json`, `package-lock.json`, and `.claude-plugin/plugin.json` — and rolls the CHANGELOG's `[Unreleased]` section into a dated `## [x.y.z]` heading, gates on the test suite, then commits + tags (and pushes + creates the GitHub Release with `--publish`). It removes the hand-syncing every prior release required, where `npm version` touched only the first two and the version-match gate punished the drift. A new CI gate (`features/25`) now also asserts `plugin.json`'s version matches `package.json`, so that manifest can no longer go stale.
24
+
7
25
  ## [0.2.0] — 2026-05-30
8
26
 
9
27
  ### Added
package/README.md CHANGED
@@ -136,36 +136,56 @@ Plugin users — two steps, **in this order** (Claude Code can't clean
136
136
  from the cache):
137
137
  ```
138
138
  /cc-cream:uninstall # 1. removes the statusLine wiring (run this FIRST)
139
- /plugin uninstall cc-cream # 2. drops the plugin from the cache
139
+ /plugin uninstall cc-cream # 2. drops the plugin
140
140
  ```
141
-
142
- > **Order matters.** `/cc-cream:uninstall` lives inside the plugin, so once you
143
- > run `/plugin uninstall` it's gone. The status line itself degrades to nothing
144
- > if the cache is missing (it won't error), but the now-inert `statusLine` block
145
- > lingers in `settings.json`. To clear it after the plugin is already gone, run
146
- > the npm bin (no global install needed):
141
+ `/cc-cream:uninstall` also auto-cleans cc-cream's regenerable scratch (the copied
142
+ runtime and session-state file); add `--purge` (`/cc-cream:uninstall --purge`) to
143
+ also delete your `~/.claude/cc-cream.json` config. The bar disappears on your next
144
+ message restart an already-open session to drop it immediately.
145
+
146
+ > **Order matters but you're covered if you get it wrong.** `/cc-cream:uninstall`
147
+ > lives inside the plugin, so once you run `/plugin uninstall` it's gone. Neither
148
+ > host command clears the `statusLine` block *or* the version cache. The renderer
149
+ > notices when it's running from a cache the host no longer lists as installed and
150
+ > **self-suppresses**, so the bar stops on the next session even though the inert
151
+ > `statusLine` line still lingers in `settings.json`.
152
+ >
153
+ > To clear that leftover line once the plugin is gone, the **guaranteed** route is
154
+ > the copy of the uninstaller still in the cache — npm-free and always present:
155
+ > ```bash
156
+ > node ~/.claude/plugins/cache/cc-cream/cc-cream/<version>/src/install.js --uninstall
157
+ > # add --purge to also remove your config
158
+ > ```
159
+ > The npm bin does the same job, but **not always**: a *freshly published* version
160
+ > is blocked by npm's min-package-age safe-chain guard (it reports "command not
161
+ > found") until it ages in, so use it only if the cache route isn't handy:
147
162
  > ```bash
148
163
  > npx -y -p cc-cream cc-cream-setup --uninstall
149
164
  > ```
150
- > or remove the `statusLine` key from `~/.claude/settings.json` by hand.
165
+ > You can always remove the `statusLine` key from `~/.claude/settings.json` by hand.
151
166
 
152
167
  npm / manual users:
153
168
  ```bash
154
- cc-cream-setup --uninstall # npm (add --purge to also remove runtime + config)
169
+ cc-cream-setup --uninstall # npm (add --purge to also remove the config)
155
170
  node cc-cream/src/install.js --uninstall # manual clone
156
171
  ```
157
172
 
158
173
  Uninstall removes the `statusLine` block **only if it is cc-cream's** — a
159
- statusLine you wired for something else is left untouched. In a terminal it asks
160
- before deleting the copied runtime and session-state files; run **non-interactively**
161
- (as the `/cc-cream:uninstall` slash command does) it leaves those artifacts in
162
- place pass `--purge` to remove them and your `~/.claude/cc-cream.json` config.
163
- Restart Claude Code to clear the bar.
174
+ statusLine you wired for something else is left untouched. It always cleans the
175
+ regenerable scratch (the copied runtime and session state, both recreated on a
176
+ reinstall); `--purge` additionally removes your `~/.claude/cc-cream.json` config.
177
+ The bar clears on your next message (restart an already-open session to drop it now).
164
178
 
165
179
  Likewise, `cc-cream-setup` run non-interactively will overwrite an existing
166
180
  *cc-cream* statusLine but never a foreign one — pass `--force` to replace
167
181
  regardless.
168
182
 
183
+ Not sure what's left behind? `cc-cream-setup --status` prints a read-only
184
+ footprint report — the statusLine wiring, every cached plugin version (the host
185
+ never garbage-collects these), the marketplace clone + registration, the auto-wire
186
+ marker, session state, config, and the manual runtime copy — so you can confirm a
187
+ clean slate or see exactly what to remove.
188
+
169
189
  ## Configuration
170
190
 
171
191
  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.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Claude Code cache/context/cost status-line tool",
5
5
  "directories": {
6
6
  "doc": "docs"
@@ -16,6 +16,7 @@
16
16
  "coverage": "c8 cucumber-js",
17
17
  "watch": "cucumber-js --watch",
18
18
  "hooks": "simple-git-hooks",
19
+ "release": "node scripts/release.mjs",
19
20
  "prepublishOnly": "npm test"
20
21
  },
21
22
  "simple-git-hooks": {
package/src/cc-cream.js CHANGED
@@ -7,6 +7,7 @@ import fs from 'node:fs';
7
7
  import os from 'node:os';
8
8
  import path from 'node:path';
9
9
  import process from 'node:process';
10
+ import { fileURLToPath } from 'node:url';
10
11
  import { loadConfig, readConfigFile } from './config.js';
11
12
  import { buildSegments, render } from './render.js';
12
13
  import {
@@ -106,7 +107,83 @@ function logDebug(env, { data, cfg, now, prevSessionState, sessionId, rawLen, tt
106
107
  ]);
107
108
  }
108
109
 
110
+ // --- Ghost-bar self-defense (CREAM-uchemxln) --------------------------------
111
+ // No Claude Code host removal path deletes our statusLine OR the version cache:
112
+ // `/plugin uninstall` and `/plugin marketplace remove` both leave the cache tree
113
+ // AND the statusLine in settings.json. So a plugin-cache copy of this renderer
114
+ // keeps executing every session after the plugin is gone — a zombie bar the user
115
+ // has no in-product way to stop (`/cc-cream:uninstall` deregisters with the
116
+ // plugin). The shell `[ -f entrypoint ] || exit 0` guard in install.js can't
117
+ // cover this: the cache it checks for is never GC'd, so the file never goes
118
+ // missing. The reliable signal is the host registry, not the filesystem — when we
119
+ // detect we're running FROM the plugin cache, confirm cc-cream is still listed in
120
+ // installed_plugins.json; if it's gone, exit 0 silently.
121
+
122
+ function realpathOr(p) {
123
+ try {
124
+ return fs.realpathSync(p);
125
+ } catch {
126
+ return path.resolve(p);
127
+ }
128
+ }
129
+
130
+ // If `selfPath` lives under `<root>/plugins/cache/<marketplace>/<plugin>/...`,
131
+ // return { pluginsDir, pluginHome }; otherwise null (a manual/dev install, which
132
+ // is never a cache orphan). Both paths are derived from the running location, so
133
+ // the registry we consult is the one that actually governs THIS install — no
134
+ // os.homedir()/CLAUDE_CONFIG_DIR assumption.
135
+ function pluginCacheLocation(selfPath) {
136
+ const segs = realpathOr(selfPath).split(path.sep);
137
+ for (let i = 0; i + 3 < segs.length; i++) {
138
+ if (segs[i] === 'plugins' && segs[i + 1] === 'cache') {
139
+ return {
140
+ pluginsDir: segs.slice(0, i + 1).join(path.sep),
141
+ pluginHome: segs.slice(0, i + 4).join(path.sep),
142
+ };
143
+ }
144
+ }
145
+ return null;
146
+ }
147
+
148
+ function isWithin(parent, child) {
149
+ const rel = path.relative(parent, child);
150
+ return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
151
+ }
152
+
153
+ // True when this renderer is a plugin-cache orphan: running from the cache while
154
+ // cc-cream is absent from the host's installed_plugins.json. Cost is one tiny
155
+ // read, and ONLY on the plugin-cache path — manual/dev installs return early
156
+ // before touching the disk. A missing registry (ENOENT) counts as orphaned; any
157
+ // other read/parse failure is treated as not-orphaned, so a transient glitch can
158
+ // never suppress a legitimately wired bar.
159
+ function isOrphanedPluginRun(selfPath) {
160
+ const loc = pluginCacheLocation(selfPath);
161
+ if (!loc) return false;
162
+ let parsed;
163
+ try {
164
+ parsed = JSON.parse(fs.readFileSync(path.join(loc.pluginsDir, 'installed_plugins.json'), 'utf8'));
165
+ } catch (err) {
166
+ return err?.code === 'ENOENT';
167
+ }
168
+ const plugins = parsed && typeof parsed === 'object' ? parsed.plugins : null;
169
+ if (!plugins || typeof plugins !== 'object') return true;
170
+ const home = realpathOr(loc.pluginHome);
171
+ for (const entries of Object.values(plugins)) {
172
+ if (!Array.isArray(entries)) continue;
173
+ for (const entry of entries) {
174
+ if (entry && typeof entry.installPath === 'string' && isWithin(home, realpathOr(entry.installPath))) {
175
+ return false; // an installed cc-cream entry lives in our cache subtree
176
+ }
177
+ }
178
+ }
179
+ return true;
180
+ }
181
+
109
182
  async function main() {
183
+ // Self-suppress a zombie bar left behind by an uninstalled plugin (before any
184
+ // stdin read — matching the intent of install.js's now-dead `[ -f ]` guard).
185
+ if (isOrphanedPluginRun(fileURLToPath(import.meta.url))) process.exit(0);
186
+
110
187
  const raw = await readStdin();
111
188
  const data = parseSession(raw);
112
189
  const cfg = loadConfig(readConfigFile());
package/src/install.js CHANGED
@@ -22,7 +22,7 @@ import { isEntrypoint } from './utils.js';
22
22
  export { writeFileAtomic } from './settings.js';
23
23
 
24
24
  const TRUST_NOTE =
25
- 'Claude Code must be trusted and possibly restarted for the status line to appear.';
25
+ 'The bar appears on your next message restart only an already-open session, and the workspace must be trusted.';
26
26
 
27
27
  // The statusLine command: a missing-file guard, then exec node on an ABSOLUTE
28
28
  // entrypoint. Both install modes use this one shape — only the entrypoint path
@@ -217,8 +217,13 @@ function ask(question) {
217
217
  }));
218
218
  }
219
219
 
220
- // Remove the cc-cream wiring (and, with consent, its install artifacts). Keeps
221
- // the user's config (~/.claude/cc-cream.json) unless `--purge` is passed.
220
+ // Remove the cc-cream wiring and its throwaway scratch. The copied runtime
221
+ // (~/.claude/cc-cream) and session state (~/.claude/cc-cream-state.json) are
222
+ // regenerable — reinstalling recreates them — so they're always cleaned, no
223
+ // prompt. (The old interactive artifact prompt was dead code: both the `!` bang
224
+ // runner and the slash commands run non-TTY — CREAM-lznfgrap.) `--purge`
225
+ // additionally removes the one file worth keeping by default: the user-authored
226
+ // config (~/.claude/cc-cream.json).
222
227
  async function uninstall({ purge }) {
223
228
  const file = settingsPath();
224
229
  const settings = readSettings(file);
@@ -226,42 +231,42 @@ async function uninstall({ purge }) {
226
231
  for (const m of result.messages) console.log(m);
227
232
  if (result.changed) {
228
233
  writeFileAtomic(file, `${JSON.stringify(result.settings, null, 2)}\n`);
229
- console.log(`\nUpdated ${file}.`);
234
+ console.log(`Updated ${file}.`);
230
235
  }
231
236
 
232
237
  const home = path.join(os.homedir(), '.claude');
233
- const runtimeDir = path.join(home, 'cc-cream');
234
- const stateFile = path.join(home, 'cc-cream-state.json');
235
238
  const configFile = path.join(home, 'cc-cream.json');
236
239
 
237
- const artifacts = [runtimeDir, stateFile].filter((p) => fs.existsSync(p));
238
- if (artifacts.length) {
239
- let remove = purge;
240
- if (!remove && process.stdin.isTTY) {
241
- remove = await ask(`Also delete the copied runtime and session state?\n ${artifacts.join('\n ')}`);
242
- }
243
- if (remove) {
244
- for (const p of artifacts) fs.rmSync(p, { recursive: true, force: true });
245
- console.log('Removed runtime and state files.');
246
- } else if (process.stdin.isTTY) {
247
- console.log('Left runtime and state files in place.');
240
+ // Auto-clean regenerable scratch (not user data — no prompt).
241
+ const scratch = [path.join(home, 'cc-cream'), path.join(home, 'cc-cream-state.json')]
242
+ .filter((p) => fs.existsSync(p));
243
+ for (const p of scratch) fs.rmSync(p, { recursive: true, force: true });
244
+ if (scratch.length) console.log('Removed the copied runtime and session state.');
245
+
246
+ if (fs.existsSync(configFile)) {
247
+ if (purge) {
248
+ fs.rmSync(configFile, { force: true });
249
+ console.log(`Removed your config ${configFile}.`);
248
250
  } else {
249
- // Non-interactive (e.g. run via the /cc-cream:uninstall slash command, which
250
- // has no TTY): never block on a prompt. The statusLine — the thing that
251
- // matters — is already removed; keep the artifacts (deletion is destructive)
252
- // and say how to remove them.
253
- console.log(`Left runtime and session state in place — no terminal to confirm deletion:\n ${artifacts.join('\n ')}`);
254
- console.log('Re-run in a terminal, or pass --purge, to remove them.');
251
+ console.log(`Kept your config ${configFile} (pass --purge to remove it too).`);
255
252
  }
256
253
  }
257
- if (purge && fs.existsSync(configFile)) {
258
- fs.rmSync(configFile, { force: true });
259
- console.log(`Removed config ${configFile}.`);
260
- } else if (fs.existsSync(configFile)) {
261
- console.log(`Kept your config ${configFile} (pass --purge to remove it too).`);
262
- }
263
254
 
264
- console.log('\nRestart Claude Code to drop the bar.');
255
+ printUninstallReceipt();
256
+ }
257
+
258
+ // The closing receipt. No Claude Code host removal path drops our statusLine OR
259
+ // the version cache, so spell out what's gone, what the host leaves behind, and
260
+ // the npm-free escape hatch (the lingering cache always has a working install.js).
261
+ // See project memory cc-cream-plugin-lifecycle-findings.
262
+ function printUninstallReceipt() {
263
+ console.log('\nDone — the bar disappears on your next message (restart an already-open session to drop it now).');
264
+ console.log('The host leaves the rest behind; to fully remove cc-cream:');
265
+ console.log(' • Plugin: /plugin uninstall cc-cream then /plugin marketplace remove cc-cream');
266
+ console.log(' • Version cache (never auto-removed): rm -rf ~/.claude/plugins/cache/cc-cream');
267
+ console.log(' • The /cc-cream:* slash commands linger in this session until you restart Claude Code.');
268
+ console.log('Already removed the plugin? This same uninstall lives in the cache:');
269
+ console.log(' node ~/.claude/plugins/cache/cc-cream/cc-cream/<version>/src/install.js --uninstall [--purge]');
265
270
  }
266
271
 
267
272
  // `cc-cream-setup --check-config`: lint ~/.claude/cc-cream.json against the
@@ -295,8 +300,119 @@ function checkConfigCli() {
295
300
  process.exit(1);
296
301
  }
297
302
 
303
+ function listDirs(dir) {
304
+ try {
305
+ return fs.readdirSync(dir, { withFileTypes: true })
306
+ .filter((e) => e.isDirectory())
307
+ .map((e) => e.name)
308
+ .sort();
309
+ } catch {
310
+ return [];
311
+ }
312
+ }
313
+
314
+ function readJsonSafe(file) {
315
+ try {
316
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
317
+ } catch {
318
+ return null;
319
+ }
320
+ }
321
+
322
+ // `cc-cream-setup --status`: a read-only report of cc-cream's entire on-disk
323
+ // footprint. Because no Claude Code host removal path drops our statusLine or GCs
324
+ // the version cache (project memory cc-cream-plugin-lifecycle-findings), users
325
+ // can't otherwise tell whether cc-cream fully went away — this command answers
326
+ // "clean slate?" in one shot, and points out what the host left behind.
327
+ function statusCli() {
328
+ const home = path.join(os.homedir(), '.claude');
329
+ const plugins = path.join(home, 'plugins');
330
+ const items = [];
331
+ const add = (label, present, detail) => items.push({ label, present, detail });
332
+
333
+ // statusLine wiring
334
+ const { state, value } = readSettingsFile(settingsPath());
335
+ if (!isSafeToWrite(state)) {
336
+ add('statusLine wiring', false, `settings.json unreadable (${state}) — not inspected`);
337
+ } else if (isCcCreamStatusLine(value.statusLine)) {
338
+ const ep = (value.statusLine.command.match(/\[ -f "([^"]+)"/) || [])[1] || '';
339
+ const ok = ep && fs.existsSync(ep);
340
+ add('statusLine wiring', true, ok
341
+ ? `belongs to cc-cream, pinned to ${ep}`
342
+ : `belongs to cc-cream, pinned to ${ep || '(unknown)'} — entrypoint MISSING (stale/ghost wiring)`);
343
+ } else if (value.statusLine) {
344
+ add('statusLine wiring', false, 'present but not cc-cream’s (left untouched)');
345
+ } else {
346
+ add('statusLine wiring', false, 'none');
347
+ }
348
+
349
+ // plugin cache versions (the host never GCs these)
350
+ const versions = listDirs(path.join(plugins, 'cache', 'cc-cream', 'cc-cream'));
351
+ add('plugin cache', versions.length > 0, versions.length
352
+ ? `${versions.length} version(s) [${versions.join(', ')}] — host never GCs these; rm to reclaim`
353
+ : 'none');
354
+
355
+ // marketplace clone
356
+ const clone = path.join(plugins, 'marketplaces', 'cc-cream');
357
+ add('marketplace clone', fs.existsSync(clone), fs.existsSync(clone) ? clone : 'none');
358
+
359
+ // registrations
360
+ const installed = readJsonSafe(path.join(plugins, 'installed_plugins.json'));
361
+ const isRegistered = !!installed?.plugins && typeof installed.plugins === 'object'
362
+ && Object.keys(installed.plugins).some((k) => k.startsWith('cc-cream@'));
363
+ add('plugin registration', isRegistered, isRegistered
364
+ ? 'listed in installed_plugins.json'
365
+ : 'not listed in installed_plugins.json');
366
+
367
+ const known = readJsonSafe(path.join(plugins, 'known_marketplaces.json'));
368
+ const knownMkt = !!known && typeof known === 'object' && Object.hasOwn(known, 'cc-cream');
369
+ add('marketplace registration', knownMkt, knownMkt
370
+ ? 'listed in known_marketplaces.json'
371
+ : 'not listed in known_marketplaces.json');
372
+
373
+ // auto-wire marker (plugin data dir, falling back to the config dir)
374
+ const markerDir = process.env.CLAUDE_PLUGIN_DATA || path.join(plugins, 'data', 'cc-cream-cc-cream');
375
+ const marker = [path.join(markerDir, 'cc-cream-autowire-done'), path.join(home, 'cc-cream-autowire-done')]
376
+ .find((p) => fs.existsSync(p));
377
+ add('auto-wire marker', !!marker, marker || 'none');
378
+
379
+ // session state
380
+ const stateFile = path.join(home, 'cc-cream-state.json');
381
+ if (fs.existsSync(stateFile)) {
382
+ const obj = readJsonSafe(stateFile);
383
+ const n = obj && typeof obj === 'object' ? Object.keys(obj).length : '?';
384
+ add('session state', true, `${n} session(s) in cc-cream-state.json`);
385
+ } else {
386
+ add('session state', false, 'none');
387
+ }
388
+
389
+ // config
390
+ const configFile = path.join(home, 'cc-cream.json');
391
+ add('config', fs.existsSync(configFile), fs.existsSync(configFile) ? configFile : 'none (using defaults)');
392
+
393
+ // manual runtime copy
394
+ const runtimeDir = path.join(home, 'cc-cream');
395
+ add('manual runtime copy', fs.existsSync(runtimeDir), fs.existsSync(runtimeDir) ? runtimeDir : 'none');
396
+
397
+ console.log('cc-cream footprint:');
398
+ for (const it of items) console.log(` [${it.present ? 'x' : ' '}] ${it.label}: ${it.detail}`);
399
+
400
+ if (items.every((i) => !i.present)) {
401
+ console.log('\nClean slate — no cc-cream footprint found.');
402
+ return;
403
+ }
404
+ console.log(`\n${items.filter((i) => i.present).length} component(s) present. To remove everything:`);
405
+ console.log(' /cc-cream:uninstall (or the cache-path install.js --uninstall) clears the statusLine + scratch;');
406
+ console.log(' then /plugin uninstall cc-cream + /plugin marketplace remove cc-cream;');
407
+ console.log(' then rm -rf ~/.claude/plugins/cache/cc-cream (the host never removes it).');
408
+ }
409
+
298
410
  async function main() {
299
411
  const args = process.argv.slice(2);
412
+ if (args.includes('--status')) {
413
+ statusCli();
414
+ return;
415
+ }
300
416
  if (args.includes('--check-config')) {
301
417
  checkConfigCli();
302
418
  return;
@@ -343,12 +459,15 @@ async function main() {
343
459
  }
344
460
 
345
461
  let result = plan(settings, planOpts);
346
- // If a replace needs consent, ask now and re-plan with the answer.
462
+ // A foreign statusLine needs consent before we replace it. This first plan()
463
+ // pass is detection only — do NOT print its messages: they include a
464
+ // speculative "Declined …" (consent was absent) that would contradict a
465
+ // subsequent --force replace. Resolve consent, re-plan, then print the single
466
+ // coherent second-pass result below (CREAM-hpjebzes).
347
467
  if (!result.changed && result.needsConsent) {
348
- for (const m of result.messages) console.log(m);
349
468
  let yes;
350
469
  if (process.stdin.isTTY) {
351
- yes = await ask('Replace it with cc-cream?');
470
+ yes = await ask('Replace your existing statusLine with cc-cream?');
352
471
  } else {
353
472
  // Non-interactive (e.g. run via the /cc-cream:setup slash command, which has
354
473
  // no TTY): never block on a prompt. Safe to overwrite our OWN wiring (an
@@ -356,7 +475,7 @@ async function main() {
356
475
  // statusLine without a terminal or an explicit --force.
357
476
  yes = force || isCcCreamStatusLine(settings.statusLine);
358
477
  console.log(yes
359
- ? 'Non-interactive: replacing the existing cc-cream statusLine.'
478
+ ? 'Non-interactive: replacing the existing statusLine with cc-cream’s.'
360
479
  : 'Non-interactive: left your existing statusLine unchanged. Re-run in a terminal, or pass --force, to replace it.');
361
480
  }
362
481
  result = plan(settings, { ...planOpts, consent: yes });