@iyowei/sweep-node-modules 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,1247 @@
1
+ // src/cli.ts
2
+ import { lstat as lstat2, mkdir, realpath as realpath3 } from "node:fs/promises";
3
+ import { homedir as homedir4 } from "node:os";
4
+ import { basename, dirname } from "node:path";
5
+
6
+ // src/config.ts
7
+ import { readFile } from "node:fs/promises";
8
+ import { homedir } from "node:os";
9
+ import { posix, win32 } from "node:path";
10
+ function platformDefaultPath(platform, env, home) {
11
+ if (platform === "win32") {
12
+ const roaming = env.APPDATA || win32.join(env.USERPROFILE || home, "AppData", "Roaming");
13
+ return win32.join(roaming, "sweep-node-modules", "config.json");
14
+ }
15
+ return posix.join(home, ".config", "sweep-node-modules", "config.json");
16
+ }
17
+ function resolveConfigPath(options) {
18
+ if (options.flag) {
19
+ return { path: options.flag, source: "flag" };
20
+ }
21
+ const env = options.env ?? process.env;
22
+ if (env.SWEEP_NM_CONFIG) {
23
+ return { path: env.SWEEP_NM_CONFIG, source: "env" };
24
+ }
25
+ const platform = options.platform ?? process.platform;
26
+ const home = options.homedir ?? homedir();
27
+ return {
28
+ path: platformDefaultPath(platform, env, home),
29
+ source: "platform-default"
30
+ };
31
+ }
32
+ function describeActual(value) {
33
+ if (value === undefined)
34
+ return "缺失";
35
+ if (value === null)
36
+ return "null";
37
+ if (Array.isArray(value))
38
+ return "array";
39
+ return typeof value;
40
+ }
41
+ function fieldError(container, field, optional) {
42
+ const value = container[field];
43
+ if (value === undefined) {
44
+ return optional ? null : `${field} 应为字符串数组 (实际: 缺失)`;
45
+ }
46
+ if (!Array.isArray(value)) {
47
+ return `${field} 应为字符串数组 (实际: ${describeActual(value)})`;
48
+ }
49
+ const badIndex = value.findIndex((item) => typeof item !== "string");
50
+ if (badIndex !== -1) {
51
+ return `${field} 第 ${badIndex + 1} 项应为字符串 (实际: ${describeActual(value[badIndex])})`;
52
+ }
53
+ return null;
54
+ }
55
+ function shapeError(value) {
56
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
57
+ return `顶层应为对象 (实际: ${describeActual(value)})`;
58
+ }
59
+ const candidate = value;
60
+ const rootsError = fieldError(candidate, "roots", false);
61
+ if (rootsError !== null)
62
+ return rootsError;
63
+ return fieldError(candidate, "exclude", true);
64
+ }
65
+ async function loadConfig(path) {
66
+ let text;
67
+ try {
68
+ text = await readFile(path, "utf8");
69
+ } catch (error) {
70
+ if (error.code === "ENOENT")
71
+ return { state: "absent" };
72
+ throw new Error(`配置读取失败 (${path}): ${error.message}`);
73
+ }
74
+ let data;
75
+ try {
76
+ data = JSON.parse(text);
77
+ } catch (error) {
78
+ throw new Error(`配置损坏 (${path}): JSON 解析失败: ${error.message}`);
79
+ }
80
+ const error = shapeError(data);
81
+ if (error !== null) {
82
+ throw new Error(`配置损坏 (${path}): ${error}`);
83
+ }
84
+ const raw = data;
85
+ return {
86
+ state: "ok",
87
+ config: { roots: raw.roots, exclude: raw.exclude ?? [] }
88
+ };
89
+ }
90
+ async function loadResolvedConfig(resolved) {
91
+ const result = await loadConfig(resolved.path);
92
+ if (result.state === "absent" && resolved.source !== "platform-default") {
93
+ throw new Error(`配置不存在 (${resolved.path}), 请检查路径`);
94
+ }
95
+ return result;
96
+ }
97
+ function mergeExcludes(configExclude, cliExclude) {
98
+ return [...new Set([...configExclude, ...cliExclude])];
99
+ }
100
+
101
+ // src/delete.ts
102
+ import { lstat, rm } from "node:fs/promises";
103
+ import { isAbsolute, join, relative, sep } from "node:path";
104
+ var ERROR_HINTS = {
105
+ EACCES: "权限不足, 拒绝删除",
106
+ EPERM: "操作不被允许",
107
+ EBUSY: "目标被占用",
108
+ ENOTEMPTY: "目录非空",
109
+ EROFS: "目标位于只读文件系统"
110
+ };
111
+ var PARTIAL_DELETION_HINT = "注意: 目录内容可能已被部分或全部删除, 请复查";
112
+ function describeCode(error) {
113
+ const fsError = error;
114
+ const code = fsError?.code;
115
+ if (typeof code !== "string" || code === "") {
116
+ return `未知错误: ${fsError?.message ?? String(error)}`;
117
+ }
118
+ return `${code}: ${ERROR_HINTS[code] ?? "删除未成功"}`;
119
+ }
120
+ function describeRemovalError(target, error) {
121
+ return `${describeCode(error)} (目标: ${target}; ${PARTIAL_DELETION_HINT})`;
122
+ }
123
+ function describeReviewError(target, path, error) {
124
+ return `安全复核未完成 (${describeCode(error)}) (目标: ${target}; 组件: ${path}; 未执行删除)`;
125
+ }
126
+ function describeVanishedReview(target, path) {
127
+ return `安全复核未完成 (ENOENT: 路径组件消失) (目标: ${target}; 组件: ${path}; 请复查目标是否仍存在; 未执行删除; 若目标确已不存在, 可忽略此条)`;
128
+ }
129
+ function isUnder(root, target) {
130
+ const rel = relative(root, target);
131
+ if (rel === "" || isAbsolute(rel))
132
+ return false;
133
+ return rel !== ".." && !rel.startsWith(`..${sep}`);
134
+ }
135
+ function componentChain(root, target) {
136
+ const chain = [root];
137
+ let cursor = root;
138
+ for (const part of relative(root, target).split(sep).slice(0, -1)) {
139
+ cursor = join(cursor, part);
140
+ chain.push(cursor);
141
+ }
142
+ return chain;
143
+ }
144
+ async function reviewComponents(chain) {
145
+ for (const path of chain) {
146
+ try {
147
+ const stats = await lstat(path);
148
+ if (!stats.isDirectory())
149
+ return { kind: "unsafe", path };
150
+ } catch (error) {
151
+ const code = error?.code;
152
+ if (code === "ENOENT")
153
+ return { kind: "vanished", path };
154
+ return { kind: "unverified", path, error };
155
+ }
156
+ }
157
+ return null;
158
+ }
159
+ async function removeTargets(targets, options) {
160
+ const result = { removed: [], missing: [], failed: [] };
161
+ const roots = options?.roots ?? [];
162
+ for (const target of targets) {
163
+ const root = roots.find((candidate) => isUnder(candidate, target));
164
+ if (root === undefined) {
165
+ result.aborted = {
166
+ target,
167
+ reason: `安全复核失败 (目标不在任何 roots 之下): ${target}`
168
+ };
169
+ break;
170
+ }
171
+ const finding = await reviewComponents(componentChain(root, target));
172
+ if (finding !== null) {
173
+ if (finding.kind === "vanished") {
174
+ result.failed.push({
175
+ target,
176
+ error: describeVanishedReview(target, finding.path)
177
+ });
178
+ continue;
179
+ }
180
+ if (finding.kind === "unsafe") {
181
+ result.aborted = {
182
+ target,
183
+ reason: `安全复核失败 (路径组件被替换): ${finding.path}`
184
+ };
185
+ break;
186
+ }
187
+ result.failed.push({
188
+ target,
189
+ error: describeReviewError(target, finding.path, finding.error)
190
+ });
191
+ continue;
192
+ }
193
+ try {
194
+ await rm(target, { recursive: true, force: false });
195
+ result.removed.push(target);
196
+ } catch (error) {
197
+ if (error?.code === "ENOENT") {
198
+ result.missing.push(target);
199
+ continue;
200
+ }
201
+ result.failed.push({
202
+ target,
203
+ error: describeRemovalError(target, error)
204
+ });
205
+ }
206
+ }
207
+ return result;
208
+ }
209
+
210
+ // src/guard.ts
211
+ import { realpath } from "node:fs/promises";
212
+ import { homedir as homedir2 } from "node:os";
213
+ import { posix as posix2, win32 as win322 } from "node:path";
214
+ var POSIX_STYLE = {
215
+ name: "posix",
216
+ ops: posix2,
217
+ caseInsensitive: false
218
+ };
219
+ var WIN32_STYLE = {
220
+ name: "win32",
221
+ ops: win322,
222
+ caseInsensitive: true
223
+ };
224
+ function nativeStyle() {
225
+ return process.platform === "win32" ? WIN32_STYLE : POSIX_STYLE;
226
+ }
227
+ function fold(p, style) {
228
+ return style.caseInsensitive ? p.toLowerCase() : p;
229
+ }
230
+ function dedupeKey(realPath, style) {
231
+ return fold(realPath, style);
232
+ }
233
+ function hasNodeModulesLeaf(target, style) {
234
+ return fold(style.ops.basename(target), style) === "node_modules";
235
+ }
236
+ function insideAnyRoot(realPath, roots, style) {
237
+ return roots.some((root) => {
238
+ const rel = style.ops.relative(fold(root, style), fold(realPath, style));
239
+ if (rel === "" || style.ops.isAbsolute(rel))
240
+ return false;
241
+ return rel !== ".." && !rel.startsWith(`..${style.ops.sep}`);
242
+ });
243
+ }
244
+ function isFilesystemRootBody(realPath, style) {
245
+ return fold(style.ops.parse(realPath).root, style) === fold(realPath, style);
246
+ }
247
+ function isHomeBody(realPath, home, style) {
248
+ return home !== null && dedupeKey(realPath, style) === dedupeKey(home, style);
249
+ }
250
+ async function tryRealpath(p) {
251
+ try {
252
+ return await realpath(p);
253
+ } catch {
254
+ return null;
255
+ }
256
+ }
257
+ async function validateTargets(targets, options) {
258
+ const style = options.style ?? nativeStyle();
259
+ const homeOption = options.home === undefined ? homedir2() : options.home;
260
+ const roots = [];
261
+ for (const root of options.roots) {
262
+ const real = await tryRealpath(root);
263
+ if (real !== null)
264
+ roots.push(real);
265
+ }
266
+ const home = homeOption === null ? null : await tryRealpath(homeOption) ?? homeOption;
267
+ const accepted = [];
268
+ const rejected = [];
269
+ const seen = new Set;
270
+ for (const target of targets) {
271
+ if (!hasNodeModulesLeaf(target, style)) {
272
+ rejected.push({ target, reason: "路径末段不是 node_modules" });
273
+ continue;
274
+ }
275
+ const real = await tryRealpath(target);
276
+ if (real === null) {
277
+ rejected.push({ target, reason: "realpath 失败 (目标不存在或不可读)" });
278
+ continue;
279
+ }
280
+ if (isFilesystemRootBody(real, style)) {
281
+ rejected.push({ target, reason: "目标是文件系统根本体" });
282
+ continue;
283
+ }
284
+ if (isHomeBody(real, home, style)) {
285
+ rejected.push({ target, reason: "目标是 home 本体" });
286
+ continue;
287
+ }
288
+ if (!hasNodeModulesLeaf(real, style)) {
289
+ rejected.push({ target, reason: "realpath 后的末段不是 node_modules" });
290
+ continue;
291
+ }
292
+ if (!insideAnyRoot(real, roots, style)) {
293
+ rejected.push({ target, reason: "realpath 后不在任何 root 之下" });
294
+ continue;
295
+ }
296
+ const key = dedupeKey(real, style);
297
+ if (seen.has(key)) {
298
+ rejected.push({ target, reason: "重复目标 (realpath 去重)" });
299
+ continue;
300
+ }
301
+ seen.add(key);
302
+ accepted.push(real);
303
+ }
304
+ return { accepted, rejected };
305
+ }
306
+
307
+ // src/init.ts
308
+ import { homedir as homedir3 } from "node:os";
309
+ import { join as join2 } from "node:path";
310
+ import { createInterface } from "node:readline/promises";
311
+ var WHITESPACE = /\s/;
312
+ function expandHome(token) {
313
+ if (token === "~")
314
+ return homedir3();
315
+ if (token.startsWith("~/") || token.startsWith("~\\"))
316
+ return join2(homedir3(), token.slice(2));
317
+ return token;
318
+ }
319
+ function splitAnswer(answer) {
320
+ const items = [];
321
+ let current = "";
322
+ let openQuote = null;
323
+ for (let index = 0;index < answer.length; index += 1) {
324
+ const char = answer[index];
325
+ if (openQuote !== null) {
326
+ if (char === openQuote) {
327
+ openQuote = null;
328
+ continue;
329
+ }
330
+ current += char;
331
+ continue;
332
+ }
333
+ if (char === '"' || char === "'") {
334
+ if (answer.indexOf(char, index + 1) !== -1) {
335
+ openQuote = char;
336
+ continue;
337
+ }
338
+ current += char;
339
+ continue;
340
+ }
341
+ if (char === "," || char === "," || WHITESPACE.test(char)) {
342
+ if (current.length > 0)
343
+ items.push(current);
344
+ current = "";
345
+ continue;
346
+ }
347
+ current += char;
348
+ }
349
+ if (current.length > 0)
350
+ items.push(current);
351
+ return items;
352
+ }
353
+ function parseList(answer, fallback) {
354
+ if (answer === null)
355
+ return [...fallback];
356
+ const items = splitAnswer(answer).map(expandHome);
357
+ return items.length > 0 ? items : [...fallback];
358
+ }
359
+ async function runInit(deps) {
360
+ const { configPath, fileExists, writeFile, io } = deps;
361
+ function cancelled() {
362
+ io.print("已取消, 未写入配置");
363
+ return { state: "cancelled" };
364
+ }
365
+ if (await fileExists(configPath)) {
366
+ const overwrite = await io.confirm(`配置已存在 (${configPath}), 是否覆盖?`, false);
367
+ if (!overwrite)
368
+ return { state: "declined-overwrite" };
369
+ }
370
+ io.print("首次使用, 先确定扫描范围");
371
+ const cwd = process.cwd();
372
+ let roots = [];
373
+ for (;; ) {
374
+ const rootsAnswer = await io.ask("扫描根 (逗号或空白分隔多个)", `默认: ${cwd}`);
375
+ if (rootsAnswer === null)
376
+ return cancelled();
377
+ roots = parseList(rootsAnswer, [cwd]);
378
+ const missing = [];
379
+ for (const root of roots) {
380
+ if (!await fileExists(root))
381
+ missing.push(root);
382
+ }
383
+ if (missing.length === 0)
384
+ break;
385
+ for (const path of missing)
386
+ io.print(`根不存在: ${path}`);
387
+ }
388
+ const excludeAnswer = await io.ask("排除名单 (目录名, 可留空)", "回车跳过");
389
+ if (excludeAnswer === null)
390
+ return cancelled();
391
+ const exclude = parseList(excludeAnswer, []);
392
+ io.print(`将写入 roots: ${JSON.stringify(roots)} · exclude: ${JSON.stringify(exclude)}`);
393
+ if (!await io.confirm("确认写入?", true))
394
+ return cancelled();
395
+ const config = { roots, exclude };
396
+ await writeFile(configPath, `${JSON.stringify(config, null, 2)}
397
+ `);
398
+ io.print(`配置已写入: ${configPath}`);
399
+ return { state: "written", config };
400
+ }
401
+ function setStdinRef(shouldRef) {
402
+ const fn = shouldRef ? process.stdin.ref : process.stdin.unref;
403
+ if (typeof fn === "function")
404
+ fn.call(process.stdin);
405
+ }
406
+ function createReadlineIO() {
407
+ const buffered = [];
408
+ let rl = null;
409
+ let ended = false;
410
+ let pending = null;
411
+ function ensure() {
412
+ if (rl !== null)
413
+ return rl;
414
+ const created = createInterface({
415
+ input: process.stdin,
416
+ output: process.stdout
417
+ });
418
+ created.on("line", (line) => {
419
+ if (pending === null) {
420
+ buffered.push(line);
421
+ return;
422
+ }
423
+ const resolve = pending;
424
+ pending = null;
425
+ setStdinRef(false);
426
+ resolve(line);
427
+ });
428
+ created.on("close", () => {
429
+ ended = true;
430
+ if (pending === null)
431
+ return;
432
+ const resolve = pending;
433
+ pending = null;
434
+ setStdinRef(false);
435
+ resolve(null);
436
+ });
437
+ created.on("SIGINT", () => created.close());
438
+ rl = created;
439
+ return created;
440
+ }
441
+ function readLine(prompt) {
442
+ if (ended)
443
+ return Promise.resolve(null);
444
+ const created = ensure();
445
+ created.setPrompt(prompt);
446
+ created.prompt();
447
+ const ready = buffered.shift();
448
+ if (ready !== undefined)
449
+ return Promise.resolve(ready);
450
+ setStdinRef(true);
451
+ return new Promise((resolve) => {
452
+ pending = resolve;
453
+ });
454
+ }
455
+ return {
456
+ ask(question, hint) {
457
+ const prompt = hint === undefined ? `${question} ` : `${question} [${hint}] `;
458
+ return readLine(prompt);
459
+ },
460
+ async confirm(question, defaultYes) {
461
+ const mark = defaultYes ? "(Y/n)" : "(y/N)";
462
+ const answer = await readLine(`${question} ${mark} `);
463
+ if (answer === null)
464
+ return false;
465
+ const normalized = answer.trim().toLowerCase();
466
+ if (normalized === "")
467
+ return defaultYes;
468
+ return normalized === "y" || normalized === "yes";
469
+ },
470
+ print(line) {
471
+ process.stdout.write(`${line}
472
+ `);
473
+ }
474
+ };
475
+ }
476
+
477
+ // src/render.ts
478
+ var BANNER = "SWEEP-NM";
479
+ var BAR_BLOCK = "▍";
480
+ var TOTAL_BLOCK = "█";
481
+ var NEUTRAL_BLOCK = "░";
482
+ var PLATFORM_STYLE = nativeStyle();
483
+ var NEUTRAL_TIER = { block: NEUTRAL_BLOCK, sgr: "2" };
484
+ var ROOT_LIST_LIMIT = 3;
485
+ var TIER_BIG = 1024 ** 3;
486
+ var TIER_MID = 100 * 1024 ** 2;
487
+ function formatBytes(bytes) {
488
+ if (bytes < 1024)
489
+ return `${Math.round(bytes)} B`;
490
+ const units = ["KB", "MB", "GB", "TB"];
491
+ let value = bytes;
492
+ let index = -1;
493
+ do {
494
+ value /= 1024;
495
+ index += 1;
496
+ } while (value >= 1024 && index < units.length - 1);
497
+ return `${Math.round(value * 10) / 10} ${units[index]}`;
498
+ }
499
+ function render(options) {
500
+ const { mode, roots, entries, color, home, releasedBytes, pathStyle } = options;
501
+ const style = pathStyle ?? PLATFORM_STYLE;
502
+ const scope = roots.length > 0 && roots.length <= ROOT_LIST_LIMIT ? `${roots.length} 个根: ${roots.map((root) => oneLine(shortenPath(root, style, home))).join(" · ")}` : `${roots.length} 个根`;
503
+ const head = paint(`${BAR_BLOCK} ${BANNER} ${mode === "execute" ? "执行" : "预览"} · ${scope}`, "1;7", color);
504
+ if (entries.length === 0) {
505
+ return [
506
+ head,
507
+ ` ${paint(`${NEUTRAL_BLOCK} 未发现 node_modules`, "2", color)}`
508
+ ].join(`
509
+ `);
510
+ }
511
+ const rows = [...entries].sort((a, b) => (b.bytes ?? -1) - (a.bytes ?? -1));
512
+ const volumeWidth = Math.max(...rows.map((row) => displayWidth(volumeOf(row))));
513
+ const nameWidth = Math.max(...rows.map((row) => displayWidth(oneLine(row.project))));
514
+ const body = rows.map((row) => {
515
+ const fail = mode === "execute" && row.ok === false;
516
+ const tier = row.bytes === undefined ? NEUTRAL_TIER : tierOf(row.bytes);
517
+ const mark = mode === "execute" ? paint(fail ? "✗" : "✓", fail ? "31" : "32", color) : paint(tier.block, tier.sgr, color);
518
+ const volume = padStart(volumeOf(row), volumeWidth);
519
+ const name = oneLine(row.project);
520
+ const rawPath = displayPath(row.target, style, home);
521
+ const path = oneLine(rawPath);
522
+ const rewritten = name !== row.project || path !== rawPath;
523
+ const tail = tailsOf(row, fail, rewritten);
524
+ const shown = `${paint(padEnd(name, nameWidth), "1", color)} ${paint(path, "2", color)}`;
525
+ return ` ${mark} ${volume} ${shown}${tail ? ` ${paint(tail, "2", color)}` : ""}`;
526
+ });
527
+ const hint = [
528
+ paint("加", "2", color),
529
+ paint("--yes", "1", color),
530
+ paint("执行删除", "2", color)
531
+ ].join(" ");
532
+ const foot = mode === "execute" ? footExecute(rows, color, releasedBytes) : ` ${paint(TOTAL_BLOCK, "7", color)} ${paint(`合计 ${rows.length} 处 · ${formatBytes(sum(rows))}`, "1", color)} ${hint}`;
533
+ return [head, ...body, foot].join(`
534
+ `);
535
+ }
536
+ function footExecute(rows, color, releasedBytes) {
537
+ const failed = rows.filter((row) => row.ok === false).length;
538
+ const released = releasedBytes === undefined ? "" : ` · 释放 ${formatBytes(releasedBytes)}`;
539
+ return ` ${paint(TOTAL_BLOCK, "7", color)} ${paint(`汇总 成功 ${rows.length - failed} 处${released} · 失败 ${failed} 处`, "1", color)}`;
540
+ }
541
+ var volumeOf = (row) => row.bytes === undefined ? "?" : formatBytes(row.bytes);
542
+ var SANITIZED_NOTE = "名字已净化显示";
543
+ function tailsOf(row, fail, rewritten) {
544
+ const parts = [];
545
+ if (rewritten)
546
+ parts.push(SANITIZED_NOTE);
547
+ if (row.note)
548
+ parts.push(oneLine(row.note));
549
+ if (fail && row.error)
550
+ parts.push(oneLine(row.error));
551
+ return parts.join(" ");
552
+ }
553
+ function tierOf(bytes) {
554
+ if (bytes >= TIER_BIG)
555
+ return { block: "█", sgr: "31" };
556
+ if (bytes >= TIER_MID)
557
+ return { block: "▓", sgr: "33" };
558
+ return { block: "▒", sgr: "36" };
559
+ }
560
+ var paint = (text, sgr, color) => color ? `\x1B[${sgr}m${text}\x1B[0m` : text;
561
+ var sum = (rows) => rows.reduce((total, row) => total + (row.bytes ?? 0), 0);
562
+ var fold2 = (text, style) => style.caseInsensitive ? text.toLowerCase() : text;
563
+ function shortenPath(path, style, home) {
564
+ if (!home)
565
+ return path;
566
+ if (fold2(path, style) === fold2(home, style))
567
+ return "~";
568
+ const prefix = `${home}${style.ops.sep}`;
569
+ return fold2(path, style).startsWith(fold2(prefix, style)) ? `~${path.slice(home.length)}` : path;
570
+ }
571
+ function displayPath(target, style, home) {
572
+ const suffix = `${style.ops.sep}node_modules`;
573
+ const dir = fold2(target, style).endsWith(fold2(suffix, style)) ? target.slice(0, -suffix.length) : target;
574
+ return shortenPath(dir, style, home);
575
+ }
576
+ var oneLine = (text) => text.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2066-\u2069\ufeff]/g, "").replace(/\s+/g, " ").trim();
577
+ var padEnd = (text, width) => text + " ".repeat(Math.max(0, width - displayWidth(text)));
578
+ var padStart = (text, width) => " ".repeat(Math.max(0, width - displayWidth(text))) + text;
579
+ function displayWidth(text) {
580
+ let width = 0;
581
+ for (const char of text) {
582
+ const code = char.codePointAt(0) ?? 0;
583
+ width += isFullWidth(code) ? 2 : 1;
584
+ }
585
+ return width;
586
+ }
587
+ function isFullWidth(code) {
588
+ return code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 65504 && code <= 65510;
589
+ }
590
+
591
+ // src/runtime.ts
592
+ import { readFile as readFile2, writeFile } from "node:fs/promises";
593
+ var isBun = typeof Bun !== "undefined";
594
+ async function writeTextFile(path, text) {
595
+ if (isBun) {
596
+ await Bun.write(path, text);
597
+ return;
598
+ }
599
+ await writeFile(path, text, "utf8");
600
+ }
601
+
602
+ // src/scan-parallel.ts
603
+ import { readdir, realpath as realpath2, stat } from "node:fs/promises";
604
+ import { join as join3 } from "node:path";
605
+ var NODE_MODULES = "node_modules";
606
+ var GIT_DIR = ".git";
607
+ var CONCURRENCY = 32;
608
+ function compareTarget(a, b) {
609
+ if (a.target < b.target)
610
+ return -1;
611
+ if (a.target > b.target)
612
+ return 1;
613
+ return 0;
614
+ }
615
+ function describeRootFailure(error) {
616
+ const { code } = error;
617
+ switch (code) {
618
+ case "ENOENT":
619
+ return "根不存在";
620
+ case "EACCES":
621
+ case "EPERM":
622
+ return "根不可读 (权限不足)";
623
+ case "ENOTDIR":
624
+ return "根不是目录";
625
+ default:
626
+ return `根不可用 (${code ?? "UNKNOWN"})`;
627
+ }
628
+ }
629
+ async function walkRoot(root, style, exclude, excludeCounts, hits, warnings) {
630
+ const stack = [root];
631
+ let outstanding = 1;
632
+ let active = 0;
633
+ const failures = [];
634
+ let resolveDrained = null;
635
+ const drained = new Promise((resolve) => {
636
+ resolveDrained = resolve;
637
+ });
638
+ function pump() {
639
+ while (active < CONCURRENCY && stack.length > 0) {
640
+ const dir = stack.pop();
641
+ active += 1;
642
+ visit(dir);
643
+ }
644
+ if (outstanding === 0 && resolveDrained !== null) {
645
+ const resolve = resolveDrained;
646
+ resolveDrained = null;
647
+ resolve();
648
+ }
649
+ }
650
+ async function visit(dir) {
651
+ try {
652
+ let entries;
653
+ try {
654
+ entries = await readdir(dir, { withFileTypes: true });
655
+ } catch {
656
+ warnings.push(`目录不可读, 已跳过: ${dir}`);
657
+ return;
658
+ }
659
+ for (const entry of entries) {
660
+ if (!entry.isDirectory())
661
+ continue;
662
+ const name = entry.name;
663
+ if (name === NODE_MODULES) {
664
+ const target = join3(dir, name);
665
+ const key = dedupeKey(await realpath2(target).catch(() => target), style);
666
+ if (!hits.has(key))
667
+ hits.set(key, { project: dir, target });
668
+ continue;
669
+ }
670
+ if (name === GIT_DIR)
671
+ continue;
672
+ if (exclude.has(name)) {
673
+ excludeCounts.set(name, (excludeCounts.get(name) ?? 0) + 1);
674
+ continue;
675
+ }
676
+ stack.push(join3(dir, name));
677
+ outstanding += 1;
678
+ }
679
+ } catch (error) {
680
+ if (failures.length === 0)
681
+ failures.push(error);
682
+ } finally {
683
+ active -= 1;
684
+ outstanding -= 1;
685
+ pump();
686
+ }
687
+ }
688
+ pump();
689
+ await drained;
690
+ if (failures.length > 0)
691
+ throw failures[0];
692
+ }
693
+ function createParallelScanner() {
694
+ return {
695
+ name: "parallel",
696
+ async scan(options) {
697
+ const warnings = [];
698
+ const exclude = new Set(options.exclude);
699
+ const excludeCounts = new Map;
700
+ for (const name of options.exclude) {
701
+ if (!excludeCounts.has(name))
702
+ excludeCounts.set(name, 0);
703
+ }
704
+ const hitsByRealTarget = new Map;
705
+ const style = nativeStyle();
706
+ const seenRoots = new Set;
707
+ const roots = [];
708
+ for (const root of options.roots) {
709
+ let key;
710
+ try {
711
+ const resolved = await realpath2(root);
712
+ if (!(await stat(resolved)).isDirectory()) {
713
+ warnings.push(`根不是目录, 已跳过: ${root}`);
714
+ continue;
715
+ }
716
+ key = dedupeKey(resolved, style);
717
+ } catch (error) {
718
+ warnings.push(`${describeRootFailure(error)}, 已跳过: ${root}`);
719
+ continue;
720
+ }
721
+ if (seenRoots.has(key))
722
+ continue;
723
+ seenRoots.add(key);
724
+ roots.push(root);
725
+ }
726
+ for (const root of roots) {
727
+ await walkRoot(root, style, exclude, excludeCounts, hitsByRealTarget, warnings);
728
+ }
729
+ const hits = [...hitsByRealTarget.values()].sort(compareTarget);
730
+ const excludeMatches = options.exclude.map((name) => ({
731
+ name,
732
+ hits: excludeCounts.get(name) ?? 0
733
+ }));
734
+ return { hits, warnings, excludeMatches };
735
+ }
736
+ };
737
+ }
738
+ // src/size-du.ts
739
+ import { spawn } from "node:child_process";
740
+ import { accessSync, constants } from "node:fs";
741
+ var DU_PROBES = ["/usr/bin/du", "/bin/du"];
742
+ var hasControlChar = (target) => target.includes(`
743
+ `) || target.includes("\r");
744
+ function findDu() {
745
+ for (const probe of DU_PROBES) {
746
+ try {
747
+ accessSync(probe, constants.X_OK);
748
+ return probe;
749
+ } catch {}
750
+ }
751
+ return null;
752
+ }
753
+ function runDu(bin, targets) {
754
+ return new Promise((resolve) => {
755
+ const child = spawn(bin, ["-sk", "--", ...targets], {
756
+ stdio: ["ignore", "pipe", "pipe"]
757
+ });
758
+ let stdout = "";
759
+ let stderr = "";
760
+ child.stdout.on("data", (chunk) => {
761
+ stdout += chunk;
762
+ });
763
+ child.stderr.on("data", (chunk) => {
764
+ stderr += chunk;
765
+ });
766
+ child.on("error", (error) => {
767
+ resolve({ stdout, stderr: `${stderr}${String(error)}` });
768
+ });
769
+ child.on("close", () => resolve({ stdout, stderr }));
770
+ });
771
+ }
772
+ function parseDuSizes(stdout) {
773
+ const sizes = new Map;
774
+ let duplicated = false;
775
+ for (const line of stdout.split(`
776
+ `)) {
777
+ const match = /^(\d+)\t(.+)$/.exec(line);
778
+ if (match === null)
779
+ continue;
780
+ const sizeKiB = match[1];
781
+ const path = match[2];
782
+ if (sizeKiB === undefined || path === undefined)
783
+ continue;
784
+ if (sizes.has(path))
785
+ duplicated = true;
786
+ sizes.set(path, Number(sizeKiB) * 1024);
787
+ }
788
+ return { sizes, duplicated };
789
+ }
790
+ function describeDuFailure(detail) {
791
+ if (/no such file or directory/i.test(detail))
792
+ return { reason: "不存在", missing: true };
793
+ if (/permission denied/i.test(detail))
794
+ return { reason: "权限不足, 无法读取", missing: false };
795
+ if (/not a directory/i.test(detail))
796
+ return { reason: "不是目录", missing: false };
797
+ if (/too many levels of symbolic links/i.test(detail)) {
798
+ return { reason: "符号链接层级过深", missing: false };
799
+ }
800
+ return { reason: `读取失败 (${detail})`, missing: false };
801
+ }
802
+ function parseDuErrorLine(line) {
803
+ const match = /^du:\s+(.+):\s+(.+)$/.exec(line);
804
+ if (match === null)
805
+ return null;
806
+ const path = match[1];
807
+ const detail = match[2];
808
+ if (path === undefined || detail === undefined)
809
+ return null;
810
+ return { path, detail };
811
+ }
812
+ function createDuSizer() {
813
+ const bin = findDu();
814
+ return {
815
+ name: "du",
816
+ async measure(targets) {
817
+ if (bin === null) {
818
+ return {
819
+ entries: [],
820
+ warnings: [
821
+ "du 候选不可用: /usr/bin/du 与 /bin/du 均不存在, 本候选跳过体积统计"
822
+ ],
823
+ unmeasured: []
824
+ };
825
+ }
826
+ if (targets.length === 0) {
827
+ return { entries: [], warnings: [], unmeasured: [] };
828
+ }
829
+ const sorted = [...targets].sort();
830
+ const warnings = [];
831
+ const entries = [];
832
+ const unmeasured = [];
833
+ const safeTargets = sorted.filter((target) => !hasControlChar(target));
834
+ const safeSet = new Set(safeTargets);
835
+ const sizes = new Map;
836
+ const failures = new Map;
837
+ let intact = true;
838
+ if (safeTargets.length > 0) {
839
+ const { stdout, stderr } = await runDu(bin, safeTargets);
840
+ const parsed = parseDuSizes(stdout);
841
+ intact = !parsed.duplicated && parsed.sizes.size <= safeTargets.length && [...parsed.sizes.keys()].every((path) => safeSet.has(path));
842
+ for (const [path, bytes] of parsed.sizes)
843
+ sizes.set(path, bytes);
844
+ for (const line of stderr.split(`
845
+ `)) {
846
+ if (line.trim() === "")
847
+ continue;
848
+ const errorLine = parseDuErrorLine(line);
849
+ if (errorLine === null) {
850
+ warnings.push(`体积统计失败 (未归因): ${line.trim()}`);
851
+ continue;
852
+ }
853
+ const failure = describeDuFailure(errorLine.detail);
854
+ if (!safeSet.has(errorLine.path)) {
855
+ warnings.push(`体积统计失败 (${failure.reason}): ${errorLine.path}`);
856
+ continue;
857
+ }
858
+ failures.set(errorLine.path, failure);
859
+ }
860
+ }
861
+ for (const target of sorted) {
862
+ if (hasControlChar(target)) {
863
+ unmeasured.push({ target, reason: "路径含控制字符" });
864
+ continue;
865
+ }
866
+ if (!intact) {
867
+ unmeasured.push({ target, reason: "输出不可解析" });
868
+ continue;
869
+ }
870
+ const bytes = sizes.get(target);
871
+ if (bytes !== undefined) {
872
+ entries.push({ target, bytes });
873
+ continue;
874
+ }
875
+ const failure = failures.get(target);
876
+ if (failure === undefined) {
877
+ unmeasured.push({ target, reason: "输出不可解析" });
878
+ continue;
879
+ }
880
+ if (failure.missing) {
881
+ warnings.push(`体积统计失败 (不存在): ${target}`);
882
+ continue;
883
+ }
884
+ unmeasured.push({ target, reason: failure.reason });
885
+ }
886
+ return { entries, warnings, unmeasured };
887
+ }
888
+ };
889
+ }
890
+
891
+ // src/size-js.ts
892
+ import { readdir as readdir2, stat as stat2 } from "node:fs/promises";
893
+ import { join as join4 } from "node:path";
894
+ function describeFailure(code) {
895
+ switch (code) {
896
+ case "ENOENT":
897
+ return "不存在";
898
+ case "EACCES":
899
+ case "EPERM":
900
+ return "权限不足, 无法读取";
901
+ case "ENOTDIR":
902
+ return "不是目录";
903
+ case "ELOOP":
904
+ return "符号链接层级过深";
905
+ default:
906
+ return code === undefined ? "读取失败" : `读取失败 (${code})`;
907
+ }
908
+ }
909
+ function errorCode(error) {
910
+ return error?.code;
911
+ }
912
+ async function sumEntries(dirents, dir, warnings) {
913
+ let total = 0;
914
+ for (const dirent of dirents) {
915
+ if (dirent.isSymbolicLink())
916
+ continue;
917
+ const path = join4(dir, dirent.name);
918
+ if (dirent.isDirectory()) {
919
+ try {
920
+ total += await sumEntries(await readdir2(path, { withFileTypes: true }), path, warnings);
921
+ } catch (error) {
922
+ warnings.push(`体积统计失败 (${describeFailure(errorCode(error))}): ${path}`);
923
+ }
924
+ } else if (dirent.isFile()) {
925
+ try {
926
+ total += (await stat2(path)).size;
927
+ } catch (error) {
928
+ warnings.push(`体积统计失败 (${describeFailure(errorCode(error))}): ${path}`);
929
+ }
930
+ }
931
+ }
932
+ return total;
933
+ }
934
+ function createJsSizer() {
935
+ return {
936
+ name: "js",
937
+ async measure(targets) {
938
+ const warnings = [];
939
+ const entries = [];
940
+ const unmeasured = [];
941
+ for (const target of [...targets].sort()) {
942
+ let dirents;
943
+ try {
944
+ dirents = await readdir2(target, { withFileTypes: true });
945
+ } catch (error) {
946
+ const code = errorCode(error);
947
+ if (code === "ENOENT") {
948
+ warnings.push(`体积统计失败 (不存在): ${target}`);
949
+ } else {
950
+ unmeasured.push({ target, reason: describeFailure(code) });
951
+ }
952
+ continue;
953
+ }
954
+ entries.push({
955
+ target,
956
+ bytes: await sumEntries(dirents, target, warnings)
957
+ });
958
+ }
959
+ return { entries, warnings, unmeasured };
960
+ }
961
+ };
962
+ }
963
+
964
+ // src/size.ts
965
+ function createSizer() {
966
+ return findDu() !== null ? createDuSizer() : createJsSizer();
967
+ }
968
+
969
+ // src/cli.ts
970
+ var COMMAND_COLUMN = 25;
971
+ function parseArgs(argv) {
972
+ const options = {
973
+ command: "sweep",
974
+ yes: false,
975
+ exclude: [],
976
+ help: false
977
+ };
978
+ const positionals = [];
979
+ for (let index = 0;index < argv.length; index += 1) {
980
+ const arg = argv[index];
981
+ if (arg === undefined)
982
+ continue;
983
+ if (arg === "--yes") {
984
+ options.yes = true;
985
+ continue;
986
+ }
987
+ if (arg === "--help" || arg === "-h") {
988
+ options.help = true;
989
+ continue;
990
+ }
991
+ if (arg === "--exclude" || arg === "--config") {
992
+ const value = argv[index + 1];
993
+ if (value === undefined || value.startsWith("-")) {
994
+ return { ok: false, message: `参数 ${arg} 缺少取值` };
995
+ }
996
+ if (arg === "--exclude")
997
+ options.exclude.push(value);
998
+ else
999
+ options.config = value;
1000
+ index += 1;
1001
+ continue;
1002
+ }
1003
+ if (arg.startsWith("-"))
1004
+ return { ok: false, message: `未知参数: ${arg}` };
1005
+ positionals.push(arg);
1006
+ }
1007
+ const [only] = positionals;
1008
+ if (positionals.length > 1 || only !== undefined && only !== "init") {
1009
+ return { ok: false, message: `未知参数: ${positionals.join(" ")}` };
1010
+ }
1011
+ if (only === "init")
1012
+ options.command = "init";
1013
+ return { ok: true, options };
1014
+ }
1015
+ function colorEnabled() {
1016
+ return process.stdout.isTTY === true && !process.env.NO_COLOR;
1017
+ }
1018
+ function interactive() {
1019
+ return process.stdin.isTTY === true && process.stdout.isTTY === true;
1020
+ }
1021
+ function print(line) {
1022
+ process.stdout.write(`${line}
1023
+ `);
1024
+ }
1025
+ function warn(line, color) {
1026
+ process.stderr.write(color ? `${paint("✗", "31", color)} ${line}
1027
+ ` : `${line}
1028
+ `);
1029
+ }
1030
+ function notice(line) {
1031
+ process.stderr.write(`${line}
1032
+ `);
1033
+ }
1034
+ function defaultConfigPath() {
1035
+ return resolveConfigPath({ env: {} }).path;
1036
+ }
1037
+ function helpText(color) {
1038
+ const row = (command, desc) => ` ${command.padEnd(COMMAND_COLUMN)} ${desc}`;
1039
+ return [
1040
+ `${paint(`${BAR_BLOCK} SWEEP-NM`, "1;7", color)} 工作区 node_modules 清理`,
1041
+ "",
1042
+ row("sweep-nm", "预览: 清单 + 体积 + 合计, 零副作用"),
1043
+ row("sweep-nm --yes", "执行删除"),
1044
+ row("sweep-nm --exclude <name>", "临时追加排除 (可重复, 与配置合并)"),
1045
+ row("sweep-nm --config <path>", "指定配置文件 (优先于 SWEEP_NM_CONFIG)"),
1046
+ row("sweep-nm init", "初始化向导: 交互生成配置文件"),
1047
+ row("sweep-nm --help", "帮助"),
1048
+ "",
1049
+ "说明:",
1050
+ " --exclude 按目录名精确匹配 (区分大小写), 从根到命中点的任意一级命中即跳过",
1051
+ ` 默认配置位置 (平台自适应): ${defaultConfigPath()}`,
1052
+ "",
1053
+ "退出码: 0 成功 (含预览与空结果); 1 删除失败 / 配置损坏 / 参数错误"
1054
+ ].join(`
1055
+ `);
1056
+ }
1057
+ function runWizard(configPath) {
1058
+ return runInit({
1059
+ configPath,
1060
+ async fileExists(path) {
1061
+ try {
1062
+ await lstat2(path);
1063
+ return true;
1064
+ } catch {
1065
+ return false;
1066
+ }
1067
+ },
1068
+ async writeFile(path, text) {
1069
+ await mkdir(dirname(path), { recursive: true });
1070
+ await writeTextFile(path, text);
1071
+ },
1072
+ io: createReadlineIO()
1073
+ });
1074
+ }
1075
+ async function resolveConfig(resolved) {
1076
+ const loaded = await loadResolvedConfig(resolved);
1077
+ if (loaded.state === "ok")
1078
+ return { config: loaded.config, source: "file" };
1079
+ if (!interactive())
1080
+ return {
1081
+ config: { roots: [process.cwd()], exclude: [] },
1082
+ source: "fallback"
1083
+ };
1084
+ const result = await runWizard(resolved.path);
1085
+ if (result.state === "written" && result.config !== undefined) {
1086
+ return { config: result.config, source: "wizard" };
1087
+ }
1088
+ print("配置未写入, 本次未执行清理");
1089
+ return null;
1090
+ }
1091
+ var ABORTED_HINT = "整批中止, 未执行删除";
1092
+ async function realRoots(roots) {
1093
+ return Promise.all(roots.map(async (root) => await realpath3(root).catch(() => null) ?? root));
1094
+ }
1095
+ function withOutcomes(entries, batch, accepted, removal) {
1096
+ const outcomes = new Map;
1097
+ for (const target of removal.removed)
1098
+ outcomes.set(target, { ok: true });
1099
+ for (const target of removal.missing)
1100
+ outcomes.set(target, { ok: true });
1101
+ for (const item of removal.failed)
1102
+ outcomes.set(item.target, { ok: false, error: item.error });
1103
+ const byTarget = new Map;
1104
+ for (const [index, target] of batch.entries()) {
1105
+ const real = accepted[index];
1106
+ const outcome = real === undefined ? undefined : outcomes.get(real);
1107
+ if (outcome !== undefined)
1108
+ byTarget.set(target, outcome);
1109
+ }
1110
+ const fallback = removal.aborted === undefined ? { ok: false } : { ok: false, error: ABORTED_HINT };
1111
+ return entries.map((entry) => ({
1112
+ ...entry,
1113
+ ...byTarget.get(entry.target) ?? fallback
1114
+ }));
1115
+ }
1116
+ function reportExcludeMatches(matches, color) {
1117
+ if (matches === undefined)
1118
+ return;
1119
+ const tty = process.stderr.isTTY === true;
1120
+ for (const item of matches) {
1121
+ if (item.hits === 0)
1122
+ warn(`排除名未匹配到任何目录: ${item.name} (按目录名精确匹配)`, color);
1123
+ else if (tty)
1124
+ notice(`排除生效: ${item.name} (${item.hits} 处)`);
1125
+ }
1126
+ }
1127
+ function toEntries(hits, sizeResult) {
1128
+ const bytesOf = new Map(sizeResult.entries.map((entry) => [
1129
+ entry.target,
1130
+ entry.bytes
1131
+ ]));
1132
+ const reasonOf = new Map(sizeResult.unmeasured.map((item) => [
1133
+ item.target,
1134
+ item.reason
1135
+ ]));
1136
+ const entries = [];
1137
+ for (const hit of hits) {
1138
+ const project = basename(hit.project);
1139
+ const bytes = bytesOf.get(hit.target);
1140
+ if (bytes !== undefined) {
1141
+ entries.push({ project, target: hit.target, bytes });
1142
+ continue;
1143
+ }
1144
+ const reason = reasonOf.get(hit.target);
1145
+ if (reason !== undefined) {
1146
+ entries.push({
1147
+ project,
1148
+ target: hit.target,
1149
+ note: `体积统计失败: ${reason}`
1150
+ });
1151
+ }
1152
+ }
1153
+ return entries;
1154
+ }
1155
+ async function sweep(config, options, color) {
1156
+ const roots = config.roots;
1157
+ const exclude = mergeExcludes(config.exclude, options.exclude);
1158
+ const scanResult = await createParallelScanner().scan({ roots, exclude });
1159
+ for (const warning of scanResult.warnings)
1160
+ warn(warning, color);
1161
+ reportExcludeMatches(scanResult.excludeMatches, color);
1162
+ const sizeResult = await createSizer().measure(scanResult.hits.map((hit) => hit.target));
1163
+ for (const warning of sizeResult.warnings)
1164
+ warn(warning, color);
1165
+ const entries = toEntries(scanResult.hits, sizeResult);
1166
+ const home = homedir4();
1167
+ if (!options.yes) {
1168
+ print(render({ mode: "preview", roots, entries, color, home }));
1169
+ return 0;
1170
+ }
1171
+ const batch = entries.filter((entry) => entry.bytes !== undefined).map((entry) => entry.target);
1172
+ const { accepted, rejected } = await validateTargets(batch, { roots });
1173
+ if (rejected.length > 0) {
1174
+ warn(`整批拒绝: ${rejected.length} 个目标未通过安全闸, 未执行任何删除`, color);
1175
+ for (const item of rejected)
1176
+ notice(` ${item.target} (${item.reason})`);
1177
+ return 1;
1178
+ }
1179
+ const removal = await removeTargets(accepted, {
1180
+ roots: await realRoots(roots)
1181
+ });
1182
+ const report = withOutcomes(entries, batch, accepted, removal);
1183
+ const releasedBytes = report.reduce((total, entry) => entry.ok === false ? total : total + (entry.bytes ?? 0), 0);
1184
+ print(render({
1185
+ mode: "execute",
1186
+ roots,
1187
+ entries: report,
1188
+ color,
1189
+ home,
1190
+ releasedBytes
1191
+ }));
1192
+ if (removal.aborted !== undefined) {
1193
+ warn(`整批中止: ${removal.aborted.reason}`, color);
1194
+ notice(` 本轮未执行删除: ${batch.length} 处`);
1195
+ }
1196
+ return report.some((entry) => entry.ok === false) ? 1 : 0;
1197
+ }
1198
+ async function main() {
1199
+ const color = colorEnabled();
1200
+ const parsed = parseArgs(process.argv.slice(2));
1201
+ if (!parsed.ok) {
1202
+ warn(`参数错误: ${parsed.message} (用 --help 查看用法)`, color);
1203
+ return 1;
1204
+ }
1205
+ const { options } = parsed;
1206
+ if (options.help) {
1207
+ print(helpText(color));
1208
+ return 0;
1209
+ }
1210
+ try {
1211
+ const resolvedPath = resolveConfigPath({ flag: options.config });
1212
+ if (options.command === "init") {
1213
+ if (!interactive()) {
1214
+ warn("init 需要交互终端 (请在终端直接运行, 不要重定向或经管道)", color);
1215
+ return 1;
1216
+ }
1217
+ const result = await runWizard(resolvedPath.path);
1218
+ if (result.state === "written")
1219
+ print("运行 sweep-nm 查看预览");
1220
+ else
1221
+ print("配置未变更");
1222
+ return 0;
1223
+ }
1224
+ const resolved = await resolveConfig(resolvedPath);
1225
+ if (resolved === null)
1226
+ return 0;
1227
+ if (resolved.source === "fallback") {
1228
+ const roots = resolved.config.roots.join(", ");
1229
+ if (options.yes) {
1230
+ warn("拒绝执行: 当前无配置文件, --yes 不可用", color);
1231
+ notice(` 将扫的根: ${roots}`);
1232
+ notice(" 先 sweep-nm init 生成配置, 或用 --config 指定配置文件");
1233
+ return 1;
1234
+ }
1235
+ notice(`未找到配置文件, 本次以当前目录为根: ${roots} (想固定此设置, 运行 sweep-nm init)`);
1236
+ }
1237
+ const freshConfig = resolved.source === "wizard";
1238
+ if (freshConfig && options.yes) {
1239
+ notice("首次配置已生成; 本轮先预览, 复核后可再运行 --yes 执行");
1240
+ }
1241
+ return await sweep(resolved.config, freshConfig ? { ...options, yes: false } : options, color);
1242
+ } catch (error) {
1243
+ warn(error instanceof Error ? error.message : String(error), color);
1244
+ return 1;
1245
+ }
1246
+ }
1247
+ process.exitCode = await main();