create-dwy 0.17.0 → 0.18.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.
Files changed (34) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/bin/index.js +24 -22
  3. package/package.json +1 -1
  4. package/src/cli-help.js +24 -0
  5. package/src/self-upgrade.js +158 -0
  6. package/src/sync-all.js +159 -5
  7. package/src/sync-packs.js +191 -0
  8. package/src/sync-scene.js +146 -0
  9. package/src/sync.js +9 -1
  10. package/templates/ai-tools/CHANGELOG.md +32 -0
  11. package/templates/ai-tools/VERSION +1 -1
  12. package/templates/ai-tools/rules/Android/dwy-android-core.md +67 -1
  13. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-dependency-freshness.md +26 -14
  14. package/templates/ai-tools/rules//345/274/200/345/217/221/346/265/201/347/250/213/dwy-git-commit.md +90 -22
  15. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-acceptance/SKILL.md +1 -1
  16. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-commercial/SKILL.md +1 -1
  17. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-competitor/SKILL.md +1 -1
  18. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-mvp/SKILL.md +1 -1
  19. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-prd/SKILL.md +1 -1
  20. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/SKILL.md +1 -1
  21. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-product-launcher/references/state-and-contract.md +3 -3
  22. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-release/SKILL.md +1 -1
  23. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tasks/SKILL.md +1 -1
  24. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-tdd-dev/SKILL.md +1 -1
  25. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-validate/SKILL.md +1 -1
  26. package/templates/ai-tools/skills//344/272/247/345/223/2010/345/210/2601/dwy-version/SKILL.md +1 -1
  27. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/SKILL.md +39 -20
  28. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/assets/appstore-submission.yaml +28 -1
  29. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/api-workflow.md +31 -1
  30. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/aso.md +52 -0
  31. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/in-app-purchase-review.md +1 -1
  32. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/screenshot-specs.yaml +98 -0
  33. package/templates/ai-tools/skills//345/217/221/345/270/203/345/217/221/347/211/210/dwy-ios-app-store-release/references/screenshot-workflow.md +62 -6
  34. package/templates/ai-tools/skills//345/237/272/347/241/200/345/272/223/dwy-eui/references/eui-design-guide.md +3 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # create-dwy
2
2
 
3
+ ## 0.18.0
4
+
5
+ ### Minor Changes
6
+
7
+ - `dwy` / `dwy sync` 开头选动作:同步项目配置(按包或按条目),刷新全局外部 skill,或升级 dwy。删除 `dwy skills` / `dwy skills install` / `dwy scene`。
8
+ - 新增 `dwy upgrade`:把全局 `create-dwy` 升到 npm `latest` 正式版(npm/pnpm/yarn/bun)。源码目录拒绝自升级。
9
+ - 按包勾选写入 `.dwy/sync-state.json` 的 `selectionStyle` / `packs`。
10
+
11
+ ---
12
+
13
+ ## 0.17.1
14
+
15
+ ### Patch Changes
16
+
17
+ - `dwy --help` 正式列出 `dwy skills install` / `dwy skills --help`,不再标成隐藏命令。`dwy skills` 无参数时打印 skills 子命令说明。
18
+
19
+ ---
20
+
3
21
  ## 0.17.0
4
22
 
5
23
  ### Minor Changes
package/bin/index.js CHANGED
@@ -3,40 +3,42 @@
3
3
  import { readFileSync } from 'fs'
4
4
  import { fileURLToPath } from 'url'
5
5
  import { dirname, resolve } from 'path'
6
- import { syncAll } from '../src/sync-all.js'
7
- import { installSkills } from '../src/skills-install.js'
6
+ import { runDwy } from '../src/sync-all.js'
7
+ import { selfUpgrade } from '../src/self-upgrade.js'
8
+ import { buildHelpText } from '../src/cli-help.js'
8
9
 
9
10
  const __dirname = dirname(fileURLToPath(import.meta.url))
10
11
  const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'))
11
12
  const argv = process.argv.slice(2)
12
13
 
14
+ /** 是否请求帮助(含 -h / --help / help) */
15
+ function wantsHelp(args) {
16
+ return args.includes('--help') || args.includes('-h') || args[0] === 'help'
17
+ }
18
+
13
19
  function printHelp() {
14
- const lines = [
15
- `dwy ${pkg.version}`,
16
- '',
17
- 'Usage:',
18
- ' dwy 交互同步:先选完整/仅 Skills,末步可选全局目录',
19
- ' dwy --help 显示本说明',
20
- ' dwy -h 显示本说明',
21
- ' dwy --version 显示当前版本',
22
- ' dwy -V 显示当前版本',
23
- '',
24
- '以下为隐藏命令(向后兼容保留,日常无需手动调用):',
25
- ' dwy sync 同上的别名',
26
- ' dwy skills install 强制刷新全局外部 skill',
27
- '',
28
- ]
29
- console.log(lines.join('\n'))
20
+ console.log(buildHelpText(pkg.version))
30
21
  }
31
22
 
32
- if (argv.includes('--help') || argv.includes('-h') || argv[0] === 'help') {
23
+ if (wantsHelp(argv)) {
33
24
  printHelp()
34
25
  } else if (argv.includes('--version') || argv.includes('-V')) {
35
26
  console.log(pkg.version)
36
- } else if (argv[0] === 'skills' && argv[1] === 'install') {
37
- await installSkills()
27
+ } else if (argv[0] === 'upgrade') {
28
+ if (argv.length > 1) {
29
+ console.error(`未知命令: dwy ${argv.join(' ')}`)
30
+ printHelp()
31
+ process.exitCode = 1
32
+ } else {
33
+ try {
34
+ await selfUpgrade()
35
+ } catch (error) {
36
+ console.error(error.message)
37
+ process.exitCode = 1
38
+ }
39
+ }
38
40
  } else if (argv.length === 0 || argv[0] === 'sync') {
39
- await syncAll()
41
+ await runDwy()
40
42
  } else {
41
43
  console.error(`未知命令: ${argv.join(' ')}`)
42
44
  printHelp()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "项目脚手架与多平台 AI 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,24 @@
1
+ /**
2
+ * dwy 命令行帮助文案。
3
+ * 已实现的入口必须出现在这里;禁止把可用命令藏进「隐藏」段。
4
+ */
5
+
6
+ /**
7
+ * 主帮助:`dwy --help`。
8
+ * version 来自 package.json,调用方传入,避免本模块读盘。
9
+ *
10
+ * @param {string} version
11
+ */
12
+ export function buildHelpText(version) {
13
+ return [
14
+ `dwy ${version}`,
15
+ '',
16
+ 'Usage:',
17
+ ' dwy 同步项目配置,或刷新全局外部 skill',
18
+ ' dwy sync 同上',
19
+ ' dwy upgrade 把 dwy 升级到 npm 最新正式版',
20
+ ' dwy --help 显示本说明',
21
+ ' dwy --version 显示版本',
22
+ '',
23
+ ].join('\n')
24
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * dwy 自升级:把全局安装的 create-dwy 升到 npm dist-tag latest(正式版)。
3
+ * 不引入第三方更新器,安装命令走用户本机 npm/pnpm/yarn/bun。
4
+ * 源码目录 / 非全局安装拒绝执行,避免把仓库工作树覆盖成 npm 包。
5
+ */
6
+
7
+ import { spawn } from 'node:child_process'
8
+ import { readFileSync } from 'node:fs'
9
+ import path from 'node:path'
10
+ import { PACKAGE_ROOT, chalk } from './utils.js'
11
+
12
+ /** npm 包名,bin 是 dwy */
13
+ export const PACKAGE_NAME = 'create-dwy'
14
+
15
+ const DEFAULT_REGISTRY = 'https://registry.npmjs.org/'
16
+
17
+ /**
18
+ * 比较 a、b 两个三段版本号。a>b 返回 1,相等 0,a<b -1。
19
+ * 只处理正式版 MAJOR.MINOR.PATCH,不解析 beta。
20
+ *
21
+ * @param {string} a
22
+ * @param {string} b
23
+ */
24
+ export function compareVersions(a, b) {
25
+ const parse = (v) => v.split('.').map(part => Number.parseInt(part, 10) || 0)
26
+ const left = parse(a)
27
+ const right = parse(b)
28
+ for (let i = 0; i < 3; i++) {
29
+ if (left[i] > right[i]) return 1
30
+ if (left[i] < right[i]) return -1
31
+ }
32
+ return 0
33
+ }
34
+
35
+ /**
36
+ * 根据包安装路径判断全局包管理器。
37
+ * pnpm/yarn/bun 路径里也常有 node_modules/create-dwy,必须先匹配专用目录。
38
+ *
39
+ * @param {string} pkgRoot
40
+ * @returns {'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown'}
41
+ */
42
+ export function detectInstaller(pkgRoot) {
43
+ const normalized = pkgRoot.replace(/\\/g, '/')
44
+ if (normalized.includes('/pnpm/global/') || normalized.includes('/.local/share/pnpm/')) return 'pnpm'
45
+ if (normalized.includes('/yarn/global/') || normalized.includes('/.yarn/')) return 'yarn'
46
+ if (normalized.includes('/.bun/install/')) return 'bun'
47
+ if (normalized.endsWith('/node_modules/create-dwy')) return 'npm'
48
+ return 'unknown'
49
+ }
50
+
51
+ /**
52
+ * 构造升级命令。一律钉 @latest,不用 @next/@beta。
53
+ *
54
+ * @param {'npm' | 'pnpm' | 'yarn' | 'bun' | 'unknown'} installer
55
+ * @returns {[string, string[]] | null}
56
+ */
57
+ export function buildUpgradeCommand(installer) {
58
+ const spec = `${PACKAGE_NAME}@latest`
59
+ if (installer === 'pnpm') return ['pnpm', ['add', '-g', spec]]
60
+ if (installer === 'yarn') return ['yarn', ['global', 'add', spec]]
61
+ if (installer === 'bun') return ['bun', ['add', '-g', spec]]
62
+ if (installer === 'npm') return ['npm', ['install', '-g', spec]]
63
+ return null
64
+ }
65
+
66
+ /**
67
+ * 读当前包版本。测试可注入 currentVersion,避免读盘。
68
+ *
69
+ * @param {string} [pkgRoot]
70
+ */
71
+ function readCurrentVersion(pkgRoot = PACKAGE_ROOT) {
72
+ const pkg = JSON.parse(readFileSync(path.join(pkgRoot, 'package.json'), 'utf-8'))
73
+ return pkg.version
74
+ }
75
+
76
+ /**
77
+ * 查 npm registry 的 latest。用官方 JSON,不跟浮动 tag 以外的通道。
78
+ *
79
+ * @param {typeof fetch} [fetchFn]
80
+ */
81
+ export async function fetchLatestVersion(fetchFn = globalThis.fetch) {
82
+ const registry = (process.env.npm_config_registry || DEFAULT_REGISTRY).replace(/\/?$/, '/')
83
+ const url = `${registry}${PACKAGE_NAME}/latest`
84
+ const response = await fetchFn(url, { headers: { Accept: 'application/json', 'User-Agent': 'create-dwy' } })
85
+ if (!response.ok) {
86
+ throw new Error(`查询最新版本失败:${url} HTTP ${response.status}`)
87
+ }
88
+ const body = await response.json()
89
+ const version = body?.version
90
+ if (!version || typeof version !== 'string') {
91
+ throw new Error(`npm latest 未返回 version:${url}`)
92
+ }
93
+ return version
94
+ }
95
+
96
+ /**
97
+ * 跑包管理器命令,stdio 继承方便用户看安装进度。
98
+ * Windows 要 shell,才能找到 npm.cmd。
99
+ *
100
+ * @param {string} cmd
101
+ * @param {string[]} args
102
+ */
103
+ function runSpawn(cmd, args) {
104
+ return new Promise((resolve, reject) => {
105
+ const child = spawn(cmd, args, {
106
+ stdio: 'inherit',
107
+ shell: process.platform === 'win32',
108
+ })
109
+ child.on('error', reject)
110
+ child.on('exit', (code) => {
111
+ if (code === 0) resolve()
112
+ else reject(new Error(`${cmd} ${args.join(' ')} 退出码 ${code}`))
113
+ })
114
+ })
115
+ }
116
+
117
+ /**
118
+ * 执行自升级。返回结构化结果供测试断言;失败抛错。
119
+ *
120
+ * @param {object} [opts]
121
+ * @param {string} [opts.currentVersion]
122
+ * @param {string} [opts.pkgRoot]
123
+ * @param {() => Promise<string>} [opts.fetchLatest]
124
+ * @param {(cmd: string, args: string[]) => Promise<void>} [opts.runCommand]
125
+ */
126
+ export async function selfUpgrade({
127
+ currentVersion,
128
+ pkgRoot = PACKAGE_ROOT,
129
+ fetchLatest = fetchLatestVersion,
130
+ runCommand = runSpawn,
131
+ } = {}) {
132
+ const current = currentVersion || readCurrentVersion(pkgRoot)
133
+ const installer = detectInstaller(pkgRoot)
134
+ const command = buildUpgradeCommand(installer)
135
+ if (!command) {
136
+ throw new Error(
137
+ `当前不是全局安装的 ${PACKAGE_NAME}(路径 ${pkgRoot}),无法自升级。请先 npm i -g ${PACKAGE_NAME},或在源码仓 git pull。`,
138
+ )
139
+ }
140
+
141
+ const latest = await fetchLatest()
142
+ const cmp = compareVersions(current, latest)
143
+ if (cmp === 0) {
144
+ console.log(chalk.green(`已是最新正式版 ${current}`))
145
+ return { status: 'up-to-date', current, latest, installer }
146
+ }
147
+ if (cmp > 0) {
148
+ console.log(chalk.yellow(`当前 ${current} 新于 npm latest ${latest},不降级`))
149
+ return { status: 'newer-local', current, latest, installer }
150
+ }
151
+
152
+ const [cmd, args] = command
153
+ console.log(chalk.blue(`升级 ${PACKAGE_NAME} ${current} → ${latest}`))
154
+ console.log(chalk.gray(` ${cmd} ${args.join(' ')}`))
155
+ await runCommand(cmd, args)
156
+ console.log(chalk.green(`已升级到 ${latest}`))
157
+ return { status: 'upgraded', current, latest, installer }
158
+ }
package/src/sync-all.js CHANGED
@@ -33,7 +33,10 @@ import {
33
33
  } from './sync-codex.js'
34
34
  import { scanExistingCursorRules, syncCursor } from './sync-cursor.js'
35
35
  import { syncOpenCode } from './sync-opencode.js'
36
- import { ensureSkillsInstalled } from './skills-install.js'
36
+ import { ensureSkillsInstalled, installSkills } from './skills-install.js'
37
+ import { selfUpgrade } from './self-upgrade.js'
38
+ import { normalizePacks } from './sync-packs.js'
39
+ import { interactiveSelectByPacks } from './sync-scene.js'
37
40
 
38
41
  const PLATFORM_OPTIONS = [
39
42
  { value: 'claude', label: 'Claude Code', description: '支持 rules / skills / commands / hooks' },
@@ -69,6 +72,19 @@ export const SYNC_MODE_ALL = 'all'
69
72
  export const SYNC_MODE_SKILLS = 'skills'
70
73
  export const DEFAULT_SYNC_MODE = SYNC_MODE_ALL
71
74
 
75
+ /** 按条目勾选 Skills / Rules / Hooks */
76
+ export const SELECTION_STYLE_ITEMS = 'items'
77
+ /** 按技术栈包 + 场景包勾选 */
78
+ export const SELECTION_STYLE_PACKS = 'packs'
79
+ export const DEFAULT_SELECTION_STYLE = SELECTION_STYLE_PACKS
80
+
81
+ /** 同步项目配置(skills / rules / hooks) */
82
+ export const ACTION_SYNC = 'sync'
83
+ /** 强制刷新 ~/.dwy/skills 外部 skill */
84
+ export const ACTION_INSTALL_SKILLS = 'install-skills'
85
+ /** 把全局 create-dwy 升到 npm latest */
86
+ export const ACTION_UPGRADE = 'upgrade'
87
+
72
88
  const SKILL_ONLY_CATEGORIES = [{ key: 'skills', label: 'Skills' }]
73
89
 
74
90
  /**
@@ -81,6 +97,28 @@ export function normalizeSyncMode(raw) {
81
97
  return raw === SYNC_MODE_SKILLS ? SYNC_MODE_SKILLS : DEFAULT_SYNC_MODE
82
98
  }
83
99
 
100
+ /**
101
+ * 规范化勾选方式。未知值回退按包,避免新用户落到 80+ 条逐项勾选。
102
+ *
103
+ * @param {unknown} raw
104
+ * @returns {'items' | 'packs'}
105
+ */
106
+ export function normalizeSelectionStyle(raw) {
107
+ return raw === SELECTION_STYLE_ITEMS ? SELECTION_STYLE_ITEMS : DEFAULT_SELECTION_STYLE
108
+ }
109
+
110
+ /**
111
+ * 规范化入口动作。未知值回退同步,避免误进强制重装。
112
+ *
113
+ * @param {unknown} raw
114
+ * @returns {'sync' | 'install-skills' | 'upgrade'}
115
+ */
116
+ export function normalizeAction(raw) {
117
+ if (raw === ACTION_INSTALL_SKILLS) return ACTION_INSTALL_SKILLS
118
+ if (raw === ACTION_UPGRADE) return ACTION_UPGRADE
119
+ return ACTION_SYNC
120
+ }
121
+
84
122
  function unionSets(...sets) {
85
123
  return new Set(sets.flatMap(set => [...set]))
86
124
  }
@@ -170,6 +208,38 @@ export async function buildPlatformDefaultsFromLocalState(projectDir) {
170
208
  return defaults.length > 0 ? defaults : ['claude', 'codex']
171
209
  }
172
210
 
211
+ /**
212
+ * dwy 入口第一问:同步配置还是强制刷新全局外部 skill。
213
+ *
214
+ * @returns {Promise<'sync' | 'install-skills' | 'upgrade' | null>}
215
+ */
216
+ async function promptAction() {
217
+ const result = await searchableSelect({
218
+ message: '选择要做的事',
219
+ options: [
220
+ {
221
+ value: ACTION_SYNC,
222
+ label: '同步项目 AI 配置',
223
+ description: '按包或按条目,把 skills / rules / hooks 写入当前项目',
224
+ },
225
+ {
226
+ value: ACTION_INSTALL_SKILLS,
227
+ label: '刷新全局外部 skill',
228
+ description: '强制重装 ~/.dwy/skills(pm-skills / superpowers)',
229
+ },
230
+ {
231
+ value: ACTION_UPGRADE,
232
+ label: '升级 dwy 到最新正式版',
233
+ description: '等同 dwy upgrade,把全局 create-dwy 升到 npm latest',
234
+ },
235
+ ],
236
+ initialValue: ACTION_SYNC,
237
+ placeholder: SEARCH_PLACEHOLDER,
238
+ })
239
+ if (isCancel(result)) return null
240
+ return normalizeAction(result)
241
+ }
242
+
173
243
  /**
174
244
  * 开头选同步范围。仅 Skills 时后面不再问 rules / commands / hooks。
175
245
  *
@@ -198,6 +268,34 @@ async function promptSyncMode(initialValue) {
198
268
  return normalizeSyncMode(result)
199
269
  }
200
270
 
271
+ /**
272
+ * 选勾选方式。按包是默认推荐;按条目保留原来的逐项勾选。
273
+ *
274
+ * @param {'items' | 'packs'} initialValue
275
+ * @returns {Promise<'items' | 'packs' | null>}
276
+ */
277
+ async function promptSelectionStyle(initialValue) {
278
+ const result = await searchableSelect({
279
+ message: '选择勾选方式',
280
+ options: [
281
+ {
282
+ value: SELECTION_STYLE_PACKS,
283
+ label: '按技术栈 / 场景包(推荐)',
284
+ description: '先选 Vue/Python 等栈,再选产品0到1/自媒体等场景包',
285
+ },
286
+ {
287
+ value: SELECTION_STYLE_ITEMS,
288
+ label: '按条目',
289
+ description: '一条条勾选 Skills / Rules / Hooks',
290
+ },
291
+ ],
292
+ initialValue,
293
+ placeholder: SEARCH_PLACEHOLDER,
294
+ })
295
+ if (isCancel(result)) return null
296
+ return normalizeSelectionStyle(result)
297
+ }
298
+
201
299
  /**
202
300
  * 仅 Skills 模式:只写项目/全局 skill,并清理未选中的模板 skill。
203
301
  * 禁止调用完整平台 sync,否则空 rules/commands 会删掉项目已有项。
@@ -351,6 +449,8 @@ async function loadSyncState(projectDir) {
351
449
  platforms: {},
352
450
  skillScope: normalizeSkillScope(undefined),
353
451
  syncMode: DEFAULT_SYNC_MODE,
452
+ selectionStyle: DEFAULT_SELECTION_STYLE,
453
+ packs: normalizePacks(undefined),
354
454
  }
355
455
  }
356
456
 
@@ -360,6 +460,8 @@ async function loadSyncState(projectDir) {
360
460
  platforms: state.platforms || {},
361
461
  skillScope: normalizeSkillScope(state.skillScope),
362
462
  syncMode: normalizeSyncMode(state.syncMode),
463
+ selectionStyle: normalizeSelectionStyle(state.selectionStyle),
464
+ packs: normalizePacks(state.packs),
363
465
  }
364
466
  }
365
467
 
@@ -719,6 +821,37 @@ async function cleanupOpenCodePlatform(projectDir, scans) {
719
821
  return removedCount
720
822
  }
721
823
 
824
+ /**
825
+ * `dwy` / `dwy sync` 入口。先选动作再分支:同步 / 刷新外部 skill / 自升级。
826
+ * 测试注入 selected / action 时跳过动作菜单。
827
+ *
828
+ * @param {object} [opts]
829
+ */
830
+ export async function runDwy(opts = {}) {
831
+ const skipMenu = !!(opts.selected || opts.selectedPlatforms || opts.action)
832
+ const action = opts.action != null
833
+ ? normalizeAction(opts.action)
834
+ : (skipMenu ? ACTION_SYNC : await promptAction())
835
+ if (action === null) {
836
+ console.log(chalk.yellow('\n已取消。'))
837
+ return
838
+ }
839
+ if (action === ACTION_INSTALL_SKILLS) {
840
+ await installSkills()
841
+ return
842
+ }
843
+ if (action === ACTION_UPGRADE) {
844
+ try {
845
+ await selfUpgrade()
846
+ } catch (error) {
847
+ console.error(error.message)
848
+ process.exitCode = 1
849
+ }
850
+ return
851
+ }
852
+ await syncAll(opts)
853
+ }
854
+
722
855
  export async function syncAll({
723
856
  sourceDir: sourceDirOverride,
724
857
  projectDir: projectDirOverride,
@@ -731,6 +864,8 @@ export async function syncAll({
731
864
  syncMode: syncModeOverride,
732
865
  // 全局 skill 写入的 home,测试注入假目录,禁止默认写真实 $HOME
733
866
  homeDir,
867
+ // 测试注入勾选方式;交互模式在同步范围之后再问
868
+ selectionStyle: selectionStyleOverride,
734
869
  } = {}) {
735
870
  const sourceDir = sourceDirOverride || await resolveSourceDir()
736
871
  if (!await fs.pathExists(sourceDir)) {
@@ -741,7 +876,7 @@ export async function syncAll({
741
876
 
742
877
  // 开头无条件自检全局外部 skill:未装 / 清单变更(cli 加了 skill 或改了 tag)则自动更新,
743
878
  // 已装且清单一致则零开销跳过。失败不阻塞 sync(离线/网络故障,下次再来)。
744
- // 这样用户跑一次 `dwy` 就把 skill 更新到位,无需另记 `dwy skills install`。
879
+ // 这样用户跑一次同步就把 skill 更新到位;强制重装走入口「刷新全局外部 skill」。
745
880
  console.log(chalk.gray('检查全局外部 skill 更新...'))
746
881
  await ensureSkillsInstalled()
747
882
 
@@ -791,9 +926,23 @@ export async function syncAll({
791
926
  return
792
927
  }
793
928
 
794
- const selected = selectedOverride || await interactiveSelect(scans, selectionDefaults, {
795
- categories: syncMode === SYNC_MODE_SKILLS ? SKILL_ONLY_CATEGORIES : undefined,
796
- })
929
+ const selectionStyle = selectionStyleOverride
930
+ ? normalizeSelectionStyle(selectionStyleOverride)
931
+ : (skipInteractive
932
+ ? normalizeSelectionStyle(syncState.selectionStyle)
933
+ : await promptSelectionStyle(normalizeSelectionStyle(syncState.selectionStyle)))
934
+ if (selectionStyle === null) {
935
+ console.log(chalk.yellow('\n已取消同步。'))
936
+ return
937
+ }
938
+
939
+ const selected = selectedOverride || (
940
+ selectionStyle === SELECTION_STYLE_PACKS
941
+ ? await interactiveSelectByPacks(scans, syncState, { skillsOnly: syncMode === SYNC_MODE_SKILLS })
942
+ : await interactiveSelect(scans, selectionDefaults, {
943
+ categories: syncMode === SYNC_MODE_SKILLS ? SKILL_ONLY_CATEGORIES : undefined,
944
+ })
945
+ )
797
946
  if (selected === null) {
798
947
  console.log(chalk.yellow('\n已取消同步。'))
799
948
  return
@@ -943,6 +1092,11 @@ export async function syncAll({
943
1092
  }
944
1093
  syncState.skillScope = skillScope
945
1094
  syncState.syncMode = syncMode
1095
+ syncState.selectionStyle = selectionStyle
1096
+ // 按条目同步不覆盖上次按包选择,方便下次再走按包
1097
+ if (selectionStyle === SELECTION_STYLE_PACKS) {
1098
+ syncState.packs = normalizePacks(selected.packs)
1099
+ }
946
1100
  await saveSyncState(projectDir, syncState)
947
1101
 
948
1102
  if (!syncedAnySupportedPlatform) {