claude-slim 2.11.0 → 2.12.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
@@ -165,6 +165,7 @@ npx claude-slim scan --no-codex # Skip the ~/.codex scan
165
165
  npx claude-slim doctor # Diagnose Node/Claude/session-log readiness
166
166
  npx claude-slim doctor --offline # Same, without the version check (no network)
167
167
  npx claude-slim check-update # Report-only version check
168
+ npx claude-slim update # Run the upgrade for this install method
168
169
  npx claude-slim restore # Undo
169
170
  npx claude-slim report # Show savings from last clean
170
171
  ```
@@ -247,16 +248,12 @@ Token counts come from [js-tiktoken](https://github.com/nicolo-ribaudo/js-tiktok
247
248
 
248
249
  ---
249
250
 
250
- ## v2.11.0 — What's new
251
+ ## v2.12.0 — What's new
251
252
 
252
- Codex gets the same tiered propose-and-choose cleanup Claude Code has, minus the two categories Codex cannot honestly support.
253
+ - **`claude-slim update`** — runs the upgrade command for however this copy was installed, after showing it and asking. `--dry-run` to preview, `--yes` to skip the prompt. Plugin installs get the marketplace refresh first, then the qualified `claude-slim@claude-slim` id, and are reminded that a restart is needed. npx and source checkouts run nothing and say why — npx already resolves the latest every invocation, and pulling your own repository is not claude-slim's to do.
254
+ - **This corrects v2.9.0's reasoning.** That release stopped at detection, arguing updating belonged to the package manager. Half held: claude-slim must not write into a directory `claude plugin` owns. The other half did not — invoking the package manager is something this tool already does during cleanup (`claude plugin disable`), so refusing to here was inconsistent. It still writes nothing itself.
253
255
 
254
- - **Codex cleanup, tiered.** Five of seven categories carry over because they are filesystem facts, not usage inferences: broken symlinks and unfilled templates (Tier 1), install leftovers in `~/.codex/.tmp` (Tier 1 146MB on the dev machine), backup copies and plugin-shadowed duplicates (Tier 2), oversized skills (Tier 3). Codex items join the same numbered list tagged `[codex]`. `--no-codex` opts out.
255
- - **`~/.codex/skills.disabled/`** — moves are reversible through the same `restore`, and manifest entries record which agent they came from.
256
- - **The path guard is now per-agent.** Not "inside any known root" — a Codex issue must not resolve into `~/.claude/` and vice versa, so a tampered manifest cannot cross between agents.
257
- - `unused_skill` and `oversized_memory` remain unavailable for Codex: no invocation record, and no `~/.codex/projects/*/memory/`.
258
-
259
- Tests: 347 → 374 (+27), the guard suite being the point — cross-agent isolation both directions, traversal escapes, and a `~/.claude-backup` sibling a naive `startsWith` would have allowed.
256
+ Tests: 374 392 (+18), pinning the safety propertiesevery argv a fixed literal, no shell metacharacters, executable only ever `claude` or `npm`, and a refusal to run unattended with no TTY and no `--yes`.
260
257
 
261
258
  For older release notes, see [CHANGELOG.md](CHANGELOG.md).
262
259
 
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ import { readManifest } from './manifest.js';
11
11
  import { formatScanSummary, formatReportBox, calculateReport, } from './report.js';
12
12
  import { collectDoctorReport, formatDoctorReport } from './doctor.js';
13
13
  import { checkForUpdate, formatUpdateNotice } from './update-check.js';
14
+ import { confirmDecision, planUpdate, renderStep, runUpdate } from './update-run.js';
14
15
  import { scanCodex } from './codex/index.js';
15
16
  import { formatCodexSummary } from './codex/report.js';
16
17
  import { classifyCodexIssues } from './codex/detectors.js';
@@ -101,6 +102,81 @@ program
101
102
  console.log(`\n \x1b[32m✓\x1b[0m claude-slim ${result.installed} is up to date.\n`);
102
103
  }
103
104
  });
105
+ // --- update ---
106
+ // Detection lives in `check-update`; this runs the command that detection
107
+ // identified. claude-slim still never writes into a package manager's
108
+ // directories itself — it invokes the manager, the same way cleanup already
109
+ // invokes `claude plugin disable`.
110
+ program
111
+ .command('update')
112
+ .description('Run the upgrade command for however this copy was installed')
113
+ .option('--yes', 'Skip the confirmation prompt')
114
+ .option('--dry-run', 'Show the commands without running them')
115
+ .action(async (opts) => {
116
+ const result = await checkForUpdate({ force: true });
117
+ const plan = planUpdate(result.installMethod);
118
+ console.log('');
119
+ console.log(` Installed: ${result.installed} Latest: ${result.latest ?? 'unknown'}`);
120
+ console.log(` Install method: ${result.installMethod}`);
121
+ console.log('');
122
+ if (result.latest === null) {
123
+ console.log(' Could not reach the npm registry, so there is nothing to compare against.');
124
+ console.log('');
125
+ process.exitCode = 1;
126
+ return;
127
+ }
128
+ if (!result.outdated) {
129
+ console.log(' \x1b[32m✓\x1b[0m Already up to date.\n');
130
+ return;
131
+ }
132
+ if (!plan.runnable) {
133
+ console.log(` ${plan.guidance}\n`);
134
+ return;
135
+ }
136
+ console.log(' Will run:');
137
+ for (const step of plan.steps)
138
+ console.log(` ${renderStep(step)}`);
139
+ console.log('');
140
+ if (opts.dryRun) {
141
+ console.log(' \x1b[90mDry run — nothing was executed.\x1b[0m\n');
142
+ return;
143
+ }
144
+ const gate = confirmDecision(opts, Boolean(process.stdin.isTTY));
145
+ if (gate === 'refuse') {
146
+ console.error(' Refusing to run unattended. Re-run with --yes to confirm.\n');
147
+ process.exitCode = 1;
148
+ return;
149
+ }
150
+ if (gate === 'prompt') {
151
+ const answer = await askUser(' Proceed? [y/N] ');
152
+ if (!/^y(es)?$/i.test(answer)) {
153
+ console.log(' Cancelled.\n');
154
+ return;
155
+ }
156
+ console.log('');
157
+ }
158
+ const results = await runUpdate(plan);
159
+ for (const r of results) {
160
+ const mark = r.ok ? '\x1b[32m✓\x1b[0m' : '\x1b[31m✗\x1b[0m';
161
+ console.log(` ${mark} ${renderStep(r.step)}`);
162
+ if (r.output)
163
+ console.log(r.output.split('\n').map((l) => ` ${l}`).join('\n'));
164
+ }
165
+ const failed = results.find((r) => !r.ok);
166
+ console.log('');
167
+ if (failed) {
168
+ console.log(' Update did not complete. Run the commands above manually to see the full error.\n');
169
+ process.exitCode = 1;
170
+ }
171
+ else if (result.installMethod === 'plugin') {
172
+ // The new version is on disk but the running session still holds the old
173
+ // one — without saying so, users update, re-run, and see identical output.
174
+ console.log(' \x1b[33m!\x1b[0m Restart Claude Code for the update to take effect.\n');
175
+ }
176
+ else {
177
+ console.log(' \x1b[32m✓\x1b[0m Updated.\n');
178
+ }
179
+ });
104
180
  // --- clean ---
105
181
  program
106
182
  .command('clean')
@@ -0,0 +1,40 @@
1
+ import type { InstallMethod } from './update-check.js';
2
+ export interface UpdateStep {
3
+ file: string;
4
+ args: string[];
5
+ }
6
+ export interface UpdatePlan {
7
+ /** false when there is nothing to execute — see `guidance`. */
8
+ runnable: boolean;
9
+ steps: UpdateStep[];
10
+ /** Why nothing runs, for the methods where that is the correct answer. */
11
+ guidance?: string;
12
+ }
13
+ export declare function planUpdate(method: InstallMethod): UpdatePlan;
14
+ /** Human-readable rendering of a step, matching what the user would type. */
15
+ export declare function renderStep(step: UpdateStep): string;
16
+ export interface StepResult {
17
+ step: UpdateStep;
18
+ ok: boolean;
19
+ output: string;
20
+ }
21
+ /**
22
+ * Run the plan's steps in order, stopping at the first failure.
23
+ *
24
+ * Sequential rather than parallel, and halting on error, because step 2 depends
25
+ * on step 1 having refreshed the marketplace — running it against a stale
26
+ * manifest would report success while changing nothing.
27
+ */
28
+ export declare function runUpdate(plan: UpdatePlan): Promise<StepResult[]>;
29
+ export type ConfirmDecision = 'run' | 'prompt' | 'refuse';
30
+ /**
31
+ * Decide how to gate an update that will modify the user's install.
32
+ *
33
+ * Refusing when there is no TTY is the important case: piped into a script or a
34
+ * CI job, there is nobody to answer the prompt, and silently proceeding would
35
+ * mean a tool that changes an installation without anyone agreeing to it.
36
+ * `--yes` is how you say so deliberately.
37
+ */
38
+ export declare function confirmDecision(opts: {
39
+ yes?: boolean;
40
+ }, isTTY: boolean): ConfirmDecision;
@@ -0,0 +1,101 @@
1
+ import { execFile } from 'node:child_process';
2
+ // Running the upgrade.
3
+ //
4
+ // v2.9.0 shipped detection only, on the reasoning that updating is the package
5
+ // manager's job. Half of that was right and half was not. Writing into the
6
+ // plugin directory ourselves would indeed corrupt an install — but *invoking*
7
+ // the package manager is something this tool already does elsewhere
8
+ // (`claude plugin disable` during unused-plugin cleanup). Refusing to invoke it
9
+ // here was inconsistent, not principled.
10
+ //
11
+ // So: still never write to those directories directly. Just run the command the
12
+ // user would have typed, after showing it to them.
13
+ //
14
+ // Every argv below is a fixed literal. Nothing from the environment, the
15
+ // manifest, or user input is interpolated, and execFile never routes through a
16
+ // shell — so there is no argument-injection surface to guard.
17
+ const STEP_TIMEOUT_MS = 180_000;
18
+ export function planUpdate(method) {
19
+ switch (method) {
20
+ case 'plugin':
21
+ return {
22
+ runnable: true,
23
+ steps: [
24
+ // Refresh the marketplace first; otherwise `plugin update` compares
25
+ // against a stale manifest and reports "already up to date".
26
+ { file: 'claude', args: ['plugin', 'marketplace', 'update', 'claude-slim'] },
27
+ // The qualified id is required: the bare name fails with
28
+ // `Plugin "claude-slim" not found` when a marketplace shares its name.
29
+ { file: 'claude', args: ['plugin', 'update', 'claude-slim@claude-slim'] },
30
+ ],
31
+ };
32
+ case 'global':
33
+ return {
34
+ runnable: true,
35
+ steps: [{ file: 'npm', args: ['install', '-g', 'claude-slim@latest'] }],
36
+ };
37
+ case 'npx':
38
+ return {
39
+ runnable: false,
40
+ steps: [],
41
+ guidance: 'npx resolves the latest version on every invocation, so there is nothing to update. ' +
42
+ 'If a stale copy is cached, run `npx claude-slim@latest` once to refresh it.',
43
+ };
44
+ case 'source':
45
+ return {
46
+ runnable: false,
47
+ steps: [],
48
+ guidance: 'This is a source checkout. Updating it means pulling your own repository, ' +
49
+ 'which claude-slim will not do on your behalf — run `git pull && npm install` yourself.',
50
+ };
51
+ default:
52
+ return {
53
+ runnable: false,
54
+ steps: [],
55
+ guidance: 'Could not tell how this copy was installed, so no upgrade command can be chosen safely. ' +
56
+ 'Update it the same way you installed it.',
57
+ };
58
+ }
59
+ }
60
+ /** Human-readable rendering of a step, matching what the user would type. */
61
+ export function renderStep(step) {
62
+ return [step.file, ...step.args].join(' ');
63
+ }
64
+ function runStep(step) {
65
+ return new Promise((resolve) => {
66
+ execFile(step.file, step.args, { timeout: STEP_TIMEOUT_MS }, (err, stdout, stderr) => {
67
+ const output = [stdout, stderr].map((s) => s?.trim()).filter(Boolean).join('\n');
68
+ resolve({ step, ok: !err, output: err ? `${err.message}${output ? `\n${output}` : ''}` : output });
69
+ });
70
+ });
71
+ }
72
+ /**
73
+ * Run the plan's steps in order, stopping at the first failure.
74
+ *
75
+ * Sequential rather than parallel, and halting on error, because step 2 depends
76
+ * on step 1 having refreshed the marketplace — running it against a stale
77
+ * manifest would report success while changing nothing.
78
+ */
79
+ export async function runUpdate(plan) {
80
+ const results = [];
81
+ for (const step of plan.steps) {
82
+ const result = await runStep(step);
83
+ results.push(result);
84
+ if (!result.ok)
85
+ break;
86
+ }
87
+ return results;
88
+ }
89
+ /**
90
+ * Decide how to gate an update that will modify the user's install.
91
+ *
92
+ * Refusing when there is no TTY is the important case: piped into a script or a
93
+ * CI job, there is nobody to answer the prompt, and silently proceeding would
94
+ * mean a tool that changes an installation without anyone agreeing to it.
95
+ * `--yes` is how you say so deliberately.
96
+ */
97
+ export function confirmDecision(opts, isTTY) {
98
+ if (opts.yes)
99
+ return 'run';
100
+ return isTTY ? 'prompt' : 'refuse';
101
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-slim",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "Audit and shrink your Claude Code startup context. Measures what every skill, plugin, agent, command, and memory file costs in the system prompt, then reversibly disables the dead weight. Non-destructive scan, tiered proposals, one-command restore — no proxy, no compression.",
5
5
  "type": "module",
6
6
  "bin": {