@mison/wecom-cleaner 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -15
- package/docs/NON_INTERACTIVE_SPEC.md +52 -0
- package/docs/TEST_MATRIX.md +110 -0
- package/docs/releases/v1.2.1.md +33 -0
- package/docs/releases/v1.3.0.md +45 -0
- package/native/bin/darwin-arm64/wecom-cleaner-core +0 -0
- package/native/bin/darwin-x64/wecom-cleaner-core +0 -0
- package/native/manifest.json +5 -5
- package/native/zig/src/main.zig +1 -1
- package/package.json +2 -1
- package/skills/wecom-cleaner-agent/SKILL.md +9 -5
- package/skills/wecom-cleaner-agent/agents/openai.yaml +1 -1
- package/skills/wecom-cleaner-agent/references/commands.md +24 -0
- package/skills/wecom-cleaner-agent/scripts/check_update_report.sh +141 -0
- package/skills/wecom-cleaner-agent/scripts/cleanup_monthly_report.sh +11 -1
- package/skills/wecom-cleaner-agent/scripts/recycle_maintain_report.sh +1 -1
- package/skills/wecom-cleaner-agent/scripts/restore_batch_report.sh +1 -1
- package/skills/wecom-cleaner-agent/scripts/space_governance_report.sh +3 -1
- package/skills/wecom-cleaner-agent/scripts/upgrade_report.sh +240 -0
- package/src/cli.js +709 -2
- package/src/config.js +51 -0
- package/src/constants.js +2 -0
- package/src/doctor.js +24 -0
- package/src/recycle-maintenance.js +44 -3
- package/src/updater.js +503 -0
package/src/config.js
CHANGED
|
@@ -2,18 +2,22 @@ import path from 'node:path';
|
|
|
2
2
|
import { DEFAULT_PROFILE_ROOT, DEFAULT_STATE_ROOT } from './constants.js';
|
|
3
3
|
import { ensureDir, expandHome, readJson, writeJson } from './utils.js';
|
|
4
4
|
import { normalizeRecycleRetention } from './recycle-maintenance.js';
|
|
5
|
+
import { normalizeSelfUpdateConfig } from './updater.js';
|
|
5
6
|
|
|
6
7
|
const ALLOWED_THEMES = new Set(['auto', 'light', 'dark']);
|
|
7
8
|
const ALLOWED_OUTPUTS = new Set(['json', 'text']);
|
|
8
9
|
const ALLOWED_CONFLICT_STRATEGIES = new Set(['skip', 'overwrite', 'rename']);
|
|
9
10
|
const ALLOWED_EXTERNAL_ROOT_SOURCES = new Set(['preset', 'configured', 'auto', 'all']);
|
|
10
11
|
const ALLOWED_GOVERNANCE_TIERS = new Set(['safe', 'caution', 'protected']);
|
|
12
|
+
const ALLOWED_UPGRADE_METHODS = new Set(['npm', 'github-script']);
|
|
13
|
+
const ALLOWED_UPGRADE_CHANNELS = new Set(['stable', 'pre']);
|
|
11
14
|
const ACTION_FLAG_MAP = new Map([
|
|
12
15
|
['--cleanup-monthly', 'cleanup_monthly'],
|
|
13
16
|
['--analysis-only', 'analysis_only'],
|
|
14
17
|
['--space-governance', 'space_governance'],
|
|
15
18
|
['--recycle-maintain', 'recycle_maintain'],
|
|
16
19
|
['--doctor', 'doctor'],
|
|
20
|
+
['--check-update', 'check_update'],
|
|
17
21
|
]);
|
|
18
22
|
const MODE_TO_ACTION_MAP = new Map([
|
|
19
23
|
['cleanup_monthly', 'cleanup_monthly'],
|
|
@@ -22,6 +26,8 @@ const MODE_TO_ACTION_MAP = new Map([
|
|
|
22
26
|
['recycle_maintain', 'recycle_maintain'],
|
|
23
27
|
['restore', 'restore'],
|
|
24
28
|
['doctor', 'doctor'],
|
|
29
|
+
['check_update', 'check_update'],
|
|
30
|
+
['upgrade', 'upgrade'],
|
|
25
31
|
]);
|
|
26
32
|
|
|
27
33
|
export class CliArgError extends Error {
|
|
@@ -106,6 +112,17 @@ export function defaultConfig() {
|
|
|
106
112
|
lastSelectedTargets: [],
|
|
107
113
|
},
|
|
108
114
|
recycleRetention,
|
|
115
|
+
selfUpdate: normalizeSelfUpdateConfig({
|
|
116
|
+
enabled: true,
|
|
117
|
+
channel: 'stable',
|
|
118
|
+
checkSchedule: 'tri_daily',
|
|
119
|
+
autoCheckOnStartup: true,
|
|
120
|
+
lastCheckAt: 0,
|
|
121
|
+
lastCheckSlot: '',
|
|
122
|
+
lastKnownLatest: '',
|
|
123
|
+
lastKnownSource: '',
|
|
124
|
+
skipVersion: '',
|
|
125
|
+
}),
|
|
109
126
|
theme: 'auto',
|
|
110
127
|
};
|
|
111
128
|
}
|
|
@@ -175,6 +192,10 @@ export function parseCliArgs(argv) {
|
|
|
175
192
|
retentionMaxAgeDays: null,
|
|
176
193
|
retentionMinKeepBatches: null,
|
|
177
194
|
retentionSizeThresholdGB: null,
|
|
195
|
+
upgradeMethod: null,
|
|
196
|
+
upgradeVersion: null,
|
|
197
|
+
upgradeChannel: null,
|
|
198
|
+
upgradeYes: false,
|
|
178
199
|
};
|
|
179
200
|
const actionValues = [];
|
|
180
201
|
|
|
@@ -251,6 +272,34 @@ export function parseCliArgs(argv) {
|
|
|
251
272
|
i += 1;
|
|
252
273
|
continue;
|
|
253
274
|
}
|
|
275
|
+
if (token === '--check-update') {
|
|
276
|
+
parsed.action = 'check_update';
|
|
277
|
+
parsed.actionFlagCount += 1;
|
|
278
|
+
actionValues.push('check_update');
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (token === '--upgrade') {
|
|
282
|
+
parsed.upgradeMethod = parseEnumValue(token, takeValue(token, i), ALLOWED_UPGRADE_METHODS);
|
|
283
|
+
parsed.action = 'upgrade';
|
|
284
|
+
parsed.actionFlagCount += 1;
|
|
285
|
+
actionValues.push('upgrade');
|
|
286
|
+
i += 1;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (token === '--upgrade-version') {
|
|
290
|
+
parsed.upgradeVersion = takeValue(token, i);
|
|
291
|
+
i += 1;
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (token === '--upgrade-channel') {
|
|
295
|
+
parsed.upgradeChannel = parseEnumValue(token, takeValue(token, i), ALLOWED_UPGRADE_CHANNELS);
|
|
296
|
+
i += 1;
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (token === '--upgrade-yes') {
|
|
300
|
+
parsed.upgradeYes = true;
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
254
303
|
if (token === '--theme') {
|
|
255
304
|
const theme = normalizeTheme(takeValue(token, i));
|
|
256
305
|
if (!theme) {
|
|
@@ -480,6 +529,7 @@ export async function loadConfig(cliArgs = {}, options = {}) {
|
|
|
480
529
|
|
|
481
530
|
merged.spaceGovernance = normalizeSpaceGovernance(fileConfig.spaceGovernance, base.spaceGovernance);
|
|
482
531
|
merged.recycleRetention = normalizeRecycleRetention(fileConfig.recycleRetention, base.recycleRetention);
|
|
532
|
+
merged.selfUpdate = normalizeSelfUpdateConfig(fileConfig.selfUpdate, base.selfUpdate);
|
|
483
533
|
|
|
484
534
|
merged.recycleRoot = expandHome(fileConfig.recycleRoot || path.join(stateRoot, 'recycle-bin'));
|
|
485
535
|
merged.indexPath = expandHome(fileConfig.indexPath || path.join(stateRoot, 'index.jsonl'));
|
|
@@ -508,6 +558,7 @@ export async function saveConfig(config) {
|
|
|
508
558
|
defaultCategories: Array.isArray(config.defaultCategories) ? config.defaultCategories : [],
|
|
509
559
|
spaceGovernance: normalizeSpaceGovernance(config.spaceGovernance, defaultConfig().spaceGovernance),
|
|
510
560
|
recycleRetention: normalizeRecycleRetention(config.recycleRetention, defaultConfig().recycleRetention),
|
|
561
|
+
selfUpdate: normalizeSelfUpdateConfig(config.selfUpdate, defaultConfig().selfUpdate),
|
|
511
562
|
theme: normalizeTheme(config.theme) || 'auto',
|
|
512
563
|
};
|
|
513
564
|
await writeJson(config.configPath, payload);
|
package/src/constants.js
CHANGED
package/src/doctor.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { spawnSync } from 'node:child_process';
|
|
4
4
|
import { collectRecycleStats, normalizeRecycleRetention } from './recycle-maintenance.js';
|
|
5
5
|
import { detectExternalStorageRoots, discoverAccounts } from './scanner.js';
|
|
6
|
+
import { normalizeSelfUpdateConfig } from './updater.js';
|
|
6
7
|
|
|
7
8
|
const STATUS_PASS = 'pass';
|
|
8
9
|
const STATUS_WARN = 'warn';
|
|
@@ -151,6 +152,14 @@ function overallStatus(checks) {
|
|
|
151
152
|
return STATUS_PASS;
|
|
152
153
|
}
|
|
153
154
|
|
|
155
|
+
function formatTimestamp(ts) {
|
|
156
|
+
const value = Number(ts || 0);
|
|
157
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
158
|
+
return '无';
|
|
159
|
+
}
|
|
160
|
+
return new Date(value).toLocaleString('zh-CN', { hour12: false });
|
|
161
|
+
}
|
|
162
|
+
|
|
154
163
|
async function readManifest(projectRoot) {
|
|
155
164
|
const manifestPath = path.join(projectRoot, 'native', 'manifest.json');
|
|
156
165
|
try {
|
|
@@ -267,6 +276,21 @@ export async function runDoctor({ config, aliases, projectRoot, appVersion }) {
|
|
|
267
276
|
)
|
|
268
277
|
);
|
|
269
278
|
|
|
279
|
+
const selfUpdate = normalizeSelfUpdateConfig(config.selfUpdate);
|
|
280
|
+
checks.push(
|
|
281
|
+
buildCheck(
|
|
282
|
+
'self_update',
|
|
283
|
+
'升级检查配置',
|
|
284
|
+
selfUpdate.enabled ? STATUS_PASS : STATUS_WARN,
|
|
285
|
+
`${selfUpdate.enabled ? '已启用' : '已关闭'},通道 ${selfUpdate.channel},最近检查 ${formatTimestamp(selfUpdate.lastCheckAt)}`,
|
|
286
|
+
selfUpdate.enabled
|
|
287
|
+
? selfUpdate.skipVersion
|
|
288
|
+
? `当前跳过版本: v${selfUpdate.skipVersion},如需恢复提醒请清空 skipVersion。`
|
|
289
|
+
: ''
|
|
290
|
+
: '建议启用升级检查,及时获取功能与安全修复。'
|
|
291
|
+
)
|
|
292
|
+
);
|
|
293
|
+
|
|
270
294
|
const target = resolveRuntimeTarget();
|
|
271
295
|
const manifest = await readManifest(projectRoot);
|
|
272
296
|
const manifestTarget = manifest.parsed?.targets?.[target.targetTag] || null;
|
|
@@ -51,8 +51,23 @@ function isPathWithinRoot(rootPath, targetPath) {
|
|
|
51
51
|
return !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
function
|
|
54
|
+
async function safeRealpath(targetPath) {
|
|
55
|
+
try {
|
|
56
|
+
return await fs.realpath(targetPath);
|
|
57
|
+
} catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function resolveBatchRootFromEntries(recycleRoot, batch) {
|
|
55
63
|
const recycleRootAbs = path.resolve(String(recycleRoot || ''));
|
|
64
|
+
const recycleRootReal = await safeRealpath(recycleRootAbs);
|
|
65
|
+
if (!recycleRootReal) {
|
|
66
|
+
return {
|
|
67
|
+
ok: false,
|
|
68
|
+
invalidReason: 'missing_recycle_root',
|
|
69
|
+
};
|
|
70
|
+
}
|
|
56
71
|
const entries = Array.isArray(batch?.entries) ? batch.entries : [];
|
|
57
72
|
if (entries.length === 0) {
|
|
58
73
|
return {
|
|
@@ -78,6 +93,19 @@ function resolveBatchRootFromEntries(recycleRoot, batch) {
|
|
|
78
93
|
invalidReason: 'recycle_path_outside_recycle_root',
|
|
79
94
|
};
|
|
80
95
|
}
|
|
96
|
+
const recyclePathReal = await safeRealpath(recyclePathAbs);
|
|
97
|
+
if (!recyclePathReal) {
|
|
98
|
+
return {
|
|
99
|
+
ok: false,
|
|
100
|
+
invalidReason: 'recycle_path_unresolvable',
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (!isPathWithinRoot(recycleRootReal, recyclePathReal)) {
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
invalidReason: 'recycle_path_symlink_escape',
|
|
107
|
+
};
|
|
108
|
+
}
|
|
81
109
|
|
|
82
110
|
const batchRootAbs = path.dirname(recyclePathAbs);
|
|
83
111
|
if (!isPathWithinRoot(recycleRootAbs, batchRootAbs)) {
|
|
@@ -92,7 +120,20 @@ function resolveBatchRootFromEntries(recycleRoot, batch) {
|
|
|
92
120
|
invalidReason: 'batch_root_is_recycle_root',
|
|
93
121
|
};
|
|
94
122
|
}
|
|
95
|
-
|
|
123
|
+
const batchRootReal = await safeRealpath(batchRootAbs);
|
|
124
|
+
if (!batchRootReal) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
invalidReason: 'batch_root_unresolvable',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
if (!isPathWithinRoot(recycleRootReal, batchRootReal)) {
|
|
131
|
+
return {
|
|
132
|
+
ok: false,
|
|
133
|
+
invalidReason: 'batch_root_symlink_escape',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
rootSet.add(batchRootReal);
|
|
96
137
|
}
|
|
97
138
|
|
|
98
139
|
if (rootSet.size !== 1) {
|
|
@@ -282,7 +323,7 @@ export async function maintainRecycleBin({ indexPath, recycleRoot, policy, dryRu
|
|
|
282
323
|
onProgress(i + 1, selected.candidates.length);
|
|
283
324
|
}
|
|
284
325
|
|
|
285
|
-
const resolvedBatchRoot = resolveBatchRootFromEntries(recycleRoot, batch);
|
|
326
|
+
const resolvedBatchRoot = await resolveBatchRootFromEntries(recycleRoot, batch);
|
|
286
327
|
if (!resolvedBatchRoot.ok) {
|
|
287
328
|
summary.failBatches += 1;
|
|
288
329
|
summary.operations.push({
|