@mison/wecom-cleaner 1.3.0 → 1.3.3

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.
Files changed (32) hide show
  1. package/README.md +45 -7
  2. package/docs/IMPLEMENTATION_PLAN.md +6 -6
  3. package/docs/NON_INTERACTIVE_SPEC.md +59 -0
  4. package/docs/TEST_MATRIX.md +17 -10
  5. package/docs/releases/v1.3.1.md +31 -0
  6. package/docs/releases/v1.3.2.md +38 -0
  7. package/docs/releases/v1.3.3.md +32 -0
  8. package/native/bin/darwin-arm64/wecom-cleaner-core +0 -0
  9. package/native/bin/darwin-x64/wecom-cleaner-core +0 -0
  10. package/native/manifest.json +5 -5
  11. package/native/zig/build.sh +3 -0
  12. package/native/zig/src/main.zig +2 -1
  13. package/native/zig/src/version.zig +1 -0
  14. package/package.json +2 -1
  15. package/skills/wecom-cleaner-agent/SKILL.md +10 -4
  16. package/skills/wecom-cleaner-agent/agents/openai.yaml +1 -1
  17. package/skills/wecom-cleaner-agent/references/commands.md +47 -11
  18. package/skills/wecom-cleaner-agent/scripts/check_update_report.sh +15 -0
  19. package/skills/wecom-cleaner-agent/scripts/cleanup_monthly_report.sh +37 -53
  20. package/skills/wecom-cleaner-agent/scripts/doctor_report.sh +3 -0
  21. package/skills/wecom-cleaner-agent/scripts/recycle_maintain_report.sh +39 -53
  22. package/skills/wecom-cleaner-agent/scripts/restore_batch_report.sh +34 -39
  23. package/skills/wecom-cleaner-agent/scripts/space_governance_report.sh +34 -49
  24. package/skills/wecom-cleaner-agent/scripts/upgrade_report.sh +15 -0
  25. package/skills/wecom-cleaner-agent/version.json +6 -0
  26. package/src/cli.js +1395 -28
  27. package/src/config.js +35 -0
  28. package/src/constants.js +1 -0
  29. package/src/doctor.js +41 -0
  30. package/src/skill-cli.js +111 -4
  31. package/src/skill-installer.js +204 -1
  32. package/src/updater.js +106 -3
package/src/config.js CHANGED
@@ -11,6 +11,9 @@ const ALLOWED_EXTERNAL_ROOT_SOURCES = new Set(['preset', 'configured', 'auto', '
11
11
  const ALLOWED_GOVERNANCE_TIERS = new Set(['safe', 'caution', 'protected']);
12
12
  const ALLOWED_UPGRADE_METHODS = new Set(['npm', 'github-script']);
13
13
  const ALLOWED_UPGRADE_CHANNELS = new Set(['stable', 'pre']);
14
+ const ALLOWED_SKILL_SYNC_METHODS = new Set(['npm', 'github-script']);
15
+ const ALLOWED_RUN_TASK_MODES = new Set(['preview', 'execute', 'preview-execute-verify']);
16
+ const ALLOWED_SCAN_DEBUG_LEVELS = new Set(['off', 'summary', 'full']);
14
17
  const ACTION_FLAG_MAP = new Map([
15
18
  ['--cleanup-monthly', 'cleanup_monthly'],
16
19
  ['--analysis-only', 'analysis_only'],
@@ -18,6 +21,7 @@ const ACTION_FLAG_MAP = new Map([
18
21
  ['--recycle-maintain', 'recycle_maintain'],
19
22
  ['--doctor', 'doctor'],
20
23
  ['--check-update', 'check_update'],
24
+ ['--sync-skills', 'sync_skills'],
21
25
  ]);
22
26
  const MODE_TO_ACTION_MAP = new Map([
23
27
  ['cleanup_monthly', 'cleanup_monthly'],
@@ -28,6 +32,7 @@ const MODE_TO_ACTION_MAP = new Map([
28
32
  ['doctor', 'doctor'],
29
33
  ['check_update', 'check_update'],
30
34
  ['upgrade', 'upgrade'],
35
+ ['sync_skills', 'sync_skills'],
31
36
  ]);
32
37
 
33
38
  export class CliArgError extends Error {
@@ -196,6 +201,11 @@ export function parseCliArgs(argv) {
196
201
  upgradeVersion: null,
197
202
  upgradeChannel: null,
198
203
  upgradeYes: false,
204
+ upgradeSyncSkills: null,
205
+ skillSyncMethod: null,
206
+ skillSyncRef: null,
207
+ runTask: null,
208
+ scanDebug: 'off',
199
209
  };
200
210
  const actionValues = [];
201
211
 
@@ -300,6 +310,31 @@ export function parseCliArgs(argv) {
300
310
  parsed.upgradeYes = true;
301
311
  continue;
302
312
  }
313
+ if (token === '--upgrade-sync-skills') {
314
+ parsed.upgradeSyncSkills = parseBooleanFlag(token, takeValue(token, i));
315
+ i += 1;
316
+ continue;
317
+ }
318
+ if (token === '--skill-sync-method') {
319
+ parsed.skillSyncMethod = parseEnumValue(token, takeValue(token, i), ALLOWED_SKILL_SYNC_METHODS);
320
+ i += 1;
321
+ continue;
322
+ }
323
+ if (token === '--skill-sync-ref') {
324
+ parsed.skillSyncRef = takeValue(token, i);
325
+ i += 1;
326
+ continue;
327
+ }
328
+ if (token === '--run-task') {
329
+ parsed.runTask = parseEnumValue(token, takeValue(token, i), ALLOWED_RUN_TASK_MODES);
330
+ i += 1;
331
+ continue;
332
+ }
333
+ if (token === '--scan-debug') {
334
+ parsed.scanDebug = parseEnumValue(token, takeValue(token, i), ALLOWED_SCAN_DEBUG_LEVELS);
335
+ i += 1;
336
+ continue;
337
+ }
303
338
  if (token === '--theme') {
304
339
  const theme = normalizeTheme(takeValue(token, i));
305
340
  if (!theme) {
package/src/constants.js CHANGED
@@ -108,6 +108,7 @@ export const MODES = {
108
108
  RECYCLE_MAINTAIN: 'recycle_maintain',
109
109
  CHECK_UPDATE: 'check_update',
110
110
  UPGRADE: 'upgrade',
111
+ SYNC_SKILLS: 'sync_skills',
111
112
  SETTINGS: 'settings',
112
113
  };
113
114
 
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 { inspectSkillBinding, skillBindingStatusLabel } from './skill-installer.js';
6
7
  import { normalizeSelfUpdateConfig } from './updater.js';
7
8
 
8
9
  const STATUS_PASS = 'pass';
@@ -291,6 +292,43 @@ export async function runDoctor({ config, aliases, projectRoot, appVersion }) {
291
292
  )
292
293
  );
293
294
 
295
+ let skillBinding = null;
296
+ try {
297
+ skillBinding = await inspectSkillBinding({
298
+ appVersion: appVersion || '',
299
+ targetRoot: process.env.WECOM_CLEANER_SKILLS_ROOT || '',
300
+ });
301
+ } catch (error) {
302
+ skillBinding = {
303
+ status: 'invalid_skill_dir',
304
+ matched: false,
305
+ installed: false,
306
+ expectedAppVersion: String(appVersion || ''),
307
+ installedManifest: null,
308
+ recommendation: `技能检测失败:${error instanceof Error ? error.message : String(error)}`,
309
+ };
310
+ }
311
+ const skillDetail = (() => {
312
+ if (!skillBinding) {
313
+ return '未获取到技能状态';
314
+ }
315
+ const installedVersion = skillBinding.installedManifest?.skillVersion || '-';
316
+ const requiredAppVersion = skillBinding.installedManifest?.requiredAppVersion || '-';
317
+ return `${skillBindingStatusLabel(skillBinding.status)},技能版本 ${installedVersion},绑定程序版本 ${requiredAppVersion}`;
318
+ })();
319
+ checks.push(
320
+ buildCheck(
321
+ 'skills_binding',
322
+ 'Agent Skills 版本绑定',
323
+ skillBinding?.matched ? STATUS_PASS : STATUS_WARN,
324
+ skillDetail,
325
+ skillBinding?.matched
326
+ ? ''
327
+ : skillBinding?.recommendation ||
328
+ '建议执行 wecom-cleaner-skill install --force 重新同步 skills 版本。'
329
+ )
330
+ );
331
+
294
332
  const target = resolveRuntimeTarget();
295
333
  const manifest = await readManifest(projectRoot);
296
334
  const manifestTarget = manifest.parsed?.targets?.[target.targetTag] || null;
@@ -384,6 +422,9 @@ export async function runDoctor({ config, aliases, projectRoot, appVersion }) {
384
422
  recycleBytes: recycleStats.totalBytes,
385
423
  recycleThresholdBytes: thresholdBytes,
386
424
  recycleOverThreshold,
425
+ skillsStatus: skillBinding?.status || 'unknown',
426
+ skillsMatched: Boolean(skillBinding?.matched),
427
+ skillsInstalled: Boolean(skillBinding?.installed),
387
428
  },
388
429
  recommendations,
389
430
  };
package/src/skill-cli.js CHANGED
@@ -1,15 +1,42 @@
1
1
  #!/usr/bin/env node
2
- import { installSkill, resolveDefaultSkillsRoot } from './skill-installer.js';
2
+ import path from 'node:path';
3
+ import { readFile } from 'node:fs/promises';
4
+ import { fileURLToPath } from 'node:url';
5
+ import {
6
+ installSkill,
7
+ inspectSkillBinding,
8
+ resolveDefaultSkillsRoot,
9
+ skillBindingStatusLabel,
10
+ } from './skill-installer.js';
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const PROJECT_ROOT = path.resolve(__dirname, '..');
15
+
16
+ async function loadAppVersion() {
17
+ try {
18
+ const packagePath = path.join(PROJECT_ROOT, 'package.json');
19
+ const text = await readFile(packagePath, 'utf-8');
20
+ const pkg = JSON.parse(text);
21
+ return String(pkg.version || '').trim();
22
+ } catch {
23
+ return '';
24
+ }
25
+ }
3
26
 
4
27
  function printHelp() {
5
28
  console.log(`wecom-cleaner-skill
6
29
 
7
30
  用法:
8
- wecom-cleaner-skill install [--target <目录>] [--force] [--dry-run]
31
+ wecom-cleaner-skill install [--target <目录>] [--force] [--dry-run] [--json]
32
+ wecom-cleaner-skill sync [--target <目录>] [--dry-run] [--json]
33
+ wecom-cleaner-skill status [--target <目录>] [--app-version <x.y.z>] [--json]
9
34
  wecom-cleaner-skill path
10
35
 
11
36
  说明:
12
37
  - install: 安装 wecom-cleaner-agent 到 Codex 技能目录
38
+ - sync: 同步/升级技能版本(等价 install --force)
39
+ - status: 检查技能是否与主程序版本匹配
13
40
  - path: 输出默认技能目录(由 CODEX_HOME 或 ~/.codex 推导)
14
41
  `);
15
42
  }
@@ -20,6 +47,8 @@ function parseArgs(argv) {
20
47
  target: '',
21
48
  force: false,
22
49
  dryRun: false,
50
+ json: false,
51
+ appVersion: '',
23
52
  help: false,
24
53
  };
25
54
 
@@ -44,6 +73,10 @@ function parseArgs(argv) {
44
73
  parsed.dryRun = true;
45
74
  continue;
46
75
  }
76
+ if (token === '--json') {
77
+ parsed.json = true;
78
+ continue;
79
+ }
47
80
  if (token === '--target') {
48
81
  const next = tokens[i + 1];
49
82
  if (!next || next.startsWith('-')) {
@@ -53,15 +86,47 @@ function parseArgs(argv) {
53
86
  i += 1;
54
87
  continue;
55
88
  }
89
+ if (token === '--app-version') {
90
+ const next = tokens[i + 1];
91
+ if (!next || next.startsWith('-')) {
92
+ throw new Error('参数 --app-version 缺少版本值');
93
+ }
94
+ parsed.appVersion = next;
95
+ i += 1;
96
+ continue;
97
+ }
56
98
  throw new Error(`未知参数: ${token}`);
57
99
  }
58
100
 
59
101
  return parsed;
60
102
  }
61
103
 
104
+ function printStatusText(result) {
105
+ console.log(`技能: ${result.skillName}`);
106
+ console.log(`状态: ${skillBindingStatusLabel(result.status)}`);
107
+ console.log(`匹配: ${result.matched ? '是' : '否'}`);
108
+ console.log(`主程序版本: ${result.expectedAppVersion || '-'}`);
109
+ console.log(`已安装: ${result.installed ? '是' : '否'}`);
110
+ if (result.installedManifest?.skillVersion) {
111
+ console.log(`技能版本: ${result.installedManifest.skillVersion}`);
112
+ } else {
113
+ console.log('技能版本: -');
114
+ }
115
+ if (result.installedManifest?.requiredAppVersion) {
116
+ console.log(`技能绑定版本: ${result.installedManifest.requiredAppVersion}`);
117
+ } else {
118
+ console.log('技能绑定版本: -');
119
+ }
120
+ console.log(`目标目录: ${result.targetSkillDir}`);
121
+ if (result.recommendation) {
122
+ console.log(`建议: ${result.recommendation}`);
123
+ }
124
+ }
125
+
62
126
  async function main() {
63
127
  try {
64
128
  const args = parseArgs(process.argv.slice(2));
129
+ const appVersion = args.appVersion || (await loadAppVersion());
65
130
 
66
131
  if (args.help) {
67
132
  printHelp();
@@ -73,21 +138,63 @@ async function main() {
73
138
  return;
74
139
  }
75
140
 
76
- if (args.command !== 'install') {
141
+ if (args.command === 'status') {
142
+ const status = await inspectSkillBinding({
143
+ targetRoot: args.target,
144
+ appVersion,
145
+ });
146
+ if (args.json) {
147
+ console.log(JSON.stringify(status, null, 2));
148
+ process.exitCode = status.matched ? 0 : 1;
149
+ return;
150
+ }
151
+ printStatusText(status);
152
+ process.exitCode = status.matched ? 0 : 1;
153
+ return;
154
+ }
155
+
156
+ if (args.command !== 'install' && args.command !== 'sync') {
77
157
  throw new Error(`不支持的命令: ${args.command}`);
78
158
  }
79
159
 
160
+ const force = args.command === 'sync' ? true : args.force;
80
161
  const result = await installSkill({
81
162
  targetRoot: args.target,
82
- force: args.force,
163
+ force,
83
164
  dryRun: args.dryRun,
165
+ appVersion,
166
+ });
167
+ const status = await inspectSkillBinding({
168
+ targetRoot: args.target,
169
+ appVersion,
84
170
  });
85
171
 
172
+ if (args.json) {
173
+ console.log(
174
+ JSON.stringify(
175
+ {
176
+ action: args.command,
177
+ result,
178
+ status,
179
+ },
180
+ null,
181
+ 2
182
+ )
183
+ );
184
+ return;
185
+ }
186
+
86
187
  const mode = result.dryRun ? '预演' : '安装';
87
188
  const replaceText = result.replaced ? '(覆盖已存在版本)' : '';
88
189
  console.log(`${mode}成功${replaceText}`);
89
190
  console.log(`技能: ${result.skillName}`);
191
+ console.log(`技能版本: ${result.targetManifest.skillVersion}`);
192
+ console.log(`绑定程序版本: ${result.targetManifest.requiredAppVersion}`);
90
193
  console.log(`目标: ${result.targetSkillDir}`);
194
+ console.log(`匹配状态: ${skillBindingStatusLabel(status.status)}`);
195
+ if (!status.matched && status.recommendation) {
196
+ console.log(`建议: ${status.recommendation}`);
197
+ }
91
198
  } catch (error) {
92
199
  console.error(`执行失败: ${error.message}`);
93
200
  process.exitCode = 1;
@@ -1,15 +1,60 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
- import { access, cp, mkdir, rm } from 'node:fs/promises';
4
+ import { access, cp, mkdir, readFile, rm } from 'node:fs/promises';
5
5
  import { constants as fsConstants } from 'node:fs';
6
6
 
7
7
  export const SKILL_NAME = 'wecom-cleaner-agent';
8
+ export const SKILL_VERSION_FILE = 'version.json';
8
9
 
9
10
  const __filename = fileURLToPath(import.meta.url);
10
11
  const __dirname = path.dirname(__filename);
11
12
  const PROJECT_ROOT = path.resolve(__dirname, '..');
12
13
  const DEFAULT_SOURCE_SKILL_DIR = path.join(PROJECT_ROOT, 'skills', SKILL_NAME);
14
+ const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?$/;
15
+
16
+ function normalizeSemver(rawValue) {
17
+ const text = String(rawValue || '').trim();
18
+ if (!text) {
19
+ return '';
20
+ }
21
+ const matched = text.match(SEMVER_RE);
22
+ if (!matched) {
23
+ return '';
24
+ }
25
+ const base = `${matched[1]}.${matched[2]}.${matched[3]}`;
26
+ return matched[4] ? `${base}-${matched[4]}` : base;
27
+ }
28
+
29
+ function buildFallbackManifest(appVersion = '') {
30
+ const normalizedAppVersion = normalizeSemver(appVersion) || '0.0.0';
31
+ return {
32
+ schemaVersion: 1,
33
+ skillName: SKILL_NAME,
34
+ skillVersion: normalizedAppVersion,
35
+ requiredAppVersion: normalizedAppVersion,
36
+ };
37
+ }
38
+
39
+ function normalizeSkillManifest(rawManifest, options = {}) {
40
+ const fallback = buildFallbackManifest(options.appVersion);
41
+ const source = rawManifest && typeof rawManifest === 'object' ? rawManifest : {};
42
+ const schemaVersion = Number.isFinite(Number(source.schemaVersion))
43
+ ? Number(source.schemaVersion)
44
+ : fallback.schemaVersion;
45
+ const skillName =
46
+ typeof source.skillName === 'string' && source.skillName.trim()
47
+ ? source.skillName.trim()
48
+ : fallback.skillName;
49
+ const skillVersion = normalizeSemver(source.skillVersion) || fallback.skillVersion;
50
+ const requiredAppVersion = normalizeSemver(source.requiredAppVersion) || fallback.requiredAppVersion;
51
+ return {
52
+ schemaVersion,
53
+ skillName,
54
+ skillVersion,
55
+ requiredAppVersion,
56
+ };
57
+ }
13
58
 
14
59
  export function resolveDefaultSkillsRoot(env = process.env) {
15
60
  const codexHome = typeof env.CODEX_HOME === 'string' ? env.CODEX_HOME.trim() : '';
@@ -35,6 +80,142 @@ async function exists(targetPath) {
35
80
  }
36
81
  }
37
82
 
83
+ export function skillBindingStatusLabel(status) {
84
+ if (status === 'matched') {
85
+ return '已匹配';
86
+ }
87
+ if (status === 'mismatch') {
88
+ return '版本不匹配';
89
+ }
90
+ if (status === 'legacy_unversioned') {
91
+ return '旧版技能(缺少版本信息)';
92
+ }
93
+ if (status === 'not_installed') {
94
+ return '未安装';
95
+ }
96
+ if (status === 'invalid_skill_dir') {
97
+ return '安装目录异常';
98
+ }
99
+ return '未知';
100
+ }
101
+
102
+ export async function readSkillManifestFromDir(skillDir, options = {}) {
103
+ const resolvedSkillDir = path.resolve(String(skillDir || ''));
104
+ const manifestPath = path.join(resolvedSkillDir, SKILL_VERSION_FILE);
105
+ const manifestExists = await exists(manifestPath);
106
+ if (!manifestExists) {
107
+ return {
108
+ path: manifestPath,
109
+ exists: false,
110
+ parseError: '',
111
+ manifest: normalizeSkillManifest(null, options),
112
+ };
113
+ }
114
+
115
+ try {
116
+ const text = await readFile(manifestPath, 'utf-8');
117
+ const parsed = JSON.parse(text);
118
+ return {
119
+ path: manifestPath,
120
+ exists: true,
121
+ parseError: '',
122
+ manifest: normalizeSkillManifest(parsed, options),
123
+ };
124
+ } catch (error) {
125
+ return {
126
+ path: manifestPath,
127
+ exists: true,
128
+ parseError: error instanceof Error ? error.message : String(error),
129
+ manifest: normalizeSkillManifest(null, options),
130
+ };
131
+ }
132
+ }
133
+
134
+ export async function inspectSkillBinding(options = {}) {
135
+ const appVersion = normalizeSemver(options.appVersion);
136
+ const sourceSkillDir = path.resolve(options.sourceSkillDir || DEFAULT_SOURCE_SKILL_DIR);
137
+ const sourceSkillFile = path.join(sourceSkillDir, 'SKILL.md');
138
+ if (!(await exists(sourceSkillFile))) {
139
+ throw new Error(`技能源目录无效,缺少 SKILL.md: ${sourceSkillDir}`);
140
+ }
141
+
142
+ const sourceManifestState = await readSkillManifestFromDir(sourceSkillDir, {
143
+ appVersion,
144
+ });
145
+ const expectedAppVersion = appVersion || sourceManifestState.manifest.requiredAppVersion;
146
+ const targetRoot = resolveTargetSkillsRoot(options.targetRoot, options.env || process.env);
147
+ const targetSkillDir = path.join(targetRoot, SKILL_NAME);
148
+ const targetSkillExists = await exists(targetSkillDir);
149
+ const targetSkillFile = path.join(targetSkillDir, 'SKILL.md');
150
+ const targetSkillFileExists = targetSkillExists ? await exists(targetSkillFile) : false;
151
+ const installedManifestState = targetSkillExists
152
+ ? await readSkillManifestFromDir(targetSkillDir, {
153
+ appVersion: expectedAppVersion,
154
+ })
155
+ : null;
156
+
157
+ const recommendation = (() => {
158
+ if (!targetSkillExists) {
159
+ return '执行 wecom-cleaner-skill install 安装技能。';
160
+ }
161
+ if (!targetSkillFileExists) {
162
+ return '目标目录缺少 SKILL.md,建议执行 wecom-cleaner-skill install --force 修复。';
163
+ }
164
+ if (installedManifestState && !installedManifestState.exists) {
165
+ return '检测到旧版技能(缺少 version.json),建议执行 wecom-cleaner-skill install --force。';
166
+ }
167
+ if (
168
+ installedManifestState &&
169
+ installedManifestState.manifest.skillName === SKILL_NAME &&
170
+ installedManifestState.manifest.requiredAppVersion === expectedAppVersion
171
+ ) {
172
+ return '';
173
+ }
174
+ return '建议执行 wecom-cleaner-skill install --force 同步技能版本。';
175
+ })();
176
+
177
+ let status = 'matched';
178
+ let matched = true;
179
+ if (!targetSkillExists) {
180
+ status = 'not_installed';
181
+ matched = false;
182
+ } else if (!targetSkillFileExists) {
183
+ status = 'invalid_skill_dir';
184
+ matched = false;
185
+ } else if (installedManifestState && !installedManifestState.exists) {
186
+ status = 'legacy_unversioned';
187
+ matched = false;
188
+ } else if (
189
+ !installedManifestState ||
190
+ installedManifestState.manifest.skillName !== SKILL_NAME ||
191
+ installedManifestState.manifest.requiredAppVersion !== expectedAppVersion
192
+ ) {
193
+ status = 'mismatch';
194
+ matched = false;
195
+ }
196
+
197
+ return {
198
+ skillName: SKILL_NAME,
199
+ status,
200
+ matched,
201
+ recommendation,
202
+ expectedAppVersion: expectedAppVersion || '',
203
+ sourceSkillDir,
204
+ sourceManifestPath: sourceManifestState.path,
205
+ sourceManifest: sourceManifestState.manifest,
206
+ sourceManifestExists: sourceManifestState.exists,
207
+ sourceManifestParseError: sourceManifestState.parseError || '',
208
+ targetRoot,
209
+ targetSkillDir,
210
+ installed: targetSkillExists,
211
+ installedSkillFileExists: targetSkillFileExists,
212
+ installedManifestPath: installedManifestState?.path || path.join(targetSkillDir, SKILL_VERSION_FILE),
213
+ installedManifest: installedManifestState?.manifest || null,
214
+ installedManifestExists: Boolean(installedManifestState?.exists),
215
+ installedManifestParseError: installedManifestState?.parseError || '',
216
+ };
217
+ }
218
+
38
219
  export async function installSkill(options = {}) {
39
220
  const sourceSkillDir = path.resolve(options.sourceSkillDir || DEFAULT_SOURCE_SKILL_DIR);
40
221
  const targetRoot = resolveTargetSkillsRoot(options.targetRoot);
@@ -46,9 +227,17 @@ export async function installSkill(options = {}) {
46
227
  if (!sourceExists) {
47
228
  throw new Error(`技能源目录无效,缺少 SKILL.md: ${sourceSkillDir}`);
48
229
  }
230
+ const sourceManifestState = await readSkillManifestFromDir(sourceSkillDir, {
231
+ appVersion: options.appVersion,
232
+ });
49
233
 
50
234
  const targetSkillDir = path.join(targetRoot, SKILL_NAME);
51
235
  const targetExists = await exists(targetSkillDir);
236
+ const previousManifestState = targetExists
237
+ ? await readSkillManifestFromDir(targetSkillDir, {
238
+ appVersion: options.appVersion || sourceManifestState.manifest.requiredAppVersion,
239
+ })
240
+ : null;
52
241
  if (targetExists && !force) {
53
242
  throw new Error(`目标技能已存在:${targetSkillDir}。如需覆盖请加 --force`);
54
243
  }
@@ -65,6 +254,12 @@ export async function installSkill(options = {}) {
65
254
  });
66
255
  }
67
256
 
257
+ const targetManifestState = dryRun
258
+ ? sourceManifestState
259
+ : await readSkillManifestFromDir(targetSkillDir, {
260
+ appVersion: options.appVersion || sourceManifestState.manifest.requiredAppVersion,
261
+ });
262
+
68
263
  return {
69
264
  skillName: SKILL_NAME,
70
265
  sourceSkillDir,
@@ -72,5 +267,13 @@ export async function installSkill(options = {}) {
72
267
  targetSkillDir,
73
268
  dryRun,
74
269
  replaced: targetExists,
270
+ sourceManifest: sourceManifestState.manifest,
271
+ sourceManifestPath: sourceManifestState.path,
272
+ previousManifest: previousManifestState?.manifest || null,
273
+ previousManifestPath: previousManifestState?.path || null,
274
+ previousManifestExists: Boolean(previousManifestState?.exists),
275
+ targetManifest: targetManifestState.manifest,
276
+ targetManifestPath: targetManifestState.path,
277
+ targetManifestExists: targetManifestState.exists,
75
278
  };
76
279
  }