@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/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
@@ -106,6 +106,8 @@ export const MODES = {
106
106
  RESTORE: 'restore',
107
107
  DOCTOR: 'doctor',
108
108
  RECYCLE_MAINTAIN: 'recycle_maintain',
109
+ CHECK_UPDATE: 'check_update',
110
+ UPGRADE: 'upgrade',
109
111
  SETTINGS: 'settings',
110
112
  };
111
113
 
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 resolveBatchRootFromEntries(recycleRoot, batch) {
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
- rootSet.add(batchRootAbs);
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({