canary-test-cli 6.8.0 → 7.0.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.
Files changed (36) hide show
  1. package/dist/engine/analysis/cli.js +155 -45
  2. package/dist/engine/analysis/engine.js +9 -9
  3. package/dist/engine/analysis/reports.js +0 -0
  4. package/dist/engine/cli-commands.js +2 -2
  5. package/dist/engine/core/migrator.js +142 -31
  6. package/dist/engine/core/static-linter.js +2 -2
  7. package/dist/engine/core/workspace-detect.js +0 -0
  8. package/dist/engine/guardian/adjudication.js +1 -1
  9. package/dist/engine/guardian/agent-tier.js +3 -3
  10. package/dist/engine/guardian/coverage.js +15 -1397
  11. package/dist/engine/guardian/diff-coverage/formats/cobertura.js +130 -0
  12. package/dist/engine/guardian/diff-coverage/formats/coverage-json-lint.js +197 -0
  13. package/dist/engine/guardian/diff-coverage/formats/coverage-json.js +107 -0
  14. package/dist/engine/guardian/diff-coverage/formats/xml.js +151 -0
  15. package/dist/engine/guardian/diff-coverage/graph-tier.js +223 -0
  16. package/dist/engine/guardian/diff-coverage/heuristic-tier.js +150 -0
  17. package/dist/engine/guardian/diff-coverage/orchestrator.js +125 -0
  18. package/dist/engine/guardian/diff-coverage/paths.js +164 -0
  19. package/dist/engine/guardian/diff-coverage/report-tier.js +153 -0
  20. package/dist/engine/guardian/diff-coverage/type-only.js +150 -0
  21. package/dist/engine/guardian/diff-coverage/types.js +115 -0
  22. package/dist/engine/guardian/pr-check.js +22 -7
  23. package/dist/engine-checks.d.ts +15 -0
  24. package/dist/engine-checks.js +92 -1
  25. package/dist/overlay-commands.d.ts +12 -1
  26. package/dist/overlay-commands.js +28 -2
  27. package/dist/router.js +17 -5
  28. package/dist/uninstall-render.d.ts +11 -0
  29. package/dist/uninstall-render.js +60 -0
  30. package/dist/uninstall-scan.d.ts +14 -0
  31. package/dist/uninstall-scan.js +273 -0
  32. package/dist/uninstall-types.d.ts +46 -0
  33. package/dist/uninstall-types.js +91 -0
  34. package/dist/uninstall.d.ts +13 -0
  35. package/dist/uninstall.js +174 -0
  36. package/package.json +1 -1
@@ -446,7 +446,10 @@ function updateOne(o, git, out, err) {
446
446
  }
447
447
  if (clean === 'dirty') {
448
448
  err.write(`overlay "${o.name}": local modifications in ${o.path} — refusing to update. ` +
449
- `Commit/stash them, or 'canary overlay remove ${o.name}' and re-add.\n`);
449
+ `Commit/stash them, or discard them with ` +
450
+ `'canary overlay remove ${o.name} --force' and re-add. ` +
451
+ `(Plain 'overlay remove' refuses on a dirty clone too, so it will not ` +
452
+ `throw the edits away by accident.)\n`);
450
453
  return 1;
451
454
  }
452
455
  if (o.ref) {
@@ -520,8 +523,15 @@ function update(name, deps = {}) {
520
523
  /**
521
524
  * `canary overlay remove <name>` — deregister an overlay and delete its clone.
522
525
  * Unknown name is an error; the registry is left unchanged in that case.
526
+ *
527
+ * Removal is destructive and irreversible, so it takes the same posture as
528
+ * `overlay update` (#675): a clone with local modifications — or one whose git
529
+ * status cannot be read, which is no safer to delete blind — is refused, and
530
+ * `--force` is the explicit opt-in. A clone that is already gone is not a
531
+ * refusal: there is nothing left to lose, so the stale registry row is dropped.
523
532
  */
524
- function remove(name, deps = {}) {
533
+ function remove(name, deps = {}, opts = {}) {
534
+ const git = deps.git ?? defaultGit;
525
535
  const homeDir = deps.homeDir ?? os.homedir();
526
536
  const out = deps.out ?? process.stdout;
527
537
  const err = deps.err ?? process.stderr;
@@ -538,6 +548,22 @@ function remove(name, deps = {}) {
538
548
  err.write(`canary overlay remove: no overlay named "${name}".\n`);
539
549
  return 1;
540
550
  }
551
+ if (!opts.force && fs.existsSync(entry.path)) {
552
+ const clean = workingTreeStatus(entry.path, git);
553
+ if (clean === 'unreadable') {
554
+ err.write(`canary overlay remove: cannot read git status at ${entry.path} — ` +
555
+ `refusing to delete it. Inspect the directory, then re-run with ` +
556
+ `--force to remove it anyway.\n`);
557
+ return 1;
558
+ }
559
+ if (clean === 'dirty') {
560
+ err.write(`canary overlay remove: local modifications in ${entry.path} — ` +
561
+ `refusing to delete them. Commit or discard the changes ` +
562
+ `(git -C ${entry.path} status), then re-run; or re-run with ` +
563
+ `--force to delete the clone and the changes.\n`);
564
+ return 1;
565
+ }
566
+ }
541
567
  fs.rmSync(entry.path, { recursive: true, force: true });
542
568
  const { registry: next } = registry.remove(reg, name);
543
569
  registry.write(next, homeDir);
package/dist/router.js CHANGED
@@ -48,8 +48,13 @@ exports.route = route;
48
48
  */
49
49
  const overlay = __importStar(require("./overlay-commands.js"));
50
50
  const doctor_js_1 = require("./doctor.js");
51
+ const uninstall_js_1 = require("./uninstall.js");
51
52
  /** Subcommands handled in TypeScript rather than forwarded to the binary. */
52
- exports.TS_COMMANDS = ['overlay', 'doctor'];
53
+ exports.TS_COMMANDS = [
54
+ 'overlay',
55
+ 'doctor',
56
+ 'uninstall',
57
+ ];
53
58
  /** True when `argv` (process.argv.slice(2)) targets a TS-handled command. */
54
59
  function isTsCommand(argv) {
55
60
  return argv.length > 0 && exports.TS_COMMANDS.includes(argv[0]);
@@ -88,7 +93,7 @@ function refFrom(flags) {
88
93
  return typeof ref === 'string' ? ref : null;
89
94
  }
90
95
  const OVERLAY_USAGE = 'usage: canary overlay <add|list|lint|update|remove> [args]\n' +
91
- ' add <source> [--ref <tag>] list [--conflicts] lint <name|path> [--json] update [name] remove <name>\n';
96
+ ' add <source> [--ref <tag>] list [--conflicts] lint <name|path> [--json] update [name] remove <name> [--force]\n';
92
97
  /** Overlay subcommand handlers, keyed by name. Each returns a process exit code. */
93
98
  const OVERLAY_SUBCOMMANDS = {
94
99
  add: ({ positionals, flags, deps, err }) => {
@@ -103,12 +108,16 @@ const OVERLAY_SUBCOMMANDS = {
103
108
  list: ({ flags, deps }) => overlay.list(deps, { conflicts: flags.conflicts === true }),
104
109
  lint: ({ positionals, flags, deps }) => overlay.lint(positionals[0], deps, { json: flags.json === true }),
105
110
  update: ({ positionals, deps }) => overlay.update(positionals[0] ?? null, deps),
106
- remove: ({ positionals, deps, err }) => {
111
+ remove: ({ positionals, flags, deps, err }) => {
107
112
  if (positionals.length < 1) {
108
- err.write('usage: canary overlay remove <name>\n');
113
+ err.write('usage: canary overlay remove <name> [--force]\n');
109
114
  return 1;
110
115
  }
111
- return overlay.remove(positionals[0], deps);
116
+ // `--force` deletes a clone with local modifications (#675); without it,
117
+ // removal refuses rather than destroying uncommitted work.
118
+ return overlay.remove(positionals[0], deps, {
119
+ force: flags.force === true,
120
+ });
112
121
  },
113
122
  };
114
123
  function runOverlay(args, deps) {
@@ -137,5 +146,8 @@ function route(argv, deps = {}) {
137
146
  if (argv[0] === 'doctor') {
138
147
  return (0, doctor_js_1.runDoctor)(argv.slice(1), deps);
139
148
  }
149
+ if (argv[0] === 'uninstall') {
150
+ return (0, uninstall_js_1.run)(argv.slice(1), deps);
151
+ }
140
152
  return null;
141
153
  }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Rendering half of `canary uninstall`. Groups artifacts by disposition and
3
+ * always states the denominator: a scope with nothing in it says so rather
4
+ * than printing an empty list that reads as "all clean".
5
+ */
6
+ import type { Artifact, Scope } from './uninstall-types.js';
7
+ export declare function render(artifacts: Artifact[], opts: {
8
+ scope: Scope;
9
+ apply: boolean;
10
+ failed?: Artifact[];
11
+ }): string;
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.render = render;
4
+ function human(bytes) {
5
+ if (bytes === undefined || bytes === 0)
6
+ return '';
7
+ const units = ['B', 'K', 'M', 'G'];
8
+ let n = bytes;
9
+ let i = 0;
10
+ while (n >= 1024 && i < units.length - 1) {
11
+ n /= 1024;
12
+ i += 1;
13
+ }
14
+ return `${n.toFixed(n < 10 && i > 0 ? 1 : 0)}${units[i]}`;
15
+ }
16
+ function render(artifacts, opts) {
17
+ const lines = [];
18
+ const mode = opts.apply ? '(apply)' : '(dry run)';
19
+ lines.push(`canary uninstall --${opts.scope} ${mode}`, '');
20
+ // `disposition` records what the scan decided, not what the removal achieved.
21
+ // A failed rmSync leaves the artifact `removable`, so rendering straight from
22
+ // the disposition would report it as removed — a clean-looking summary over
23
+ // work that did not happen.
24
+ const failedSet = new Set(opts.failed ?? []);
25
+ const removable = artifacts.filter((a) => a.disposition === 'removable' && !failedSet.has(a));
26
+ const failed = artifacts.filter((a) => failedSet.has(a));
27
+ const blocked = artifacts.filter((a) => a.disposition === 'blocked');
28
+ const manual = artifacts.filter((a) => a.disposition === 'manual');
29
+ const section = (title, rows) => {
30
+ if (rows.length === 0)
31
+ return;
32
+ lines.push(` ${title} (${rows.length})`);
33
+ for (const a of rows) {
34
+ const size = human(a.sizeBytes);
35
+ const tail = a.reason ? ` — ${a.reason}` : '';
36
+ lines.push(` ${a.label}${size ? ` ${size}` : ''}${a.path ? `\n ${a.path}` : ''}${tail}`);
37
+ }
38
+ lines.push('');
39
+ };
40
+ section(opts.apply ? 'Removed' : 'Removable', removable);
41
+ section('FAILED to remove', failed);
42
+ section('Skipped', blocked);
43
+ section('You must do these yourself', manual);
44
+ // A zero denominator is an abstention, not a pass: say so explicitly rather
45
+ // than printing an empty list that reads as "all clean".
46
+ if (artifacts.length === 0) {
47
+ lines.push(' Nothing found for this scope — nothing to remove.', '');
48
+ return lines.join('\n');
49
+ }
50
+ if (removable.length === 0) {
51
+ lines.push(' Nothing removable for this scope.');
52
+ }
53
+ const summary = ` ${removable.length} ${opts.apply ? 'removed' : 'removable'}, ` +
54
+ (failed.length > 0 ? `${failed.length} FAILED, ` : '') +
55
+ `${blocked.length} skipped, ${manual.length} manual.`;
56
+ lines.push(opts.apply
57
+ ? summary
58
+ : `${summary} Nothing was removed — re-run with --apply.`);
59
+ return lines.join('\n');
60
+ }
@@ -0,0 +1,14 @@
1
+ import { type Artifact, type EnumerateOptions, type UninstallDeps } from './uninstall-types.js';
2
+ export declare function enumerate(opts: EnumerateOptions, deps?: UninstallDeps): Artifact[];
3
+ export interface RemovalOutcome {
4
+ removed: Artifact[];
5
+ failed: Array<{
6
+ artifact: Artifact;
7
+ error: string;
8
+ }>;
9
+ }
10
+ /**
11
+ * Remove `.canary/` while preserving the quarantine ledger when it was not
12
+ * opted into. Deleting the directory wholesale would take the ledger with it,
13
+ * silently undoing the protection the `blocked` disposition just promised.
14
+ */
@@ -0,0 +1,273 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.enumerate = enumerate;
37
+ /**
38
+ * Enumeration half of `canary uninstall` — walks the install footprint and
39
+ * classifies every artifact it finds. Pure discovery: nothing here writes or
40
+ * deletes, so the dry-run and `--apply` paths read from one source of truth.
41
+ */
42
+ const fs = __importStar(require("node:fs"));
43
+ const os = __importStar(require("node:os"));
44
+ const path = __importStar(require("node:path"));
45
+ const registry = __importStar(require("./overlays-registry.js"));
46
+ const overlay_commands_js_1 = require("./overlay-commands.js");
47
+ const uninstall_types_js_1 = require("./uninstall-types.js");
48
+ /**
49
+ * Every install path Claude Code currently considers live.
50
+ *
51
+ * Read from `installPath`, NEVER inferred by sorting version numbers: a real
52
+ * machine had 4.0.0, 6.3.0, 6.4.0, 6.5.0, 6.6.0 and 6.7.1 cached with **6.4.0**
53
+ * registered, so a highest-version heuristic would delete the live install and
54
+ * leave five orphans. Every entry counts (a plugin may be registered at more
55
+ * than one scope), so all of them are protected.
56
+ */
57
+ function registeredInstallPaths(pluginsDir) {
58
+ const manifest = (0, uninstall_types_js_1.readJson)(path.join(pluginsDir, 'installed_plugins.json'));
59
+ const entries = manifest?.plugins?.[uninstall_types_js_1.PLUGIN_KEY];
60
+ const out = new Set();
61
+ if (!Array.isArray(entries))
62
+ return out;
63
+ for (const e of entries) {
64
+ const p = e?.installPath;
65
+ if (typeof p === 'string' && p.length > 0)
66
+ out.add(path.resolve(p));
67
+ }
68
+ return out;
69
+ }
70
+ function enumerateGlobal(opts, deps) {
71
+ const home = deps.homeDir ?? os.homedir();
72
+ const git = deps.git ?? uninstall_types_js_1.defaultGit;
73
+ const found = [];
74
+ const pluginsDir = path.join(home, '.claude', 'plugins');
75
+ const cacheRoot = path.join(pluginsDir, 'cache', uninstall_types_js_1.MARKETPLACE, 'canary');
76
+ const registered = registeredInstallPaths(pluginsDir);
77
+ let versions = [];
78
+ try {
79
+ versions = fs
80
+ .readdirSync(cacheRoot, { withFileTypes: true })
81
+ .filter((d) => d.isDirectory())
82
+ .map((d) => d.name);
83
+ }
84
+ catch {
85
+ versions = [];
86
+ }
87
+ for (const v of versions.sort()) {
88
+ const dir = path.join(cacheRoot, v);
89
+ const isLive = registered.has(path.resolve(dir));
90
+ found.push({
91
+ scope: 'global',
92
+ kind: 'plugin-cache',
93
+ label: `plugin cache ${v}`,
94
+ path: dir,
95
+ disposition: isLive ? 'blocked' : 'removable',
96
+ reason: isLive
97
+ ? 'currently registered — run `/plugin uninstall` first, then re-run'
98
+ : undefined,
99
+ sizeBytes: (0, uninstall_types_js_1.dirSize)(dir),
100
+ });
101
+ }
102
+ if (registered.size > 0) {
103
+ found.push({
104
+ scope: 'global',
105
+ kind: 'plugin-registration',
106
+ label: 'Claude Code plugin registration',
107
+ path: path.join(pluginsDir, 'installed_plugins.json'),
108
+ disposition: 'manual',
109
+ reason: `/plugin uninstall ${uninstall_types_js_1.PLUGIN_KEY}`,
110
+ });
111
+ }
112
+ const marketplace = path.join(pluginsDir, 'marketplaces', uninstall_types_js_1.MARKETPLACE);
113
+ if (fs.existsSync(marketplace)) {
114
+ found.push({
115
+ scope: 'global',
116
+ kind: 'marketplace',
117
+ label: 'marketplace clone',
118
+ path: marketplace,
119
+ disposition: 'manual',
120
+ reason: `/plugin marketplace remove ${uninstall_types_js_1.MARKETPLACE}`,
121
+ sizeBytes: (0, uninstall_types_js_1.dirSize)(marketplace),
122
+ });
123
+ }
124
+ // Overlays: skip dirty, remove clean — via the same `workingTreeStatus` the
125
+ // `overlay update`, `overlay remove` (#675), and doctor paths consult, so the
126
+ // removal paths cannot drift apart about whether local edits are safe.
127
+ // A git runner that could not report (`status: null`) maps to a non-zero
128
+ // status, i.e. `unreadable`, which is treated as dirty: a clone whose state
129
+ // cannot be read is not one to sweep away.
130
+ const overlayGit = (args, opts) => {
131
+ const r = git(args, opts);
132
+ return { status: r.status ?? 1, stdout: r.stdout, stderr: r.stderr };
133
+ };
134
+ let overlays = [];
135
+ try {
136
+ overlays = registry.list(registry.read(home));
137
+ }
138
+ catch {
139
+ overlays = [];
140
+ }
141
+ for (const entry of overlays) {
142
+ const exists = fs.existsSync(entry.path);
143
+ const dirty = exists
144
+ ? (0, overlay_commands_js_1.workingTreeStatus)(entry.path, overlayGit) !== 'clean'
145
+ : false;
146
+ found.push({
147
+ scope: 'global',
148
+ kind: 'overlay',
149
+ label: `overlay ${entry.name}`,
150
+ path: entry.path,
151
+ disposition: dirty ? 'blocked' : 'removable',
152
+ reason: dirty
153
+ ? 'has local edits — commit or discard them first'
154
+ : undefined,
155
+ sizeBytes: exists ? (0, uninstall_types_js_1.dirSize)(entry.path) : 0,
156
+ });
157
+ }
158
+ found.push({
159
+ scope: 'global',
160
+ kind: 'cli-binary',
161
+ label: 'CLI binary',
162
+ path: deps.cliPath ?? process.argv[1] ?? null,
163
+ disposition: 'manual',
164
+ reason: cliRemovalHint(deps.cliPath ?? process.argv[1] ?? ''),
165
+ });
166
+ return found;
167
+ }
168
+ /**
169
+ * The removal command for however the CLI was installed. Detection is a hint,
170
+ * not a claim — an unrecognised root falls back to naming all of them rather
171
+ * than guessing one.
172
+ */
173
+ function cliRemovalHint(cliPath) {
174
+ const p = cliPath.toLowerCase();
175
+ if (p.includes('volta'))
176
+ return 'volta uninstall canary-test-cli';
177
+ if (p.includes('mise') || p.includes('.asdf'))
178
+ return 'mise unuse -g npm:canary-test-cli';
179
+ if (p.includes('node_modules'))
180
+ return 'npm uninstall -g canary-test-cli';
181
+ return 'npm uninstall -g canary-test-cli (or volta/mise equivalent)';
182
+ }
183
+ function enumerateProject(opts, deps) {
184
+ const cwd = deps.cwd ?? process.cwd();
185
+ const found = [];
186
+ const includeGenerated = opts.includeGenerated === true;
187
+ const canaryDir = path.join(cwd, '.canary');
188
+ const quarantine = path.join(canaryDir, 'quarantine.json');
189
+ const hasQuarantine = fs.existsSync(quarantine);
190
+ if (fs.existsSync(canaryDir)) {
191
+ found.push({
192
+ scope: 'project',
193
+ kind: 'project-config',
194
+ label: '.canary/ project config',
195
+ path: canaryDir,
196
+ disposition: 'removable',
197
+ sizeBytes: (0, uninstall_types_js_1.dirSize)(canaryDir),
198
+ });
199
+ }
200
+ if (hasQuarantine) {
201
+ found.push({
202
+ scope: 'project',
203
+ kind: 'quarantine-ledger',
204
+ label: '.canary/quarantine.json (deleted-test ledger)',
205
+ path: quarantine,
206
+ disposition: includeGenerated ? 'removable' : 'blocked',
207
+ reason: includeGenerated
208
+ ? undefined
209
+ : `records why tests were deleted — pass --include-generated to remove`,
210
+ sizeBytes: (0, uninstall_types_js_1.dirSize)(quarantine),
211
+ });
212
+ }
213
+ const reports = path.join(cwd, 'test-results', 'reports');
214
+ if (fs.existsSync(reports)) {
215
+ found.push({
216
+ scope: 'project',
217
+ kind: 'project-reports',
218
+ label: 'test-results/reports/',
219
+ path: reports,
220
+ disposition: 'removable',
221
+ sizeBytes: (0, uninstall_types_js_1.dirSize)(reports),
222
+ });
223
+ }
224
+ const generated = path.join(cwd, 'tests', 'generated');
225
+ if (fs.existsSync(generated)) {
226
+ let count = 0;
227
+ try {
228
+ count = fs.readdirSync(generated).length;
229
+ }
230
+ catch {
231
+ count = 0;
232
+ }
233
+ found.push({
234
+ scope: 'project',
235
+ kind: 'generated-tests',
236
+ label: `tests/generated/ (${count} file(s))`,
237
+ path: generated,
238
+ disposition: includeGenerated ? 'removable' : 'blocked',
239
+ reason: includeGenerated
240
+ ? undefined
241
+ : 'unpromoted generated tests — pass --include-generated to remove',
242
+ sizeBytes: (0, uninstall_types_js_1.dirSize)(generated),
243
+ });
244
+ }
245
+ // `.mcp.json` is shared with other servers, so only our key comes out.
246
+ const mcpPath = path.join(cwd, '.mcp.json');
247
+ const mcp = (0, uninstall_types_js_1.readJson)(mcpPath);
248
+ if (mcp?.mcpServers && Object.hasOwn(mcp.mcpServers, uninstall_types_js_1.MCP_KEY)) {
249
+ found.push({
250
+ scope: 'project',
251
+ kind: 'mcp-entry',
252
+ label: `.mcp.json → "${uninstall_types_js_1.MCP_KEY}" server entry`,
253
+ path: mcpPath,
254
+ disposition: 'removable',
255
+ });
256
+ }
257
+ return found;
258
+ }
259
+ function enumerate(opts, deps = {}) {
260
+ const out = [];
261
+ if (opts.scope === 'global' || opts.scope === 'all') {
262
+ out.push(...enumerateGlobal(opts, deps));
263
+ }
264
+ if (opts.scope === 'project' || opts.scope === 'all') {
265
+ out.push(...enumerateProject(opts, deps));
266
+ }
267
+ return out;
268
+ }
269
+ /**
270
+ * Remove `.canary/` while preserving the quarantine ledger when it was not
271
+ * opted into. Deleting the directory wholesale would take the ledger with it,
272
+ * silently undoing the protection the `blocked` disposition just promised.
273
+ */
@@ -0,0 +1,46 @@
1
+ export declare const MARKETPLACE = "bop-clocktower";
2
+ export declare const PLUGIN_KEY = "canary@bop-clocktower";
3
+ export declare const MCP_KEY = "canary-mcp";
4
+ export type Scope = 'global' | 'project' | 'all';
5
+ /**
6
+ * `removable` — the CLI will delete it under `--apply`.
7
+ * `blocked` — found, deliberately not removed; `reason` says why.
8
+ * `manual` — someone else owns it; `reason` carries the exact command.
9
+ */
10
+ export type Disposition = 'removable' | 'blocked' | 'manual';
11
+ export interface Artifact {
12
+ scope: 'global' | 'project';
13
+ kind: string;
14
+ label: string;
15
+ path: string | null;
16
+ disposition: Disposition;
17
+ reason?: string;
18
+ sizeBytes?: number;
19
+ }
20
+ export interface EnumerateOptions {
21
+ scope: Scope;
22
+ includeGenerated?: boolean;
23
+ }
24
+ export type GitRunner = (args: string[], opts?: {
25
+ cwd?: string;
26
+ }) => {
27
+ status: number | null;
28
+ stdout: string;
29
+ stderr: string;
30
+ };
31
+ export interface Writer {
32
+ write: (s: string) => void;
33
+ }
34
+ export interface UninstallDeps {
35
+ homeDir?: string;
36
+ cwd?: string;
37
+ out?: Writer;
38
+ err?: Writer;
39
+ git?: GitRunner;
40
+ /** Resolved path of the running CLI, injectable for tests. */
41
+ cliPath?: string;
42
+ }
43
+ export declare const defaultGit: GitRunner;
44
+ export declare function readJson(file: string): unknown;
45
+ /** Recursive byte size; best-effort, never throws. */
46
+ export declare function dirSize(target: string): number;
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.defaultGit = exports.MCP_KEY = exports.PLUGIN_KEY = exports.MARKETPLACE = void 0;
37
+ exports.readJson = readJson;
38
+ exports.dirSize = dirSize;
39
+ /**
40
+ * Shared vocabulary for `canary uninstall`: the artifact model every phase
41
+ * speaks, plus the filesystem helpers the scan needs. Split out so the scan,
42
+ * render, and removal modules each stay independently readable.
43
+ */
44
+ const fs = __importStar(require("node:fs"));
45
+ const path = __importStar(require("node:path"));
46
+ const node_child_process_1 = require("node:child_process");
47
+ exports.MARKETPLACE = 'bop-clocktower';
48
+ exports.PLUGIN_KEY = 'canary@bop-clocktower';
49
+ exports.MCP_KEY = 'canary-mcp';
50
+ const defaultGit = (args, opts = {}) => {
51
+ const r = (0, node_child_process_1.spawnSync)('git', args, { cwd: opts.cwd, encoding: 'utf8' });
52
+ return { status: r.status, stdout: r.stdout ?? '', stderr: r.stderr ?? '' };
53
+ };
54
+ exports.defaultGit = defaultGit;
55
+ function readJson(file) {
56
+ try {
57
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ /** Recursive byte size; best-effort, never throws. */
64
+ function dirSize(target) {
65
+ let total = 0;
66
+ const walk = (p) => {
67
+ let st;
68
+ try {
69
+ st = fs.statSync(p);
70
+ }
71
+ catch {
72
+ return;
73
+ }
74
+ if (st.isDirectory()) {
75
+ let kids = [];
76
+ try {
77
+ kids = fs.readdirSync(p);
78
+ }
79
+ catch {
80
+ return;
81
+ }
82
+ for (const k of kids)
83
+ walk(path.join(p, k));
84
+ }
85
+ else {
86
+ total += st.size;
87
+ }
88
+ };
89
+ walk(target);
90
+ return total;
91
+ }
@@ -0,0 +1,13 @@
1
+ import { type Artifact, type UninstallDeps } from './uninstall-types.js';
2
+ export { enumerate } from './uninstall-scan.js';
3
+ export { render } from './uninstall-render.js';
4
+ export type { Artifact, Disposition, EnumerateOptions, Scope, UninstallDeps, } from './uninstall-types.js';
5
+ export interface RemovalOutcome {
6
+ removed: Artifact[];
7
+ failed: Array<{
8
+ artifact: Artifact;
9
+ error: string;
10
+ }>;
11
+ }
12
+ export declare function removeAll(artifacts: Artifact[], deps?: UninstallDeps): RemovalOutcome;
13
+ export declare function run(argv: readonly string[], deps?: UninstallDeps): number;