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 +18 -0
- package/README.md +34 -14
- package/package.json +2 -1
- package/src/cc-cream.js +77 -0
- package/src/install.js +153 -34
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
|
|
139
|
+
/plugin uninstall cc-cream # 2. drops the plugin
|
|
140
140
|
```
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
>
|
|
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
|
-
>
|
|
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
|
|
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.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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.
|
|
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
|
-
'
|
|
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
|
|
221
|
-
//
|
|
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(
|
|
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
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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 });
|