milens 0.6.4 → 0.6.6
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/.agents/skills/adapters/SKILL.md +20 -0
- package/.agents/skills/analyzer/SKILL.md +38 -16
- package/.agents/skills/apps/SKILL.md +25 -1
- package/.agents/skills/docs/SKILL.md +33 -5
- package/.agents/skills/milens/SKILL.md +36 -6
- package/.agents/skills/milens-architect/SKILL.md +128 -0
- package/.agents/skills/milens-debugger/SKILL.md +141 -0
- package/.agents/skills/orchestrator/SKILL.md +59 -0
- package/.agents/skills/parser/SKILL.md +35 -14
- package/.agents/skills/root/SKILL.md +39 -17
- package/.agents/skills/scripts/SKILL.md +21 -3
- package/.agents/skills/security/SKILL.md +32 -11
- package/.agents/skills/server/SKILL.md +47 -20
- package/.agents/skills/store/SKILL.md +40 -18
- package/.agents/skills/test/SKILL.md +57 -9
- package/LICENSE +21 -75
- package/README.md +294 -467
- package/adapters/claude-code/CLAUDE.md +36 -15
- package/adapters/codex/.codex/codex.md +38 -23
- package/adapters/copilot/.github/copilot-instructions.md +29 -22
- package/adapters/gemini/.gemini/context.md +33 -10
- package/adapters/opencode/AGENTS.md +36 -15
- package/dist/agents-md.d.ts.map +1 -1
- package/dist/agents-md.js +56 -5
- package/dist/agents-md.js.map +1 -1
- package/dist/analyzer/engine.d.ts +4 -0
- package/dist/analyzer/engine.d.ts.map +1 -1
- package/dist/analyzer/engine.js +378 -14
- package/dist/analyzer/engine.js.map +1 -1
- package/dist/analyzer/resolver.d.ts +2 -0
- package/dist/analyzer/resolver.d.ts.map +1 -1
- package/dist/analyzer/resolver.js +187 -9
- package/dist/analyzer/resolver.js.map +1 -1
- package/dist/analyzer/review.d.ts.map +1 -1
- package/dist/analyzer/review.js +254 -32
- package/dist/analyzer/review.js.map +1 -1
- package/dist/analyzer/scope-resolver.d.ts +42 -0
- package/dist/analyzer/scope-resolver.d.ts.map +1 -0
- package/dist/analyzer/scope-resolver.js +687 -0
- package/dist/analyzer/scope-resolver.js.map +1 -0
- package/dist/cli.js +590 -20
- package/dist/cli.js.map +1 -1
- package/dist/orchestrator/orchestrator.d.ts +65 -0
- package/dist/orchestrator/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator/orchestrator.js +178 -0
- package/dist/orchestrator/orchestrator.js.map +1 -0
- package/dist/orchestrator/reporter.d.ts +15 -0
- package/dist/orchestrator/reporter.d.ts.map +1 -0
- package/dist/orchestrator/reporter.js +38 -0
- package/dist/orchestrator/reporter.js.map +1 -0
- package/dist/parser/extract.d.ts +6 -1
- package/dist/parser/extract.d.ts.map +1 -1
- package/dist/parser/extract.js +14 -2
- package/dist/parser/extract.js.map +1 -1
- package/dist/parser/lang-css.d.ts.map +1 -1
- package/dist/parser/lang-css.js +7 -1
- package/dist/parser/lang-css.js.map +1 -1
- package/dist/parser/lang-go.d.ts.map +1 -1
- package/dist/parser/lang-go.js +16 -0
- package/dist/parser/lang-go.js.map +1 -1
- package/dist/parser/lang-html.d.ts +4 -0
- package/dist/parser/lang-html.d.ts.map +1 -1
- package/dist/parser/lang-html.js +40 -1
- package/dist/parser/lang-html.js.map +1 -1
- package/dist/parser/lang-java.d.ts.map +1 -1
- package/dist/parser/lang-java.js +12 -0
- package/dist/parser/lang-java.js.map +1 -1
- package/dist/parser/lang-js.d.ts.map +1 -1
- package/dist/parser/lang-js.js +3 -0
- package/dist/parser/lang-js.js.map +1 -1
- package/dist/parser/lang-php.d.ts.map +1 -1
- package/dist/parser/lang-php.js +11 -0
- package/dist/parser/lang-php.js.map +1 -1
- package/dist/parser/lang-py.d.ts.map +1 -1
- package/dist/parser/lang-py.js +14 -0
- package/dist/parser/lang-py.js.map +1 -1
- package/dist/parser/lang-ruby.d.ts.map +1 -1
- package/dist/parser/lang-ruby.js +20 -0
- package/dist/parser/lang-ruby.js.map +1 -1
- package/dist/parser/lang-rust.d.ts.map +1 -1
- package/dist/parser/lang-rust.js +27 -4
- package/dist/parser/lang-rust.js.map +1 -1
- package/dist/parser/lang-ts.d.ts.map +1 -1
- package/dist/parser/lang-ts.js +3 -0
- package/dist/parser/lang-ts.js.map +1 -1
- package/dist/parser/lang-vue.d.ts +17 -1
- package/dist/parser/lang-vue.d.ts.map +1 -1
- package/dist/parser/lang-vue.js +177 -0
- package/dist/parser/lang-vue.js.map +1 -1
- package/dist/parser/language-provider.d.ts +27 -0
- package/dist/parser/language-provider.d.ts.map +1 -0
- package/dist/parser/language-provider.js +2 -0
- package/dist/parser/language-provider.js.map +1 -0
- package/dist/security/rules.d.ts.map +1 -1
- package/dist/security/rules.js +4 -1
- package/dist/security/rules.js.map +1 -1
- package/dist/server/hooks.d.ts +3 -0
- package/dist/server/hooks.d.ts.map +1 -1
- package/dist/server/hooks.js +79 -0
- package/dist/server/hooks.js.map +1 -1
- package/dist/server/mcp-prompts.d.ts.map +1 -1
- package/dist/server/mcp-prompts.js +1 -1
- package/dist/server/mcp-prompts.js.map +1 -1
- package/dist/server/mcp.d.ts.map +1 -1
- package/dist/server/mcp.js +638 -61
- package/dist/server/mcp.js.map +1 -1
- package/dist/server/watcher.d.ts +47 -0
- package/dist/server/watcher.d.ts.map +1 -0
- package/dist/server/watcher.js +136 -0
- package/dist/server/watcher.js.map +1 -0
- package/dist/skills.js +201 -36
- package/dist/skills.js.map +1 -1
- package/dist/store/annotations.d.ts.map +1 -1
- package/dist/store/annotations.js +18 -15
- package/dist/store/annotations.js.map +1 -1
- package/dist/store/confidence.d.ts +10 -0
- package/dist/store/confidence.d.ts.map +1 -1
- package/dist/store/confidence.js +28 -1
- package/dist/store/confidence.js.map +1 -1
- package/dist/store/db.d.ts +16 -0
- package/dist/store/db.d.ts.map +1 -1
- package/dist/store/db.js +121 -7
- package/dist/store/db.js.map +1 -1
- package/dist/store/schema.sql +25 -10
- package/dist/uninstall.d.ts +54 -0
- package/dist/uninstall.d.ts.map +1 -0
- package/dist/uninstall.js +795 -0
- package/dist/uninstall.js.map +1 -0
- package/docs/README.md +7 -6
- package/package.json +4 -3
|
@@ -0,0 +1,795 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, rmSync, existsSync, readdirSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { resolve, relative, join, extname, basename, dirname } from 'node:path';
|
|
3
|
+
import { execSync } from 'node:child_process';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
const START_MARKER = '<!-- milens:start -->';
|
|
6
|
+
const END_MARKER = '<!-- milens:end -->';
|
|
7
|
+
const INJECTED_FILES = [
|
|
8
|
+
'AGENTS.md',
|
|
9
|
+
'CLAUDE.md',
|
|
10
|
+
'.windsurfrules',
|
|
11
|
+
join('.github', 'copilot-instructions.md'),
|
|
12
|
+
join('.cursor', 'index.mdc'),
|
|
13
|
+
];
|
|
14
|
+
const MCP_CONFIG_FILES = [
|
|
15
|
+
{ file: '.mcp.json', editor: 'VS Code', key: 'mcpServers.milens' },
|
|
16
|
+
{ file: join('.vscode', 'mcp.json'), editor: 'VS Code (workspace)', key: 'mcpServers.milens' },
|
|
17
|
+
{ file: join('.cursor', 'mcp.json'), editor: 'Cursor', key: 'mcpServers.milens' },
|
|
18
|
+
{ file: join('.claude', 'mcp.json'), editor: 'Claude Code', key: 'mcpServers.milens' },
|
|
19
|
+
{ file: 'opencode.json', editor: 'OpenCode', key: 'mcp.milens' },
|
|
20
|
+
{ file: join('.zed', 'settings.json'), editor: 'Zed', key: 'context_servers.milens' },
|
|
21
|
+
{ file: '.cursorrules', editor: 'Cursor', key: 'mcpServers.milens' },
|
|
22
|
+
];
|
|
23
|
+
const GEN_DIR_PATTERNS = [
|
|
24
|
+
join('.cursor', 'rules'),
|
|
25
|
+
join('.claude', 'skills', 'generated'),
|
|
26
|
+
join('.claude', 'rules'),
|
|
27
|
+
join('.agents', 'skills'),
|
|
28
|
+
join('.github', 'instructions'),
|
|
29
|
+
];
|
|
30
|
+
export function uninstall(opts) {
|
|
31
|
+
const root = resolve(opts.rootPath);
|
|
32
|
+
// ── Scan Phase ──
|
|
33
|
+
const injectedBlocks = scanInjectedBlocks(root, opts.keepAgents);
|
|
34
|
+
const generatedFiles = scanGeneratedFiles(root);
|
|
35
|
+
const gitHookInfo = scanGitHooks(root);
|
|
36
|
+
const cronInfo = scanCronEntry();
|
|
37
|
+
const winTaskInfo = scanWinScheduledTask();
|
|
38
|
+
const dbInfo = scanDatabase(root);
|
|
39
|
+
const regInfo = scanRegistryEntry(root);
|
|
40
|
+
const mcpConfigs = scanMcpConfigs(root);
|
|
41
|
+
const pkgDep = scanPackageJson(root);
|
|
42
|
+
const envVars = scanEnvFiles(root);
|
|
43
|
+
const manualRefs = scanManualReferences(root);
|
|
44
|
+
const autoRemoved = [];
|
|
45
|
+
const interactiveItems = [];
|
|
46
|
+
let filesRemoved = 0;
|
|
47
|
+
let dirsRemoved = 0;
|
|
48
|
+
let blocksRemoved = 0;
|
|
49
|
+
let configsCleaned = 0;
|
|
50
|
+
// ── Auto-Remove Phase ──
|
|
51
|
+
if (!opts.dryRun && !opts.scanOnly) {
|
|
52
|
+
// Injected blocks
|
|
53
|
+
for (const fp of injectedBlocks) {
|
|
54
|
+
const r = removeInjectedBlock(join(root, fp));
|
|
55
|
+
if (r.removed) {
|
|
56
|
+
blocksRemoved++;
|
|
57
|
+
if (r.kept) {
|
|
58
|
+
autoRemoved.push({ file: fp, action: `removed milens block, kept user content` });
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
filesRemoved++;
|
|
62
|
+
autoRemoved.push({ file: fp, action: `deleted (milens content only)` });
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
// Generated files/dirs
|
|
67
|
+
for (const item of generatedFiles) {
|
|
68
|
+
const fullPath = join(root, item.path);
|
|
69
|
+
try {
|
|
70
|
+
if (item.type === 'dir') {
|
|
71
|
+
rmSync(fullPath, { recursive: true, force: true });
|
|
72
|
+
dirsRemoved++;
|
|
73
|
+
autoRemoved.push({ file: item.path, action: `deleted directory (${item.count} files)` });
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
rmSync(fullPath, { force: true });
|
|
77
|
+
filesRemoved++;
|
|
78
|
+
autoRemoved.push({ file: item.path, action: 'deleted' });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
autoRemoved.push({ file: item.path, action: 'failed to delete (permission denied)' });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Clean up empty parent dirs after removing generated files
|
|
86
|
+
for (const pattern of GEN_DIR_PATTERNS) {
|
|
87
|
+
cleanupEmptyDirs(join(root, pattern), root, autoRemoved, dirsRemoved);
|
|
88
|
+
}
|
|
89
|
+
// Git hooks
|
|
90
|
+
if (gitHookInfo) {
|
|
91
|
+
const result = removeGitHooks(root);
|
|
92
|
+
if (result) {
|
|
93
|
+
filesRemoved++;
|
|
94
|
+
autoRemoved.push({ file: gitHookInfo, action: result });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// Cron
|
|
98
|
+
if (cronInfo) {
|
|
99
|
+
const result = removeCronEntry();
|
|
100
|
+
if (result) {
|
|
101
|
+
autoRemoved.push({ file: 'crontab', action: result });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Windows Scheduled Task
|
|
105
|
+
if (winTaskInfo) {
|
|
106
|
+
const result = removeWinScheduledTask();
|
|
107
|
+
if (result) {
|
|
108
|
+
autoRemoved.push({ file: 'Windows Scheduled Task', action: result });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Database
|
|
112
|
+
if (dbInfo) {
|
|
113
|
+
const result = removeDatabase(root);
|
|
114
|
+
if (result) {
|
|
115
|
+
dirsRemoved++;
|
|
116
|
+
autoRemoved.push({ file: '.milens/', action: result });
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// Registry entry
|
|
120
|
+
if (regInfo) {
|
|
121
|
+
const result = removeRegistryEntry(root);
|
|
122
|
+
if (result) {
|
|
123
|
+
autoRemoved.push({ file: '~/.milens/registry.json', action: result });
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Purge: remove all global milens data
|
|
127
|
+
if (opts.purge) {
|
|
128
|
+
try {
|
|
129
|
+
const milensHome = join(homedir(), '.milens');
|
|
130
|
+
if (existsSync(milensHome)) {
|
|
131
|
+
rmSync(milensHome, { recursive: true, force: true });
|
|
132
|
+
dirsRemoved++;
|
|
133
|
+
autoRemoved.push({ file: '~/.milens/', action: 'purged all global data' });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
autoRemoved.push({ file: '~/.milens/', action: 'failed to purge (permission denied)' });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Interactive items — scan and report (actual removal via confirm)
|
|
141
|
+
if (!opts.skipConfirm && !opts.keepConfigs) {
|
|
142
|
+
for (const cfg of mcpConfigs) {
|
|
143
|
+
interactiveItems.push({
|
|
144
|
+
type: 'mcp-config',
|
|
145
|
+
file: cfg.file,
|
|
146
|
+
editor: cfg.editor,
|
|
147
|
+
detail: cfg.preview || `milens entry in ${cfg.file}`,
|
|
148
|
+
confirmed: false,
|
|
149
|
+
action: 'skipped (requires confirmation)',
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (!opts.skipConfirm) {
|
|
154
|
+
if (pkgDep) {
|
|
155
|
+
interactiveItems.push({
|
|
156
|
+
type: 'dependency',
|
|
157
|
+
file: 'package.json',
|
|
158
|
+
detail: pkgDep.type === 'dependency'
|
|
159
|
+
? `"milens" in dependencies (${pkgDep.version})`
|
|
160
|
+
: `"milens" in devDependencies (${pkgDep.version})`,
|
|
161
|
+
confirmed: false,
|
|
162
|
+
action: 'skipped (requires confirmation)',
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
for (const env of envVars) {
|
|
166
|
+
interactiveItems.push({
|
|
167
|
+
type: 'env-var',
|
|
168
|
+
file: env.file,
|
|
169
|
+
detail: env.vars.join(', '),
|
|
170
|
+
confirmed: false,
|
|
171
|
+
action: 'skipped (requires confirmation)',
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
// Dry run — just populate items for reporting
|
|
178
|
+
for (const fp of injectedBlocks) {
|
|
179
|
+
const fullPath = join(root, fp);
|
|
180
|
+
const content = tryRead(fullPath);
|
|
181
|
+
if (content) {
|
|
182
|
+
const before = content.slice(0, content.indexOf(START_MARKER)).trim();
|
|
183
|
+
const after = content.slice(content.indexOf(END_MARKER) + END_MARKER.length).trim();
|
|
184
|
+
if (!before && !after) {
|
|
185
|
+
autoRemoved.push({ file: fp, action: 'will delete file (milens content only)' });
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
autoRemoved.push({ file: fp, action: 'will remove milens block, keep user content' });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (const item of generatedFiles) {
|
|
193
|
+
autoRemoved.push({
|
|
194
|
+
file: item.path,
|
|
195
|
+
action: `will delete${item.type === 'dir' ? ` directory (${item.count} files)` : ''}`,
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
if (gitHookInfo)
|
|
199
|
+
autoRemoved.push({ file: gitHookInfo, action: 'will remove' });
|
|
200
|
+
if (cronInfo)
|
|
201
|
+
autoRemoved.push({ file: 'crontab', action: 'will remove milens entry' });
|
|
202
|
+
if (winTaskInfo)
|
|
203
|
+
autoRemoved.push({ file: 'Windows Scheduled Task', action: 'will remove' });
|
|
204
|
+
if (dbInfo)
|
|
205
|
+
autoRemoved.push({ file: '.milens/', action: 'will delete database' });
|
|
206
|
+
if (regInfo)
|
|
207
|
+
autoRemoved.push({ file: '~/.milens/registry.json', action: 'will remove entry' });
|
|
208
|
+
if (opts.purge)
|
|
209
|
+
autoRemoved.push({ file: '~/.milens/', action: 'will purge all global data' });
|
|
210
|
+
for (const cfg of mcpConfigs) {
|
|
211
|
+
interactiveItems.push({
|
|
212
|
+
type: 'mcp-config', file: cfg.file, editor: cfg.editor,
|
|
213
|
+
detail: cfg.preview || `milens entry`, confirmed: false,
|
|
214
|
+
action: 'requires confirmation',
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (pkgDep) {
|
|
218
|
+
interactiveItems.push({
|
|
219
|
+
type: 'dependency', file: 'package.json',
|
|
220
|
+
detail: pkgDep.type === 'dependency'
|
|
221
|
+
? `"milens" in dependencies (${pkgDep.version})`
|
|
222
|
+
: `"milens" in devDependencies (${pkgDep.version})`,
|
|
223
|
+
confirmed: false, action: 'requires confirmation',
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
for (const env of envVars) {
|
|
227
|
+
interactiveItems.push({
|
|
228
|
+
type: 'env-var', file: env.file, detail: env.vars.join(', '),
|
|
229
|
+
confirmed: false, action: 'requires confirmation',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
autoRemoved,
|
|
235
|
+
interactiveItems,
|
|
236
|
+
manualReview: manualRefs,
|
|
237
|
+
summary: {
|
|
238
|
+
filesRemoved,
|
|
239
|
+
dirsRemoved,
|
|
240
|
+
blocksRemoved,
|
|
241
|
+
configsCleaned,
|
|
242
|
+
manualRemaining: manualRefs.length,
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
// ── Scan Functions ──
|
|
247
|
+
function scanInjectedBlocks(rootPath, keepAgents) {
|
|
248
|
+
const results = [];
|
|
249
|
+
for (const relPath of INJECTED_FILES) {
|
|
250
|
+
if (keepAgents && (relPath === 'AGENTS.md' || relPath === 'CLAUDE.md'))
|
|
251
|
+
continue;
|
|
252
|
+
const fullPath = join(rootPath, relPath);
|
|
253
|
+
try {
|
|
254
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
255
|
+
if (content.includes(START_MARKER) && content.includes(END_MARKER)) {
|
|
256
|
+
results.push(relPath);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
catch { /* file doesn't exist */ }
|
|
260
|
+
}
|
|
261
|
+
return results;
|
|
262
|
+
}
|
|
263
|
+
function scanGeneratedFiles(rootPath) {
|
|
264
|
+
const results = [];
|
|
265
|
+
for (const pattern of GEN_DIR_PATTERNS) {
|
|
266
|
+
const dir = join(rootPath, pattern);
|
|
267
|
+
if (!existsSync(dir))
|
|
268
|
+
continue;
|
|
269
|
+
try {
|
|
270
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
271
|
+
const fullPath = join(dir, entry.name);
|
|
272
|
+
if (isMilensGenerated(fullPath)) {
|
|
273
|
+
if (entry.isDirectory()) {
|
|
274
|
+
let fileCount = 0;
|
|
275
|
+
try {
|
|
276
|
+
fileCount = countFilesRecursive(fullPath);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
fileCount = 1;
|
|
280
|
+
}
|
|
281
|
+
results.push({ path: relative(rootPath, fullPath).replace(/\\/g, '/'), type: 'dir', count: fileCount });
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
results.push({ path: relative(rootPath, fullPath).replace(/\\/g, '/'), type: 'file', count: 1 });
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
catch { /* can't read dir */ }
|
|
290
|
+
}
|
|
291
|
+
return results;
|
|
292
|
+
}
|
|
293
|
+
function isMilensGenerated(filePath) {
|
|
294
|
+
const name = basename(filePath);
|
|
295
|
+
if (name.startsWith('milens'))
|
|
296
|
+
return true;
|
|
297
|
+
if (name === 'milens.mdc')
|
|
298
|
+
return true;
|
|
299
|
+
if (name.endsWith('.instructions.md'))
|
|
300
|
+
return true;
|
|
301
|
+
try {
|
|
302
|
+
const content = readFileSync(filePath, 'utf-8').slice(0, 500);
|
|
303
|
+
if (content.includes('name: milens') || content.includes('Auto-generated by milens')) {
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
if (content.includes(START_MARKER))
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
catch { /* binary/unreadable */ }
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
function countFilesRecursive(dir) {
|
|
313
|
+
let count = 0;
|
|
314
|
+
try {
|
|
315
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
316
|
+
if (entry.isDirectory()) {
|
|
317
|
+
count += countFilesRecursive(join(dir, entry.name));
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
count++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
count = 1;
|
|
326
|
+
}
|
|
327
|
+
return count;
|
|
328
|
+
}
|
|
329
|
+
function scanGitHooks(rootPath) {
|
|
330
|
+
const hookPath = join(rootPath, '.git', 'hooks', 'pre-commit');
|
|
331
|
+
if (!existsSync(hookPath))
|
|
332
|
+
return null;
|
|
333
|
+
try {
|
|
334
|
+
const content = readFileSync(hookPath, 'utf-8');
|
|
335
|
+
if (content.includes('milens') && content.includes('Auto-installed by milens init')) {
|
|
336
|
+
return join('.git', 'hooks', 'pre-commit');
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
catch { /* can't read */ }
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
function scanCronEntry() {
|
|
343
|
+
try {
|
|
344
|
+
const current = execSync('crontab -l 2>/dev/null || echo ""', { encoding: 'utf-8', stdio: 'pipe' });
|
|
345
|
+
return current.includes('milens evolve');
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return false;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function scanWinScheduledTask() {
|
|
352
|
+
if (process.platform !== 'win32')
|
|
353
|
+
return false;
|
|
354
|
+
try {
|
|
355
|
+
execSync('schtasks /Query /TN "MilensEvolve"', { encoding: 'utf-8', stdio: 'pipe' });
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function scanDatabase(rootPath) {
|
|
363
|
+
return existsSync(join(rootPath, '.milens', 'milens.db'));
|
|
364
|
+
}
|
|
365
|
+
function scanRegistryEntry(rootPath) {
|
|
366
|
+
try {
|
|
367
|
+
const { RepoRegistry } = require('./store/registry.js');
|
|
368
|
+
const reg = new RepoRegistry();
|
|
369
|
+
const entry = reg.findByRoot(resolve(rootPath));
|
|
370
|
+
return entry ? entry.rootPath : null;
|
|
371
|
+
}
|
|
372
|
+
catch {
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
function scanMcpConfigs(rootPath) {
|
|
377
|
+
const results = [];
|
|
378
|
+
for (const cfg of MCP_CONFIG_FILES) {
|
|
379
|
+
const fullPath = join(rootPath, cfg.file);
|
|
380
|
+
if (!existsSync(fullPath))
|
|
381
|
+
continue;
|
|
382
|
+
try {
|
|
383
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
384
|
+
if (!content.includes('"milens"') && !content.includes("'milens'"))
|
|
385
|
+
continue;
|
|
386
|
+
const idx = content.indexOf('"milens"');
|
|
387
|
+
const previewStart = Math.max(0, idx - 20);
|
|
388
|
+
const previewEnd = Math.min(content.length, idx + 120);
|
|
389
|
+
let preview = content.slice(previewStart, previewEnd).replace(/\n/g, ' ').trim();
|
|
390
|
+
// Find the milens object
|
|
391
|
+
const braceIdx = content.indexOf('{', idx);
|
|
392
|
+
if (braceIdx !== -1) {
|
|
393
|
+
let depth = 0;
|
|
394
|
+
let endIdx = braceIdx;
|
|
395
|
+
for (let i = braceIdx; i < Math.min(content.length, braceIdx + 300); i++) {
|
|
396
|
+
if (content[i] === '{')
|
|
397
|
+
depth++;
|
|
398
|
+
if (content[i] === '}') {
|
|
399
|
+
depth--;
|
|
400
|
+
if (depth === 0) {
|
|
401
|
+
endIdx = i + 1;
|
|
402
|
+
break;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
preview = content.slice(idx, endIdx).replace(/\n/g, ' ').trim();
|
|
407
|
+
}
|
|
408
|
+
results.push({ file: cfg.file, editor: cfg.editor, preview: preview.slice(0, 150) });
|
|
409
|
+
}
|
|
410
|
+
catch { /* parse error */ }
|
|
411
|
+
}
|
|
412
|
+
return results;
|
|
413
|
+
}
|
|
414
|
+
function scanPackageJson(rootPath) {
|
|
415
|
+
const pkgPath = join(rootPath, 'package.json');
|
|
416
|
+
if (!existsSync(pkgPath))
|
|
417
|
+
return null;
|
|
418
|
+
try {
|
|
419
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
420
|
+
if (pkg.dependencies?.milens) {
|
|
421
|
+
return { type: 'dependency', version: pkg.dependencies.milens };
|
|
422
|
+
}
|
|
423
|
+
if (pkg.devDependencies?.milens) {
|
|
424
|
+
return { type: 'devDependency', version: pkg.devDependencies.milens };
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
catch { /* parse error */ }
|
|
428
|
+
return null;
|
|
429
|
+
}
|
|
430
|
+
function scanEnvFiles(rootPath) {
|
|
431
|
+
const envFiles = ['.env', '.env.local', '.env.development'];
|
|
432
|
+
const results = [];
|
|
433
|
+
for (const envFile of envFiles) {
|
|
434
|
+
const fullPath = join(rootPath, envFile);
|
|
435
|
+
if (!existsSync(fullPath))
|
|
436
|
+
continue;
|
|
437
|
+
try {
|
|
438
|
+
const lines = readFileSync(fullPath, 'utf-8').split('\n');
|
|
439
|
+
const milensLines = [];
|
|
440
|
+
for (const line of lines) {
|
|
441
|
+
if (/MILENS_/i.test(line)) {
|
|
442
|
+
milensLines.push(line.trim());
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (milensLines.length > 0) {
|
|
446
|
+
results.push({ file: envFile, vars: milensLines });
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
catch { /* can't read */ }
|
|
450
|
+
}
|
|
451
|
+
return results;
|
|
452
|
+
}
|
|
453
|
+
function scanManualReferences(rootPath) {
|
|
454
|
+
const refs = [];
|
|
455
|
+
const skipDirs = ['node_modules', '.git', '.milens', 'dist', 'build', '.next', '__pycache__'];
|
|
456
|
+
const skipPrefixes = [
|
|
457
|
+
join(rootPath, '.cursor', 'rules'),
|
|
458
|
+
join(rootPath, '.claude', 'skills', 'generated'),
|
|
459
|
+
join(rootPath, '.claude', 'rules'),
|
|
460
|
+
join(rootPath, '.agents', 'skills'),
|
|
461
|
+
join(rootPath, '.github', 'instructions'),
|
|
462
|
+
];
|
|
463
|
+
const isSkipped = (p) => {
|
|
464
|
+
if (skipDirs.some(d => p.includes(`${join('', d)}`)))
|
|
465
|
+
return true;
|
|
466
|
+
if (skipPrefixes.some(prefix => p.startsWith(prefix)))
|
|
467
|
+
return true;
|
|
468
|
+
if (MCP_CONFIG_FILES.some(c => p.endsWith(c.file)))
|
|
469
|
+
return true;
|
|
470
|
+
if (p.endsWith(join('', '.env')) || p.endsWith(join('', '.env.local')) || p.endsWith(join('', '.env.development')))
|
|
471
|
+
return true;
|
|
472
|
+
return false;
|
|
473
|
+
};
|
|
474
|
+
const extensions = ['.md', '.mdx', '.yml', '.yaml', '.toml', '.txt', '.sh'];
|
|
475
|
+
const candidateFiles = collectFiles(rootPath, extensions, skipDirs);
|
|
476
|
+
for (const file of candidateFiles) {
|
|
477
|
+
if (isSkipped(file))
|
|
478
|
+
continue;
|
|
479
|
+
try {
|
|
480
|
+
const lines = readFileSync(file, 'utf-8').split('\n');
|
|
481
|
+
for (let i = 0; i < lines.length; i++) {
|
|
482
|
+
if (lines[i].toLowerCase().includes('milens')) {
|
|
483
|
+
refs.push({
|
|
484
|
+
file: relative(rootPath, file).replace(/\\/g, '/'),
|
|
485
|
+
line: i + 1,
|
|
486
|
+
match: lines[i].trim().slice(0, 120),
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
catch { /* binary/unreadable */ }
|
|
492
|
+
}
|
|
493
|
+
return refs;
|
|
494
|
+
}
|
|
495
|
+
function collectFiles(root, extensions, skipDirs) {
|
|
496
|
+
const results = [];
|
|
497
|
+
const walk = (dir) => {
|
|
498
|
+
try {
|
|
499
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
500
|
+
const fullPath = join(dir, entry.name);
|
|
501
|
+
const rel = relative(root, fullPath);
|
|
502
|
+
if (entry.isDirectory()) {
|
|
503
|
+
if (skipDirs.includes(entry.name))
|
|
504
|
+
continue;
|
|
505
|
+
if (entry.name.startsWith('.'))
|
|
506
|
+
continue;
|
|
507
|
+
walk(fullPath);
|
|
508
|
+
}
|
|
509
|
+
else if (entry.isFile()) {
|
|
510
|
+
const ext = extname(entry.name);
|
|
511
|
+
if (extensions.includes(ext)) {
|
|
512
|
+
results.push(fullPath);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
catch { /* can't read dir */ }
|
|
518
|
+
};
|
|
519
|
+
walk(root);
|
|
520
|
+
return results;
|
|
521
|
+
}
|
|
522
|
+
// ── Remove Functions ──
|
|
523
|
+
export function removeInjectedBlock(filePath) {
|
|
524
|
+
try {
|
|
525
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
526
|
+
const startIdx = content.indexOf(START_MARKER);
|
|
527
|
+
const endIdx = content.indexOf(END_MARKER);
|
|
528
|
+
if (startIdx === -1 || endIdx === -1)
|
|
529
|
+
return { removed: false, kept: false };
|
|
530
|
+
let before = content.slice(0, startIdx);
|
|
531
|
+
let after = content.slice(endIdx + END_MARKER.length);
|
|
532
|
+
before = before.replace(/\n{2,}$/, '\n');
|
|
533
|
+
after = after.replace(/^\n{2,}/, '\n');
|
|
534
|
+
const cleaned = (before + after).trim();
|
|
535
|
+
if (!cleaned) {
|
|
536
|
+
rmSync(filePath, { force: true });
|
|
537
|
+
return { removed: true, kept: false };
|
|
538
|
+
}
|
|
539
|
+
writeFileSync(filePath, cleaned + '\n');
|
|
540
|
+
return { removed: true, kept: true };
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
return { removed: false, kept: false };
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
export function removeDatabase(rootPath) {
|
|
547
|
+
const milensDir = join(rootPath, '.milens');
|
|
548
|
+
if (existsSync(milensDir)) {
|
|
549
|
+
rmSync(milensDir, { recursive: true, force: true });
|
|
550
|
+
return 'removed .milens/ (database + configs)';
|
|
551
|
+
}
|
|
552
|
+
return '';
|
|
553
|
+
}
|
|
554
|
+
export function removeRegistryEntry(rootPath) {
|
|
555
|
+
try {
|
|
556
|
+
const { RepoRegistry } = require('./store/registry.js');
|
|
557
|
+
const reg = new RepoRegistry();
|
|
558
|
+
const absolute = resolve(rootPath);
|
|
559
|
+
const removed = reg.remove(absolute);
|
|
560
|
+
return removed ? `removed registry entry for ${absolute}` : '';
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
return '';
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
export function removeGitHooks(rootPath) {
|
|
567
|
+
const hookPath = join(rootPath, '.git', 'hooks', 'pre-commit');
|
|
568
|
+
if (!existsSync(hookPath))
|
|
569
|
+
return '';
|
|
570
|
+
try {
|
|
571
|
+
const content = readFileSync(hookPath, 'utf-8');
|
|
572
|
+
if (!content.includes('milens') || !content.includes('Auto-installed by milens init')) {
|
|
573
|
+
return 'git hook exists but was not installed by milens — kept';
|
|
574
|
+
}
|
|
575
|
+
rmSync(hookPath, { force: true });
|
|
576
|
+
return 'removed milens-installed pre-commit hook';
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return '';
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
export function removeCronEntry() {
|
|
583
|
+
try {
|
|
584
|
+
const current = execSync('crontab -l 2>/dev/null || echo ""', { encoding: 'utf-8', stdio: 'pipe' });
|
|
585
|
+
const lines = current.split('\n');
|
|
586
|
+
const filtered = lines.filter(line => !line.includes('milens evolve'));
|
|
587
|
+
if (filtered.length === lines.length)
|
|
588
|
+
return '';
|
|
589
|
+
const tmpFile = join(homedir(), '.milens', 'crontab.tmp');
|
|
590
|
+
mkdirSync(dirname(tmpFile), { recursive: true });
|
|
591
|
+
writeFileSync(tmpFile, filtered.join('\n').trim() + '\n');
|
|
592
|
+
execSync(`crontab "${tmpFile}"`, { stdio: 'pipe' });
|
|
593
|
+
rmSync(tmpFile, { force: true });
|
|
594
|
+
return 'removed milens cron job';
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
return '';
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
export function removeWinScheduledTask() {
|
|
601
|
+
if (process.platform !== 'win32')
|
|
602
|
+
return '';
|
|
603
|
+
try {
|
|
604
|
+
execSync('schtasks /Delete /TN "MilensEvolve" /F', { stdio: 'pipe' });
|
|
605
|
+
return 'removed Windows Scheduled Task "MilensEvolve"';
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
return '';
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
export function removeMcpEntry(rootPath, configFile) {
|
|
612
|
+
const fullPath = join(rootPath, configFile);
|
|
613
|
+
if (!existsSync(fullPath))
|
|
614
|
+
return false;
|
|
615
|
+
try {
|
|
616
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
617
|
+
const parsed = JSON.parse(content);
|
|
618
|
+
let removed = false;
|
|
619
|
+
if (configFile === 'opencode.json' && parsed.mcp?.milens) {
|
|
620
|
+
delete parsed.mcp.milens;
|
|
621
|
+
if (Object.keys(parsed.mcp).length === 0)
|
|
622
|
+
delete parsed.mcp;
|
|
623
|
+
removed = true;
|
|
624
|
+
}
|
|
625
|
+
else if (configFile === '.zed' + join('', 'settings.json') && parsed.context_servers?.milens) {
|
|
626
|
+
delete parsed.context_servers.milens;
|
|
627
|
+
if (Object.keys(parsed.context_servers).length === 0)
|
|
628
|
+
delete parsed.context_servers;
|
|
629
|
+
removed = true;
|
|
630
|
+
}
|
|
631
|
+
else if (parsed.mcpServers?.milens) {
|
|
632
|
+
delete parsed.mcpServers.milens;
|
|
633
|
+
if (Object.keys(parsed.mcpServers).length === 0)
|
|
634
|
+
delete parsed.mcpServers;
|
|
635
|
+
removed = true;
|
|
636
|
+
}
|
|
637
|
+
if (removed) {
|
|
638
|
+
writeFileSync(fullPath, JSON.stringify(parsed, null, 2) + '\n');
|
|
639
|
+
}
|
|
640
|
+
return removed;
|
|
641
|
+
}
|
|
642
|
+
catch {
|
|
643
|
+
return false;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
export function removePackageDep(rootPath) {
|
|
647
|
+
try {
|
|
648
|
+
execSync('npm uninstall milens', { cwd: rootPath, stdio: 'pipe' });
|
|
649
|
+
return 'removed milens from package.json';
|
|
650
|
+
}
|
|
651
|
+
catch {
|
|
652
|
+
// Fall back to manual removal
|
|
653
|
+
const pkgPath = join(rootPath, 'package.json');
|
|
654
|
+
if (!existsSync(pkgPath))
|
|
655
|
+
return '';
|
|
656
|
+
try {
|
|
657
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
658
|
+
let removed = false;
|
|
659
|
+
if (pkg.dependencies?.milens) {
|
|
660
|
+
delete pkg.dependencies.milens;
|
|
661
|
+
removed = true;
|
|
662
|
+
}
|
|
663
|
+
if (pkg.devDependencies?.milens) {
|
|
664
|
+
delete pkg.devDependencies.milens;
|
|
665
|
+
removed = true;
|
|
666
|
+
}
|
|
667
|
+
if (removed) {
|
|
668
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
|
|
669
|
+
return 'removed milens from package.json (manual)';
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
catch { /* parse error */ }
|
|
673
|
+
return '';
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
export function removeEnvVars(rootPath, envFile) {
|
|
677
|
+
const fullPath = join(rootPath, envFile);
|
|
678
|
+
if (!existsSync(fullPath))
|
|
679
|
+
return '';
|
|
680
|
+
try {
|
|
681
|
+
const lines = readFileSync(fullPath, 'utf-8').split('\n');
|
|
682
|
+
const filtered = lines.filter(line => !/MILENS_/i.test(line));
|
|
683
|
+
const cleaned = filtered.join('\n').trim();
|
|
684
|
+
if (cleaned) {
|
|
685
|
+
writeFileSync(fullPath, cleaned + '\n');
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
rmSync(fullPath, { force: true });
|
|
689
|
+
}
|
|
690
|
+
return `cleaned MILENS_* vars from ${envFile}`;
|
|
691
|
+
}
|
|
692
|
+
catch {
|
|
693
|
+
return '';
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
// ── Helpers ──
|
|
697
|
+
function cleanupEmptyDirs(dirPath, rootPath, items, _dirsCount) {
|
|
698
|
+
try {
|
|
699
|
+
if (!existsSync(dirPath))
|
|
700
|
+
return;
|
|
701
|
+
if (readdirSync(dirPath).length === 0) {
|
|
702
|
+
rmSync(dirPath, { recursive: true, force: true });
|
|
703
|
+
items.push({ file: relative(rootPath, dirPath).replace(/\\/g, '/'), action: 'cleaned empty directory' });
|
|
704
|
+
}
|
|
705
|
+
// Check parent
|
|
706
|
+
const parent = dirname(dirPath);
|
|
707
|
+
if (parent !== rootPath && existsSync(parent)) {
|
|
708
|
+
try {
|
|
709
|
+
if (readdirSync(parent).length === 0) {
|
|
710
|
+
rmSync(parent, { recursive: true, force: true });
|
|
711
|
+
items.push({ file: relative(rootPath, parent).replace(/\\/g, '/'), action: 'cleaned empty directory' });
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
catch { /* parent may not exist */ }
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
catch { /* dir doesn't exist */ }
|
|
718
|
+
}
|
|
719
|
+
function tryRead(filePath) {
|
|
720
|
+
try {
|
|
721
|
+
return readFileSync(filePath, 'utf-8');
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
// ── Output Formatting ──
|
|
728
|
+
export function formatUninstallOutput(result, dryRun, scanOnly) {
|
|
729
|
+
const lines = [];
|
|
730
|
+
const header = dryRun
|
|
731
|
+
? 'Milens Uninstall — Dry Run'
|
|
732
|
+
: 'Milens Uninstall';
|
|
733
|
+
const sub = dryRun ? '(No files will be modified)' : scanOnly ? '(Scan only)' : '';
|
|
734
|
+
lines.push('╔════════════════════════════════════════════════════╗');
|
|
735
|
+
lines.push(`║ ${padCenter(header, 48)}║`);
|
|
736
|
+
if (sub)
|
|
737
|
+
lines.push(`║ ${padCenter(sub, 48)}║`);
|
|
738
|
+
lines.push('╚════════════════════════════════════════════════════╝');
|
|
739
|
+
lines.push('');
|
|
740
|
+
// Step 1: Auto-remove section
|
|
741
|
+
if (result.autoRemoved.length > 0) {
|
|
742
|
+
const label = dryRun ? 'WILL AUTO-REMOVE' : 'AUTO-REMOVED';
|
|
743
|
+
lines.push(`📁 ${label} (${result.autoRemoved.length} items)`);
|
|
744
|
+
lines.push('');
|
|
745
|
+
for (const item of result.autoRemoved) {
|
|
746
|
+
lines.push(` ✗ ${item.file} — ${item.action}`);
|
|
747
|
+
}
|
|
748
|
+
lines.push('');
|
|
749
|
+
lines.push('━'.repeat(50));
|
|
750
|
+
lines.push('');
|
|
751
|
+
}
|
|
752
|
+
// Step 2: Interactive items
|
|
753
|
+
if (result.interactiveItems.length > 0) {
|
|
754
|
+
const label = scanOnly ? 'SCAN RESULTS' : dryRun ? 'NEEDS CONFIRMATION' : 'INTERACTIVE';
|
|
755
|
+
lines.push(`⚠️ ${label} (${result.interactiveItems.length} items)`);
|
|
756
|
+
lines.push('');
|
|
757
|
+
for (const item of result.interactiveItems) {
|
|
758
|
+
const prefix = item.confirmed ? '✓' : '?';
|
|
759
|
+
const editorInfo = item.editor ? ` — ${item.editor}` : '';
|
|
760
|
+
lines.push(` ${prefix} ${item.file}${editorInfo}: ${item.detail}`);
|
|
761
|
+
}
|
|
762
|
+
lines.push('');
|
|
763
|
+
lines.push('━'.repeat(50));
|
|
764
|
+
lines.push('');
|
|
765
|
+
}
|
|
766
|
+
// Step 3: Manual review
|
|
767
|
+
if (result.manualReview.length > 0) {
|
|
768
|
+
lines.push(`📋 MANUAL REVIEW (${result.manualReview.length} files — your content, cannot auto-remove)`);
|
|
769
|
+
lines.push('');
|
|
770
|
+
for (const ref of result.manualReview.slice(0, 15)) {
|
|
771
|
+
lines.push(` ${ref.file}:${ref.line} "${ref.match}"`);
|
|
772
|
+
}
|
|
773
|
+
if (result.manualReview.length > 15) {
|
|
774
|
+
lines.push(` ... and ${result.manualReview.length - 15} more references`);
|
|
775
|
+
}
|
|
776
|
+
lines.push('');
|
|
777
|
+
lines.push('━'.repeat(50));
|
|
778
|
+
lines.push('');
|
|
779
|
+
}
|
|
780
|
+
// Summary
|
|
781
|
+
if (!scanOnly) {
|
|
782
|
+
lines.push('Summary:');
|
|
783
|
+
lines.push(` Auto-remove: ${result.autoRemoved.length} items (injected blocks, generated files, hooks, DB, cron)`);
|
|
784
|
+
lines.push(` Interactive: ${result.interactiveItems.length} items (MCP configs, deps, env)`);
|
|
785
|
+
lines.push(` Manual: ${result.summary.manualRemaining} files to review`);
|
|
786
|
+
}
|
|
787
|
+
return lines.join('\n');
|
|
788
|
+
}
|
|
789
|
+
function padCenter(text, width) {
|
|
790
|
+
const pad = Math.max(0, width - text.length);
|
|
791
|
+
const left = Math.floor(pad / 2);
|
|
792
|
+
const right = pad - left;
|
|
793
|
+
return ' '.repeat(left) + text + ' '.repeat(right);
|
|
794
|
+
}
|
|
795
|
+
//# sourceMappingURL=uninstall.js.map
|