sweet-search 2.5.2 → 2.5.4
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/core/cli.js +24 -3
- package/core/graph/graph-expansion.js +215 -36
- package/core/graph/graph-extractor.js +196 -11
- package/core/graph/graph-search.js +395 -92
- package/core/graph/hcgs-generator.js +2 -1
- package/core/graph/index.js +2 -0
- package/core/graph/repo-map.js +28 -6
- package/core/graph/structural-answer-cues.js +168 -0
- package/core/graph/structural-callsite-hints.js +40 -0
- package/core/graph/structural-context-format.js +40 -0
- package/core/graph/structural-context.js +450 -0
- package/core/graph/structural-forward-push.js +156 -0
- package/core/graph/structural-header-context.js +19 -0
- package/core/graph/structural-importance.js +148 -0
- package/core/graph/structural-pagerank.js +197 -0
- package/core/graph/summary-manager.js +13 -9
- package/core/incremental-indexing/application/dirty-scan.mjs +236 -0
- package/core/incremental-indexing/application/file-watcher.mjs +197 -0
- package/core/incremental-indexing/application/maintenance-handlers.mjs +519 -0
- package/core/incremental-indexing/application/maintenance-worker.mjs +380 -0
- package/core/incremental-indexing/application/operator-cli.mjs +554 -0
- package/core/incremental-indexing/application/production-li-delta.mjs +192 -0
- package/core/incremental-indexing/application/production-reconciler-helpers.mjs +107 -0
- package/core/incremental-indexing/application/production-reconciler.mjs +583 -0
- package/core/incremental-indexing/application/reconciler.mjs +477 -0
- package/core/incremental-indexing/application/tombstone-injector.mjs +148 -0
- package/core/incremental-indexing/domain/chunk-identity.mjs +260 -0
- package/core/incremental-indexing/domain/encoder-deps.mjs +193 -0
- package/core/incremental-indexing/domain/encoder-input.mjs +225 -0
- package/core/incremental-indexing/domain/interval-autotune.mjs +255 -0
- package/core/incremental-indexing/domain/reconcile-counters.mjs +149 -0
- package/core/incremental-indexing/domain/watermark-scheduler.mjs +239 -0
- package/core/incremental-indexing/infrastructure/artifact-temp-sweep.mjs +163 -0
- package/core/incremental-indexing/infrastructure/baseline-readiness.mjs +121 -0
- package/core/incremental-indexing/infrastructure/dirty-set.mjs +233 -0
- package/core/incremental-indexing/infrastructure/graph-gc.mjs +314 -0
- package/core/incremental-indexing/infrastructure/hashing.mjs +298 -0
- package/core/incremental-indexing/infrastructure/hcgs-invalidation.mjs +182 -0
- package/core/incremental-indexing/infrastructure/li-segment-merge.mjs +278 -0
- package/core/incremental-indexing/infrastructure/li-segment-state.mjs +173 -0
- package/core/incremental-indexing/infrastructure/lockfile.mjs +119 -0
- package/core/incremental-indexing/infrastructure/maintenance-state-reader.mjs +283 -0
- package/core/incremental-indexing/infrastructure/manifest.mjs +194 -0
- package/core/incremental-indexing/infrastructure/path-filter.mjs +190 -0
- package/core/incremental-indexing/infrastructure/reader-heartbeat.mjs +201 -0
- package/core/incremental-indexing/infrastructure/schema-migrations.mjs +257 -0
- package/core/incremental-indexing/infrastructure/sparse-gram-delta.mjs +335 -0
- package/core/incremental-indexing/infrastructure/sqlite-fts5.mjs +176 -0
- package/core/incremental-indexing/infrastructure/staleness-display.mjs +105 -0
- package/core/incremental-indexing/infrastructure/tombstone-bitmap.mjs +234 -0
- package/core/incremental-indexing/infrastructure/vector-delta-writer.mjs +359 -0
- package/core/incremental-indexing/infrastructure/vector-gc.mjs +133 -0
- package/core/incremental-indexing/infrastructure/worktree-stamp.mjs +155 -0
- package/core/incremental-indexing/infrastructure/wsl2-detect.mjs +115 -0
- package/core/indexing/admission-policy.js +139 -0
- package/core/indexing/artifact-builder.js +29 -12
- package/core/indexing/ast-chunker.js +107 -30
- package/core/indexing/dedup/exemplar-selector.js +19 -1
- package/core/indexing/gitignore-filter.js +223 -0
- package/core/indexing/incremental-tracker.js +99 -30
- package/core/indexing/index-codebase-v21.js +6 -5
- package/core/indexing/index-maintainer.mjs +698 -6
- package/core/indexing/indexer-ann.js +99 -15
- package/core/indexing/indexer-build.js +158 -45
- package/core/indexing/indexer-empty-baseline.js +80 -0
- package/core/indexing/indexer-manifest.js +66 -0
- package/core/indexing/indexer-phases.js +56 -23
- package/core/indexing/indexer-sparse-gram.js +54 -13
- package/core/indexing/indexer-utils.js +26 -208
- package/core/indexing/indexing-file-policy.js +32 -7
- package/core/indexing/maintainer-launcher.mjs +137 -0
- package/core/indexing/merkle-tracker.js +251 -244
- package/core/indexing/model-pool.js +46 -5
- package/core/infrastructure/code-graph-repository.js +758 -6
- package/core/infrastructure/code-graph-visibility.js +157 -0
- package/core/infrastructure/codebase-repository.js +100 -13
- package/core/infrastructure/config/search.js +1 -1
- package/core/infrastructure/db-utils.js +118 -0
- package/core/infrastructure/dedup-hashing.js +10 -13
- package/core/infrastructure/hardware-capability.js +17 -7
- package/core/infrastructure/index.js +8 -2
- package/core/infrastructure/language-patterns/maps.js +4 -1
- package/core/infrastructure/language-patterns/registry-core.js +56 -17
- package/core/infrastructure/language-patterns/registry-object-oriented.js +12 -5
- package/core/infrastructure/language-patterns.js +69 -0
- package/core/infrastructure/model-registry.js +20 -0
- package/core/infrastructure/native-inference.js +7 -12
- package/core/infrastructure/native-resolver.js +52 -37
- package/core/infrastructure/native-sparse-gram.js +261 -20
- package/core/infrastructure/native-tokenizer.js +6 -15
- package/core/infrastructure/simd-distance.js +10 -16
- package/core/infrastructure/sparse-gram-delta-reader.js +76 -0
- package/core/infrastructure/structural-alias-resolver.js +122 -0
- package/core/infrastructure/structural-candidate-ranker.js +34 -0
- package/core/infrastructure/structural-context-repository.js +472 -0
- package/core/infrastructure/structural-context-utils.js +51 -0
- package/core/infrastructure/structural-graph-signals.js +121 -0
- package/core/infrastructure/structural-qualified-resolution.js +15 -0
- package/core/infrastructure/structural-source-definitions.js +100 -0
- package/core/infrastructure/tombstone-bitmap-reader.js +139 -0
- package/core/infrastructure/tree-sitter-provider.js +811 -37
- package/core/prompt-optimization/data/p7-final/sweet-search-system-prompt.md +50 -0
- package/core/query/query-router.js +55 -5
- package/core/ranking/file-kind-ranking.js +2192 -15
- package/core/ranking/late-interaction-index.js +87 -12
- package/core/search/cli-decoration.js +290 -0
- package/core/search/context-expander.js +988 -78
- package/core/search/index.js +1 -0
- package/core/search/output-policy.js +275 -0
- package/core/search/search-anchor.js +499 -0
- package/core/search/search-boost.js +93 -1
- package/core/search/search-cli.js +61 -204
- package/core/search/search-hybrid.js +250 -10
- package/core/search/search-pattern-chunks.js +57 -8
- package/core/search/search-pattern-planner.js +68 -9
- package/core/search/search-pattern-prefilter.js +30 -10
- package/core/search/search-pattern-ripgrep.js +40 -4
- package/core/search/search-pattern-sparse-overlay.js +256 -0
- package/core/search/search-pattern.js +117 -29
- package/core/search/search-postprocess.js +479 -5
- package/core/search/search-read-semantic.js +260 -23
- package/core/search/search-read.js +82 -64
- package/core/search/search-reader-pin.js +71 -0
- package/core/search/search-rrf.js +279 -0
- package/core/search/search-semantic.js +110 -5
- package/core/search/search-server.js +130 -57
- package/core/search/search-trace.js +107 -0
- package/core/search/server-identity.js +93 -0
- package/core/search/session-daemon-prewarm.mjs +33 -10
- package/core/search/sweet-search.js +399 -7
- package/core/skills/sweet-index/SKILL.md +8 -6
- package/core/vector-store/binary-hnsw-index.js +194 -30
- package/core/vector-store/float-vector-store.js +96 -6
- package/core/vector-store/hnsw-index.js +220 -49
- package/eval/agent-read-workflows/bin/_ss-helpers.mjs +471 -0
- package/eval/agent-read-workflows/bin/ss-find +15 -0
- package/eval/agent-read-workflows/bin/ss-grep +12 -0
- package/eval/agent-read-workflows/bin/ss-read +14 -0
- package/eval/agent-read-workflows/bin/ss-search +18 -0
- package/eval/agent-read-workflows/bin/ss-semantic +12 -0
- package/eval/agent-read-workflows/bin/ss-trace +11 -0
- package/mcp/read-tool.js +109 -0
- package/mcp/server.js +55 -15
- package/mcp/tool-handlers.js +14 -124
- package/mcp/trace-tool.js +81 -0
- package/package.json +25 -10
- package/scripts/hooks/intercept-read.mjs +55 -0
- package/scripts/hooks/remind-tools.mjs +40 -0
- package/scripts/init.js +698 -54
- package/scripts/inject-agent-instructions.js +431 -0
- package/scripts/install-prompt-reminders.js +188 -0
- package/scripts/install-tool-enforcement.js +220 -0
- package/scripts/smoke-test.js +12 -9
- package/scripts/uninstall.js +276 -18
- package/scripts/write-claude-rules.js +110 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install / remove sweet-search Claude-specific tool-enforcement.
|
|
3
|
+
*
|
|
4
|
+
* Plan reference: §4D / §10 step 17. Opt-in via `--enforce-tools` because
|
|
5
|
+
* strict enforcement is opinionated and Claude-specific. Universally gated
|
|
6
|
+
* by `--no-claude` (handled in init.js — when --no-claude, never called).
|
|
7
|
+
*
|
|
8
|
+
* Two-part install (per §4D):
|
|
9
|
+
* 1. Deny native `Grep` in `.claude/settings.json::permissions.deny`.
|
|
10
|
+
* Sweet-search wants `ss-grep` to be the default; denying Grep
|
|
11
|
+
* forces the agent toward the indexed path.
|
|
12
|
+
* 2. PreToolUse hint hook for `Read` — *hints*, never blocks. Edit
|
|
13
|
+
* workflows require Read before Edit, so a deny would break tooling.
|
|
14
|
+
*
|
|
15
|
+
* Removable cleanly: tracks the deny entry by exact value match and the
|
|
16
|
+
* hook entry by filename match (same pattern as prewarm SessionStart hook).
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { copyFileSync, existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
|
+
import { dirname, isAbsolute, join, relative } from 'node:path';
|
|
21
|
+
|
|
22
|
+
export const INTERCEPT_READ_HOOK_FILENAME = 'sweet-search-intercept-read.mjs';
|
|
23
|
+
const SOURCE_REL = ['scripts', 'hooks', 'intercept-read.mjs'];
|
|
24
|
+
const GREP_DENY_VALUE = 'Grep';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Install the strict-mode enforcement: Grep deny + Read hint hook.
|
|
28
|
+
* Idempotent. Never throws — surfaces errors via the status object.
|
|
29
|
+
*
|
|
30
|
+
* @param {object} args
|
|
31
|
+
* @param {string} args.projectRoot
|
|
32
|
+
* @param {string} args.packageRoot
|
|
33
|
+
* @param {boolean} [args.skipped=false] — set when --enforce-tools is OFF
|
|
34
|
+
* @returns {{ status: string, detail: string, hookPath?: string }}
|
|
35
|
+
* status ∈ { installed, skipped, error }
|
|
36
|
+
*/
|
|
37
|
+
export function installToolEnforcement({ projectRoot, packageRoot, skipped = false } = {}) {
|
|
38
|
+
if (skipped) return { status: 'skipped', detail: '--enforce-tools not set' };
|
|
39
|
+
if (!projectRoot) return { status: 'error', detail: 'install-tool-enforcement: projectRoot is required' };
|
|
40
|
+
if (!packageRoot) return { status: 'error', detail: 'install-tool-enforcement: packageRoot is required' };
|
|
41
|
+
|
|
42
|
+
// Hook source must exist in the package (covered by package.json files).
|
|
43
|
+
const hookSrcAbs = join(packageRoot, ...SOURCE_REL);
|
|
44
|
+
if (!existsSync(hookSrcAbs)) {
|
|
45
|
+
return { status: 'error', detail: `hook source missing: ${hookSrcAbs}` };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const hookDestAbs = join(projectRoot, '.claude', 'hooks', INTERCEPT_READ_HOOK_FILENAME);
|
|
49
|
+
const hookRelFromProject = relative(projectRoot, hookDestAbs);
|
|
50
|
+
if (hookRelFromProject.startsWith('..') || isAbsolute(hookRelFromProject)) {
|
|
51
|
+
return { status: 'error', detail: 'hook destination escapes projectRoot' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
mkdirSync(dirname(hookDestAbs), { recursive: true });
|
|
56
|
+
copyFileSync(hookSrcAbs, hookDestAbs);
|
|
57
|
+
} catch (err) {
|
|
58
|
+
return { status: 'error', detail: `hook copy failed: ${err.message}` };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const settingsResult = upsertSettings({
|
|
62
|
+
projectRoot,
|
|
63
|
+
addDeny: GREP_DENY_VALUE,
|
|
64
|
+
addPreToolUseHook: {
|
|
65
|
+
matcher: 'Read',
|
|
66
|
+
command: `node ${hookRelFromProject}`,
|
|
67
|
+
ownershipFilename: INTERCEPT_READ_HOOK_FILENAME,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
if (settingsResult.status !== 'installed') return settingsResult;
|
|
71
|
+
return { ...settingsResult, hookPath: hookRelFromProject };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Reverse `installToolEnforcement`. Removes the Grep deny entry (only
|
|
76
|
+
* when present), the PreToolUse hook entry (by filename match), and the
|
|
77
|
+
* hook script file. Other denies / hooks are preserved.
|
|
78
|
+
*
|
|
79
|
+
* @returns {{ status: string, detail: string }} status ∈
|
|
80
|
+
* { removed, not-found, dry-run, error }
|
|
81
|
+
*/
|
|
82
|
+
export function removeToolEnforcement({ projectRoot, dryRun = false } = {}) {
|
|
83
|
+
if (!projectRoot) return { status: 'error', detail: 'remove-tool-enforcement: projectRoot is required' };
|
|
84
|
+
|
|
85
|
+
const hookDestAbs = join(projectRoot, '.claude', 'hooks', INTERCEPT_READ_HOOK_FILENAME);
|
|
86
|
+
const settingsPath = join(projectRoot, '.claude', 'settings.json');
|
|
87
|
+
|
|
88
|
+
const hookExists = existsSync(hookDestAbs);
|
|
89
|
+
const denyExists = settingsHasDeny(settingsPath, GREP_DENY_VALUE);
|
|
90
|
+
const hookEntryExists = settingsHasPreToolUseHook(settingsPath, INTERCEPT_READ_HOOK_FILENAME);
|
|
91
|
+
|
|
92
|
+
if (!hookExists && !denyExists && !hookEntryExists) {
|
|
93
|
+
return { status: 'not-found', detail: 'no enforcement to undo' };
|
|
94
|
+
}
|
|
95
|
+
if (dryRun) {
|
|
96
|
+
const parts = [
|
|
97
|
+
hookExists && 'hook file',
|
|
98
|
+
denyExists && 'Grep deny',
|
|
99
|
+
hookEntryExists && 'PreToolUse entry',
|
|
100
|
+
].filter(Boolean);
|
|
101
|
+
return { status: 'dry-run', detail: parts.join(' + ') };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const removed = [];
|
|
105
|
+
if (hookExists) {
|
|
106
|
+
try { unlinkSync(hookDestAbs); removed.push('hook file'); }
|
|
107
|
+
catch (err) { return { status: 'error', detail: `failed to remove hook: ${err.message}` }; }
|
|
108
|
+
}
|
|
109
|
+
if (denyExists || hookEntryExists) {
|
|
110
|
+
const r = upsertSettings({
|
|
111
|
+
projectRoot,
|
|
112
|
+
removeDeny: denyExists ? GREP_DENY_VALUE : null,
|
|
113
|
+
removePreToolUseHook: hookEntryExists ? INTERCEPT_READ_HOOK_FILENAME : null,
|
|
114
|
+
});
|
|
115
|
+
if (r.status === 'error') return r;
|
|
116
|
+
if (denyExists) removed.push('Grep deny');
|
|
117
|
+
if (hookEntryExists) removed.push('PreToolUse entry');
|
|
118
|
+
}
|
|
119
|
+
return { status: 'removed', detail: removed.join(' + ') };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ─── Settings.json upsert (read → mutate → atomic write) ────────────────────
|
|
123
|
+
|
|
124
|
+
function upsertSettings({
|
|
125
|
+
projectRoot,
|
|
126
|
+
addDeny = null,
|
|
127
|
+
removeDeny = null,
|
|
128
|
+
addPreToolUseHook = null,
|
|
129
|
+
removePreToolUseHook = null,
|
|
130
|
+
}) {
|
|
131
|
+
const settingsDir = join(projectRoot, '.claude');
|
|
132
|
+
const settingsPath = join(settingsDir, 'settings.json');
|
|
133
|
+
|
|
134
|
+
let settings = {};
|
|
135
|
+
if (existsSync(settingsPath)) {
|
|
136
|
+
try { settings = JSON.parse(readFileSync(settingsPath, 'utf8')); }
|
|
137
|
+
catch (err) { return { status: 'error', detail: `settings.json invalid: ${err.message}` }; }
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Permissions deny.
|
|
141
|
+
if (addDeny) {
|
|
142
|
+
settings.permissions = settings.permissions || {};
|
|
143
|
+
const deny = Array.isArray(settings.permissions.deny) ? settings.permissions.deny : [];
|
|
144
|
+
if (!deny.includes(addDeny)) deny.push(addDeny);
|
|
145
|
+
settings.permissions.deny = deny;
|
|
146
|
+
}
|
|
147
|
+
if (removeDeny) {
|
|
148
|
+
const deny = Array.isArray(settings?.permissions?.deny) ? settings.permissions.deny : [];
|
|
149
|
+
const next = deny.filter((v) => v !== removeDeny);
|
|
150
|
+
if (next.length === 0) {
|
|
151
|
+
if (settings.permissions) delete settings.permissions.deny;
|
|
152
|
+
} else {
|
|
153
|
+
settings.permissions.deny = next;
|
|
154
|
+
}
|
|
155
|
+
if (settings.permissions && Object.keys(settings.permissions).length === 0) delete settings.permissions;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// PreToolUse hook entry.
|
|
159
|
+
if (addPreToolUseHook) {
|
|
160
|
+
settings.hooks = settings.hooks || {};
|
|
161
|
+
const arr = Array.isArray(settings.hooks.PreToolUse) ? settings.hooks.PreToolUse : [];
|
|
162
|
+
const entry = {
|
|
163
|
+
matcher: addPreToolUseHook.matcher,
|
|
164
|
+
hooks: [
|
|
165
|
+
{ type: 'command', command: addPreToolUseHook.command, timeout: 4000, continueOnError: true },
|
|
166
|
+
],
|
|
167
|
+
};
|
|
168
|
+
const ownedIdx = arr.findIndex(group =>
|
|
169
|
+
Array.isArray(group?.hooks)
|
|
170
|
+
&& group.hooks.some(h => typeof h?.command === 'string' && h.command.includes(addPreToolUseHook.ownershipFilename)),
|
|
171
|
+
);
|
|
172
|
+
if (ownedIdx >= 0) arr[ownedIdx] = entry;
|
|
173
|
+
else arr.push(entry);
|
|
174
|
+
settings.hooks.PreToolUse = arr;
|
|
175
|
+
}
|
|
176
|
+
if (removePreToolUseHook) {
|
|
177
|
+
const arr = Array.isArray(settings?.hooks?.PreToolUse) ? settings.hooks.PreToolUse : [];
|
|
178
|
+
const next = arr.filter(group =>
|
|
179
|
+
!Array.isArray(group?.hooks)
|
|
180
|
+
|| !group.hooks.some(h => typeof h?.command === 'string' && h.command.includes(removePreToolUseHook)),
|
|
181
|
+
);
|
|
182
|
+
if (next.length === 0) {
|
|
183
|
+
if (settings.hooks) delete settings.hooks.PreToolUse;
|
|
184
|
+
} else {
|
|
185
|
+
settings.hooks.PreToolUse = next;
|
|
186
|
+
}
|
|
187
|
+
if (settings.hooks && Object.keys(settings.hooks).length === 0) delete settings.hooks;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
mkdirSync(settingsDir, { recursive: true });
|
|
192
|
+
const tmp = settingsPath + '.tmp';
|
|
193
|
+
writeFileSync(tmp, JSON.stringify(settings, null, 2) + '\n', 'utf8');
|
|
194
|
+
renameSync(tmp, settingsPath);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
return { status: 'error', detail: err.message };
|
|
197
|
+
}
|
|
198
|
+
return { status: 'installed', detail: 'permissions + PreToolUse updated' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function settingsHasDeny(settingsPath, value) {
|
|
202
|
+
if (!existsSync(settingsPath)) return false;
|
|
203
|
+
try {
|
|
204
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
205
|
+
const deny = Array.isArray(settings?.permissions?.deny) ? settings.permissions.deny : [];
|
|
206
|
+
return deny.includes(value);
|
|
207
|
+
} catch { return false; }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function settingsHasPreToolUseHook(settingsPath, ownershipFilename) {
|
|
211
|
+
if (!existsSync(settingsPath)) return false;
|
|
212
|
+
try {
|
|
213
|
+
const settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
|
|
214
|
+
const arr = Array.isArray(settings?.hooks?.PreToolUse) ? settings.hooks.PreToolUse : [];
|
|
215
|
+
return arr.some(group =>
|
|
216
|
+
Array.isArray(group?.hooks)
|
|
217
|
+
&& group.hooks.some(h => typeof h?.command === 'string' && h.command.includes(ownershipFilename)),
|
|
218
|
+
);
|
|
219
|
+
} catch { return false; }
|
|
220
|
+
}
|
package/scripts/smoke-test.js
CHANGED
|
@@ -259,16 +259,19 @@ for (const profile of profiles) {
|
|
|
259
259
|
|
|
260
260
|
if (profile === 'full') {
|
|
261
261
|
assert(config.runtime.allowRuntimeModelDownload === false, 'Runtime downloads should be disabled for full');
|
|
262
|
-
// Model count
|
|
263
|
-
//
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
262
|
+
// Model count is hardware-conditional since commit d282bb0 ("use ORT CPU
|
|
263
|
+
// without accelerators"): a NO-ACCELERATOR host (no CoreML/CUDA/Metal —
|
|
264
|
+
// e.g. CI's linux-x64 ubuntu runner, or the darwin-x64 binary running
|
|
265
|
+
// cross-arch on an arm64 macos runner) correctly registers only the 3
|
|
266
|
+
// core ORT-INT8 models (embed-int8 + LI-int8 + semantic-cache MiniLM) and
|
|
267
|
+
// SKIPS the 2 native FP32 variants (coderankembed-fp32, lateon-code-fp32),
|
|
268
|
+
// since indexing falls back to ORT INT8 CPU. An accelerator host adds those
|
|
269
|
+
// 2 → 5 (opt-in cross-encoders excluded by default). So 3 is the true
|
|
270
|
+
// floor; pinning ≥4 wrongly failed no-accelerator CI runners. Assert the
|
|
271
|
+
// floor — dropping below the 3 core models is a real regression.
|
|
269
272
|
assert(
|
|
270
|
-
Object.keys(config.models).length >=
|
|
271
|
-
`Expected ≥
|
|
273
|
+
Object.keys(config.models).length >= 3,
|
|
274
|
+
`Expected ≥3 core models, got ${Object.keys(config.models).length}`,
|
|
272
275
|
);
|
|
273
276
|
} else {
|
|
274
277
|
assert(config.runtime.allowRuntimeModelDownload === true, 'Runtime downloads should be enabled for core');
|
package/scripts/uninstall.js
CHANGED
|
@@ -18,12 +18,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
18
18
|
|
|
19
19
|
import { getCoremlCascadeRoot, getCoremlCascadeState } from '../core/infrastructure/coreml-cascade.js';
|
|
20
20
|
import { PREWARM_HOOK_FILENAME } from './init.js';
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const DEFAULT_SOCKET_PATH = process.env.SWEET_SEARCH_SOCKET_PATH || '/tmp/sweet-search.sock';
|
|
21
|
+
import { removeAgentInstructions } from './inject-agent-instructions.js';
|
|
22
|
+
import { removeClaudeRules } from './write-claude-rules.js';
|
|
23
|
+
import { removePromptReminderHook } from './install-prompt-reminders.js';
|
|
24
|
+
import { removeToolEnforcement } from './install-tool-enforcement.js';
|
|
25
|
+
import { projectSocketPath, projectPidFile } from '../core/search/server-identity.js';
|
|
27
26
|
|
|
28
27
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
28
|
const PACKAGE_ROOT = join(__dirname, '..');
|
|
@@ -175,8 +174,12 @@ export function getCoremlCascadeRemovals() {
|
|
|
175
174
|
*/
|
|
176
175
|
export function stopRunningDaemon({
|
|
177
176
|
projectRoot,
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
// Per-project socket/pidfile (C3) — derived from this project's root so
|
|
178
|
+
// uninstalling project A never stops project B's server. Honors explicit
|
|
179
|
+
// SWEET_SEARCH_SOCKET_PATH / SWEET_SEARCH_PID_FILE overrides. Tests pass
|
|
180
|
+
// explicit values for isolation.
|
|
181
|
+
pidFile = projectPidFile(process.env, projectRoot || process.cwd()),
|
|
182
|
+
socketPath = projectSocketPath(process.env, projectRoot || process.cwd()),
|
|
180
183
|
} = {}) {
|
|
181
184
|
const result = { gracefulAttempted: false, killed: false, pidFileRemoved: false, socketRemoved: false };
|
|
182
185
|
|
|
@@ -221,6 +224,63 @@ export function stopRunningDaemon({
|
|
|
221
224
|
return result;
|
|
222
225
|
}
|
|
223
226
|
|
|
227
|
+
const MAINTAINER_LOCK_FILENAME = 'index-maintainer.lock';
|
|
228
|
+
|
|
229
|
+
/** Synchronous sleep (uninstall is one-shot; a sub-second block is fine). */
|
|
230
|
+
function sleepSyncMs(ms) {
|
|
231
|
+
try { Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms); } catch { /* ignore */ }
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Is this pid alive right now? EPERM (foreign owner) counts as alive. */
|
|
235
|
+
function pidAlive(pid) {
|
|
236
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
237
|
+
try { process.kill(pid, 0); return true; } catch (err) { return err.code === 'EPERM'; }
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Stop a reconcile-v2 incremental-index maintainer that an earlier SessionStart
|
|
242
|
+
* prewarm hook auto-launched. The maintainer records its pid in
|
|
243
|
+
* `<stateDir>/index-maintainer.lock`.
|
|
244
|
+
*
|
|
245
|
+
* Strategy:
|
|
246
|
+
* 1. SIGTERM for a clean shutdown (the daemon flushes + releases its lock).
|
|
247
|
+
* 2. Escalate to SIGKILL if it is still alive after a short grace. Its tick
|
|
248
|
+
* interval can be up to 5 minutes — far longer than uninstall can wait —
|
|
249
|
+
* so we do not block for graceful exit. Maintainer writes are atomic
|
|
250
|
+
* temp+rename, so a SIGKILL is crash-safe (validated by the RC soak +
|
|
251
|
+
* crash probe).
|
|
252
|
+
* 3. Remove the lock file so the next start is clean.
|
|
253
|
+
*
|
|
254
|
+
* Never throws — every branch swallows errors (the daemon may not be running).
|
|
255
|
+
*
|
|
256
|
+
* Returns `{ present, pid, signalled, killed, lockRemoved }`.
|
|
257
|
+
*/
|
|
258
|
+
export function stopRunningMaintainer({
|
|
259
|
+
projectRoot,
|
|
260
|
+
stateDir = projectRoot ? join(projectRoot, DATA_DIR_NAME) : null,
|
|
261
|
+
} = {}) {
|
|
262
|
+
const result = { present: false, pid: null, signalled: false, killed: false, lockRemoved: false };
|
|
263
|
+
if (!stateDir) return result;
|
|
264
|
+
const lockFile = join(stateDir, MAINTAINER_LOCK_FILENAME);
|
|
265
|
+
if (!existsSync(lockFile)) return result;
|
|
266
|
+
result.present = true;
|
|
267
|
+
|
|
268
|
+
let pid = null;
|
|
269
|
+
try { pid = Number(JSON.parse(readFileSync(lockFile, 'utf-8')).pid); } catch { pid = null; }
|
|
270
|
+
|
|
271
|
+
if (pidAlive(pid)) {
|
|
272
|
+
result.pid = pid;
|
|
273
|
+
try { process.kill(pid, 'SIGTERM'); result.signalled = true; } catch { /* ignore */ }
|
|
274
|
+
sleepSyncMs(300);
|
|
275
|
+
if (pidAlive(pid)) {
|
|
276
|
+
try { process.kill(pid, 'SIGKILL'); result.killed = true; } catch { /* ignore */ }
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
try { unlinkSync(lockFile); result.lockRemoved = true; } catch { /* ignore */ }
|
|
281
|
+
return result;
|
|
282
|
+
}
|
|
283
|
+
|
|
224
284
|
/**
|
|
225
285
|
* Remove the index-maintainer daemon hook init copied into
|
|
226
286
|
* `.claude/hooks/index-maintainer.mjs`. Only removes the file when it
|
|
@@ -414,6 +474,84 @@ export function removePrewarmSessionStartHook(projectRoot, { dryRun = false } =
|
|
|
414
474
|
return { status: 'removed', detail: `spliced out ${sessionStart.length - filtered.length} entry` };
|
|
415
475
|
}
|
|
416
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Remove the Codex CLI SessionStart hook entry that `--codex` init wrote into
|
|
479
|
+
* `.codex/hooks.json`. Mirrors `removePrewarmSessionStartHook`: only the
|
|
480
|
+
* sweet-search-owned entry (matched by the launcher filename) is spliced out;
|
|
481
|
+
* other events/entries are preserved. When our entry was the only content the
|
|
482
|
+
* file is deleted rather than left as an empty shell. The `[features] hooks`
|
|
483
|
+
* feature flag in config.toml is intentionally left in place — it's harmless and
|
|
484
|
+
* may be shared with other tooling.
|
|
485
|
+
*
|
|
486
|
+
* Returns `{ status, detail }`:
|
|
487
|
+
* removed — our entry was spliced out (file rewritten or deleted)
|
|
488
|
+
* not-found — no .codex/hooks.json, no SessionStart, or no matching entry
|
|
489
|
+
* dry-run — would remove (no write)
|
|
490
|
+
* error — non-fatal (unreadable / invalid JSON / write failed)
|
|
491
|
+
*/
|
|
492
|
+
export function removeCodexSessionStartHook(projectRoot, { dryRun = false } = {}) {
|
|
493
|
+
const hooksPath = join(projectRoot, '.codex', 'hooks.json');
|
|
494
|
+
if (!existsSync(hooksPath)) {
|
|
495
|
+
return { status: 'not-found', detail: 'no .codex/hooks.json' };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
let raw;
|
|
499
|
+
try {
|
|
500
|
+
raw = readFileSync(hooksPath, 'utf-8');
|
|
501
|
+
} catch (err) {
|
|
502
|
+
return { status: 'error', detail: `read failed: ${err.message}` };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
let doc;
|
|
506
|
+
try {
|
|
507
|
+
doc = JSON.parse(raw);
|
|
508
|
+
} catch (err) {
|
|
509
|
+
return { status: 'error', detail: `.codex/hooks.json is not valid JSON: ${err.message}` };
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const sessionStart = doc?.hooks?.SessionStart;
|
|
513
|
+
if (!Array.isArray(sessionStart) || sessionStart.length === 0) {
|
|
514
|
+
return { status: 'not-found', detail: 'no SessionStart entries' };
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const filtered = sessionStart.filter((group) =>
|
|
518
|
+
!(Array.isArray(group?.hooks) &&
|
|
519
|
+
group.hooks.some((h) => typeof h?.command === 'string' && h.command.includes(PREWARM_HOOK_FILENAME)))
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
if (filtered.length === sessionStart.length) {
|
|
523
|
+
return { status: 'not-found', detail: 'no matching entry' };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
if (dryRun) {
|
|
527
|
+
return { status: 'dry-run', detail: `would remove ${sessionStart.length - filtered.length} entry` };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
if (filtered.length === 0) {
|
|
531
|
+
delete doc.hooks.SessionStart;
|
|
532
|
+
if (doc.hooks && Object.keys(doc.hooks).length === 0) {
|
|
533
|
+
delete doc.hooks;
|
|
534
|
+
}
|
|
535
|
+
} else {
|
|
536
|
+
doc.hooks.SessionStart = filtered;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
try {
|
|
540
|
+
if (doc && Object.keys(doc).length === 0) {
|
|
541
|
+
// Our hook was the only content — remove the file rather than leave `{}`.
|
|
542
|
+
unlinkSync(hooksPath);
|
|
543
|
+
} else {
|
|
544
|
+
const tmpPath = hooksPath + '.tmp';
|
|
545
|
+
writeFileSync(tmpPath, JSON.stringify(doc, null, 2) + '\n', 'utf-8');
|
|
546
|
+
renameSync(tmpPath, hooksPath);
|
|
547
|
+
}
|
|
548
|
+
} catch (err) {
|
|
549
|
+
return { status: 'error', detail: `write failed: ${err.message}` };
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return { status: 'removed', detail: `spliced out ${sessionStart.length - filtered.length} entry` };
|
|
553
|
+
}
|
|
554
|
+
|
|
417
555
|
// ---------------------------------------------------------------------------
|
|
418
556
|
// Optional native package list (derived from package.json)
|
|
419
557
|
// ---------------------------------------------------------------------------
|
|
@@ -550,8 +688,35 @@ export async function runUninstall(args) {
|
|
|
550
688
|
const indexMaintainerSkippedReason =
|
|
551
689
|
indexMaintainerPreview.status === 'skipped' ? indexMaintainerPreview.detail : null;
|
|
552
690
|
|
|
691
|
+
// P1: agent-instruction files (AGENTS.md / CLAUDE.md / GEMINI.md /
|
|
692
|
+
// .cursor/rules/sweet-search.mdc) and the .claude/rules/sweet-search.md
|
|
693
|
+
// sentinel file. The marker block contract guarantees we only strip
|
|
694
|
+
// sweet-search-managed content; user prose outside the marker is preserved.
|
|
695
|
+
const agentInstructionsPreview = removeAgentInstructions({ projectRoot, dryRun: true });
|
|
696
|
+
const agentInstructionsTouched = Object.values(agentInstructionsPreview.harnesses ?? {})
|
|
697
|
+
.some(s => s === 'dry-run');
|
|
698
|
+
const claudeRulesPreview = removeClaudeRules({ projectRoot, dryRun: true });
|
|
699
|
+
const hasClaudeRules = claudeRulesPreview === 'dry-run';
|
|
700
|
+
|
|
701
|
+
// P2: UserPromptSubmit reminder hook (.claude/hooks/sweet-search-remind-tools.mjs
|
|
702
|
+
// + the matching settings.json entry).
|
|
703
|
+
const promptReminderPreview = removePromptReminderHook({ projectRoot, dryRun: true });
|
|
704
|
+
const hasPromptReminder = promptReminderPreview.status === 'dry-run';
|
|
705
|
+
|
|
706
|
+
// P3: Tool enforcement (Grep deny + PreToolUse hint hook for Read).
|
|
707
|
+
const toolEnforcementPreview = removeToolEnforcement({ projectRoot, dryRun: true });
|
|
708
|
+
const hasToolEnforcement = toolEnforcementPreview.status === 'dry-run';
|
|
709
|
+
|
|
710
|
+
// Codex CLI SessionStart hook (.codex/hooks.json), written by `init --codex`.
|
|
711
|
+
const codexHookPreview = removeCodexSessionStartHook(projectRoot, { dryRun: true });
|
|
712
|
+
const hasCodexHook = codexHookPreview.status === 'dry-run';
|
|
713
|
+
|
|
553
714
|
// Nothing to remove?
|
|
554
|
-
if (
|
|
715
|
+
if (
|
|
716
|
+
removals.length === 0 && !hasHookEntry && !hasSkillEntry && !hasIndexMaintainerHook
|
|
717
|
+
&& !agentInstructionsTouched && !hasClaudeRules
|
|
718
|
+
&& !hasPromptReminder && !hasToolEnforcement && !hasCodexHook
|
|
719
|
+
) {
|
|
555
720
|
console.log('Nothing to remove — Sweet Search is not initialized in this project.');
|
|
556
721
|
return;
|
|
557
722
|
}
|
|
@@ -576,6 +741,23 @@ export async function runUninstall(args) {
|
|
|
576
741
|
} else if (indexMaintainerSkippedReason) {
|
|
577
742
|
console.log(` [skipped] ${indexMaintainerSkippedReason}`);
|
|
578
743
|
}
|
|
744
|
+
if (agentInstructionsTouched) {
|
|
745
|
+
const targets = Object.entries(agentInstructionsPreview.harnesses)
|
|
746
|
+
.filter(([, v]) => v === 'dry-run').map(([k]) => k).join(', ');
|
|
747
|
+
console.log(` agent-instruction marker blocks (${targets})`);
|
|
748
|
+
}
|
|
749
|
+
if (hasClaudeRules) {
|
|
750
|
+
console.log(` .claude/rules/sweet-search.md`);
|
|
751
|
+
}
|
|
752
|
+
if (hasPromptReminder) {
|
|
753
|
+
console.log(` UserPromptSubmit reminder hook (${promptReminderPreview.detail})`);
|
|
754
|
+
}
|
|
755
|
+
if (hasToolEnforcement) {
|
|
756
|
+
console.log(` tool-enforcement strict mode (${toolEnforcementPreview.detail})`);
|
|
757
|
+
}
|
|
758
|
+
if (hasCodexHook) {
|
|
759
|
+
console.log(` Codex SessionStart hook (.codex/hooks.json)`);
|
|
760
|
+
}
|
|
579
761
|
console.log(` Total: ${formatBytes(totalBytes)}`);
|
|
580
762
|
if (parsed.keepModels) {
|
|
581
763
|
console.log(' Model cache: kept (--keep-models)');
|
|
@@ -597,6 +779,10 @@ export async function runUninstall(args) {
|
|
|
597
779
|
} else if (dryMaintainer.status === 'skipped') {
|
|
598
780
|
console.log(` Would skip: index-maintainer hook — ${dryMaintainer.detail}`);
|
|
599
781
|
}
|
|
782
|
+
const dryCodex = removeCodexSessionStartHook(projectRoot, { dryRun: true });
|
|
783
|
+
if (dryCodex.status === 'dry-run') {
|
|
784
|
+
console.log(` Would also remove: Codex SessionStart hook (.codex/hooks.json — ${dryCodex.detail})`);
|
|
785
|
+
}
|
|
600
786
|
console.log('Dry run — nothing was removed.');
|
|
601
787
|
return;
|
|
602
788
|
}
|
|
@@ -615,6 +801,30 @@ export async function runUninstall(args) {
|
|
|
615
801
|
}
|
|
616
802
|
}
|
|
617
803
|
|
|
804
|
+
// Stop the running daemon + maintainer BEFORE deleting .sweet-search/. The
|
|
805
|
+
// maintainer records its pid in .sweet-search/index-maintainer.lock; if we
|
|
806
|
+
// removed the state dir first, stopRunningMaintainer() would have no pid to
|
|
807
|
+
// signal — the maintainer would leak and, because its tick loop recreates the
|
|
808
|
+
// state dir (mkdirSync), resurrect the very directory we just deleted. So this
|
|
809
|
+
// must run after the confirmation/dry-run gates but before any removal.
|
|
810
|
+
const daemonResult = stopRunningDaemon({ projectRoot });
|
|
811
|
+
if (daemonResult.killed) {
|
|
812
|
+
console.log(' Stopped: running prewarm daemon (SIGKILL via PID file)');
|
|
813
|
+
} else if (daemonResult.gracefulAttempted) {
|
|
814
|
+
console.log(' Stopped: running prewarm daemon (graceful via CLI)');
|
|
815
|
+
}
|
|
816
|
+
// If neither happened, daemon wasn't running — silent.
|
|
817
|
+
|
|
818
|
+
const maintainerResult = stopRunningMaintainer({ projectRoot });
|
|
819
|
+
if (maintainerResult.killed) {
|
|
820
|
+
console.log(` Stopped: incremental-index maintainer (SIGKILL after grace, pid ${maintainerResult.pid})`);
|
|
821
|
+
} else if (maintainerResult.signalled) {
|
|
822
|
+
console.log(` Stopped: incremental-index maintainer (SIGTERM, pid ${maintainerResult.pid})`);
|
|
823
|
+
} else if (maintainerResult.lockRemoved) {
|
|
824
|
+
console.log(' Cleared: stale incremental-index maintainer lock');
|
|
825
|
+
}
|
|
826
|
+
// If none happened, the maintainer wasn't running — silent.
|
|
827
|
+
|
|
618
828
|
// Remove
|
|
619
829
|
let removed = 0;
|
|
620
830
|
let kept = 0;
|
|
@@ -680,16 +890,64 @@ export async function runUninstall(args) {
|
|
|
680
890
|
kept++;
|
|
681
891
|
}
|
|
682
892
|
|
|
683
|
-
//
|
|
684
|
-
//
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
} else if (
|
|
690
|
-
console.log(
|
|
893
|
+
// Reverse the Codex SessionStart hook written by `init --codex`. The
|
|
894
|
+
// config.toml feature flag is left in place (harmless, possibly shared).
|
|
895
|
+
const codexHookResult = removeCodexSessionStartHook(projectRoot, { dryRun: parsed.dryRun });
|
|
896
|
+
if (codexHookResult.status === 'removed') {
|
|
897
|
+
console.log(` Removed: Codex SessionStart hook (.codex/hooks.json — ${codexHookResult.detail})`);
|
|
898
|
+
removed++;
|
|
899
|
+
} else if (codexHookResult.status === 'error') {
|
|
900
|
+
console.log(` Failed to remove Codex SessionStart hook: ${codexHookResult.detail}`);
|
|
901
|
+
kept++;
|
|
902
|
+
}
|
|
903
|
+
// 'not-found' and 'dry-run' are silent in the main output.
|
|
904
|
+
|
|
905
|
+
// P1: strip agent-instruction marker blocks across all five harness files.
|
|
906
|
+
// The marker contract guarantees we never delete user prose outside of it.
|
|
907
|
+
const agentInstructionsResult = removeAgentInstructions({ projectRoot, dryRun: parsed.dryRun });
|
|
908
|
+
for (const [harness, status] of Object.entries(agentInstructionsResult.harnesses)) {
|
|
909
|
+
if (status === 'removed') {
|
|
910
|
+
console.log(` Removed: ${harness} agent-instruction block`);
|
|
911
|
+
removed++;
|
|
912
|
+
} else if (status === 'file-deleted') {
|
|
913
|
+
console.log(` Removed: ${harness} agent-instruction file (wholly sweet-search-managed)`);
|
|
914
|
+
removed++;
|
|
915
|
+
}
|
|
916
|
+
// 'not-found' / 'not-our-symlink' / 'dry-run' are silent.
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
// Remove the .claude/rules/sweet-search.md sentinel file. CLAUDE.md import
|
|
920
|
+
// line was already stripped by removeAgentInstructions above (it lived
|
|
921
|
+
// inside the agent-instructions marker).
|
|
922
|
+
const claudeRulesResult = removeClaudeRules({ projectRoot, dryRun: parsed.dryRun });
|
|
923
|
+
if (claudeRulesResult === 'removed') {
|
|
924
|
+
console.log(` Removed: .claude/rules/sweet-search.md`);
|
|
925
|
+
removed++;
|
|
926
|
+
} else if (claudeRulesResult === 'preserved-user-file') {
|
|
927
|
+
console.log(` Kept: .claude/rules/sweet-search.md — no sweet-search sentinel (user-edited)`);
|
|
928
|
+
kept++;
|
|
929
|
+
}
|
|
930
|
+
// 'not-found' / 'dry-run' are silent.
|
|
931
|
+
|
|
932
|
+
// P2: strip the UserPromptSubmit reminder hook + settings entry.
|
|
933
|
+
const promptReminderResult = removePromptReminderHook({ projectRoot, dryRun: parsed.dryRun });
|
|
934
|
+
if (promptReminderResult.status === 'removed') {
|
|
935
|
+
console.log(` Removed: UserPromptSubmit reminder hook (${promptReminderResult.detail})`);
|
|
936
|
+
removed++;
|
|
937
|
+
} else if (promptReminderResult.status === 'error') {
|
|
938
|
+
console.log(` Failed to remove UserPromptSubmit reminder hook: ${promptReminderResult.detail}`);
|
|
939
|
+
kept++;
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// P3: strip the tool-enforcement Grep deny + PreToolUse hook + hook file.
|
|
943
|
+
const toolEnforcementResult = removeToolEnforcement({ projectRoot, dryRun: parsed.dryRun });
|
|
944
|
+
if (toolEnforcementResult.status === 'removed') {
|
|
945
|
+
console.log(` Removed: tool-enforcement (${toolEnforcementResult.detail})`);
|
|
946
|
+
removed++;
|
|
947
|
+
} else if (toolEnforcementResult.status === 'error') {
|
|
948
|
+
console.log(` Failed to remove tool-enforcement: ${toolEnforcementResult.detail}`);
|
|
949
|
+
kept++;
|
|
691
950
|
}
|
|
692
|
-
// If neither happened, daemon wasn't running — silent.
|
|
693
951
|
|
|
694
952
|
// Purge npm packages
|
|
695
953
|
if (parsed.purge) {
|