create-dwy 0.17.1 → 0.18.1

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 +19 -0
  2. package/bin/index.js +18 -20
  3. package/package.json +1 -1
  4. package/src/cli-help.js +5 -25
  5. package/src/self-upgrade.js +158 -0
  6. package/src/sync-all.js +155 -5
  7. package/src/sync-packs.js +191 -0
  8. package/src/sync-scene.js +169 -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,24 @@
1
1
  # create-dwy
2
2
 
3
+ ## 0.18.1
4
+
5
+ ### Patch Changes
6
+
7
+ - `dwy` 菜单只留两项:同步全局 skills、同步项目配置。升级只走 `dwy upgrade`。
8
+ - 同步项目配置里分「按场景和技术栈」与「一个一个选」。按包后直接列出已预勾子条目,可取消。
9
+
10
+ ---
11
+
12
+ ## 0.18.0
13
+
14
+ ### Minor Changes
15
+
16
+ - `dwy` / `dwy sync` 开头选动作:同步项目配置(按包或按条目),刷新全局外部 skill,或升级 dwy。删除 `dwy skills` / `dwy skills install` / `dwy scene`。
17
+ - 新增 `dwy upgrade`:把全局 `create-dwy` 升到 npm `latest` 正式版(npm/pnpm/yarn/bun)。源码目录拒绝自升级。
18
+ - 按包勾选写入 `.dwy/sync-state.json` 的 `selectionStyle` / `packs`。
19
+
20
+ ---
21
+
3
22
  ## 0.17.1
4
23
 
5
24
  ### Patch Changes
package/bin/index.js CHANGED
@@ -3,9 +3,9 @@
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'
8
- import { buildHelpText, buildSkillsHelpText } from '../src/cli-help.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'
9
9
 
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url))
11
11
  const pkg = JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json'), 'utf-8'))
@@ -20,27 +20,25 @@ function printHelp() {
20
20
  console.log(buildHelpText(pkg.version))
21
21
  }
22
22
 
23
- function printSkillsHelp() {
24
- console.log(buildSkillsHelpText())
25
- }
26
-
27
- if (argv[0] === 'skills') {
28
- const rest = argv.slice(1)
29
- if (rest.length === 0 || wantsHelp(rest) || rest[0] === 'help') {
30
- printSkillsHelp()
31
- } else if (rest[0] === 'install') {
32
- await installSkills()
33
- } else {
34
- console.error(`未知命令: dwy skills ${rest.join(' ')}`)
35
- printSkillsHelp()
36
- process.exitCode = 1
37
- }
38
- } else if (wantsHelp(argv)) {
23
+ if (wantsHelp(argv)) {
39
24
  printHelp()
40
25
  } else if (argv.includes('--version') || argv.includes('-V')) {
41
26
  console.log(pkg.version)
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
+ }
42
40
  } else if (argv.length === 0 || argv[0] === 'sync') {
43
- await syncAll()
41
+ await runDwy()
44
42
  } else {
45
43
  console.error(`未知命令: ${argv.join(' ')}`)
46
44
  printHelp()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-dwy",
3
- "version": "0.17.1",
3
+ "version": "0.18.1",
4
4
  "description": "项目脚手架与多平台 AI 配置同步工具",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli-help.js CHANGED
@@ -14,31 +14,11 @@ export function buildHelpText(version) {
14
14
  `dwy ${version}`,
15
15
  '',
16
16
  'Usage:',
17
- ' dwy 交互同步:选范围(完整/仅 Skills)、平台、skill 落点',
18
- ' dwy sync 同上',
19
- ' dwy skills install 强制刷新全局外部 skill 到 ~/.dwy/skills',
20
- ' dwy skills --help 显示 skills 子命令',
21
- ' dwy --help 显示本说明',
22
- ' dwy -h 显示本说明',
23
- ' dwy --version 显示当前版本',
24
- ' dwy -V 显示当前版本',
25
- '',
26
- 'Skills:',
27
- ' dwy skills install 按清单安装/覆盖更新 pm-skills、superpowers(一台机一份)',
28
- '',
29
- ].join('\n')
30
- }
31
-
32
- /**
33
- * skills 子命令帮助:`dwy skills` / `dwy skills --help`。
34
- */
35
- export function buildSkillsHelpText() {
36
- return [
37
- 'dwy skills',
38
- '',
39
- 'Usage:',
40
- ' dwy skills install 强制刷新全局外部 skill 到 ~/.dwy/skills',
41
- ' dwy skills --help 显示本说明',
17
+ ' dwy 同步全局 skills,或同步项目配置',
18
+ ' dwy sync 同上',
19
+ ' dwy upgrade 把 dwy 升级到 npm 最新正式版',
20
+ ' dwy --help 显示本说明',
21
+ ' dwy --version 显示版本',
42
22
  '',
43
23
  ].join('\n')
44
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,34 @@ export async function buildPlatformDefaultsFromLocalState(projectDir) {
170
208
  return defaults.length > 0 ? defaults : ['claude', 'codex']
171
209
  }
172
210
 
211
+ /**
212
+ * dwy 入口第一问:同步全局 skills,或同步项目配置。
213
+ * 自升级只走 `dwy upgrade`,不出现在这张菜单。
214
+ *
215
+ * @returns {Promise<'sync' | 'install-skills' | null>}
216
+ */
217
+ async function promptAction() {
218
+ const result = await searchableSelect({
219
+ message: '选择要做的事',
220
+ options: [
221
+ {
222
+ value: ACTION_INSTALL_SKILLS,
223
+ label: '同步全局 skills',
224
+ description: '强制重装 ~/.dwy/skills(pm-skills / superpowers)',
225
+ },
226
+ {
227
+ value: ACTION_SYNC,
228
+ label: '同步项目配置',
229
+ description: '按场景和技术栈,或一条条勾选,写入当前项目',
230
+ },
231
+ ],
232
+ initialValue: ACTION_SYNC,
233
+ placeholder: SEARCH_PLACEHOLDER,
234
+ })
235
+ if (isCancel(result)) return null
236
+ return normalizeAction(result)
237
+ }
238
+
173
239
  /**
174
240
  * 开头选同步范围。仅 Skills 时后面不再问 rules / commands / hooks。
175
241
  *
@@ -198,6 +264,34 @@ async function promptSyncMode(initialValue) {
198
264
  return normalizeSyncMode(result)
199
265
  }
200
266
 
267
+ /**
268
+ * 选勾选方式。按包是默认推荐;按条目保留原来的逐项勾选。
269
+ *
270
+ * @param {'items' | 'packs'} initialValue
271
+ * @returns {Promise<'items' | 'packs' | null>}
272
+ */
273
+ async function promptSelectionStyle(initialValue) {
274
+ const result = await searchableSelect({
275
+ message: '项目配置怎么勾',
276
+ options: [
277
+ {
278
+ value: SELECTION_STYLE_PACKS,
279
+ label: '按场景和技术栈(推荐)',
280
+ description: '先选栈和场景包,再展示已勾子条目,可取消',
281
+ },
282
+ {
283
+ value: SELECTION_STYLE_ITEMS,
284
+ label: '一个一个选',
285
+ description: '一条条勾选 Skills / Rules / Hooks',
286
+ },
287
+ ],
288
+ initialValue,
289
+ placeholder: SEARCH_PLACEHOLDER,
290
+ })
291
+ if (isCancel(result)) return null
292
+ return normalizeSelectionStyle(result)
293
+ }
294
+
201
295
  /**
202
296
  * 仅 Skills 模式:只写项目/全局 skill,并清理未选中的模板 skill。
203
297
  * 禁止调用完整平台 sync,否则空 rules/commands 会删掉项目已有项。
@@ -351,6 +445,8 @@ async function loadSyncState(projectDir) {
351
445
  platforms: {},
352
446
  skillScope: normalizeSkillScope(undefined),
353
447
  syncMode: DEFAULT_SYNC_MODE,
448
+ selectionStyle: DEFAULT_SELECTION_STYLE,
449
+ packs: normalizePacks(undefined),
354
450
  }
355
451
  }
356
452
 
@@ -360,6 +456,8 @@ async function loadSyncState(projectDir) {
360
456
  platforms: state.platforms || {},
361
457
  skillScope: normalizeSkillScope(state.skillScope),
362
458
  syncMode: normalizeSyncMode(state.syncMode),
459
+ selectionStyle: normalizeSelectionStyle(state.selectionStyle),
460
+ packs: normalizePacks(state.packs),
363
461
  }
364
462
  }
365
463
 
@@ -719,6 +817,37 @@ async function cleanupOpenCodePlatform(projectDir, scans) {
719
817
  return removedCount
720
818
  }
721
819
 
820
+ /**
821
+ * `dwy` / `dwy sync` 入口。先选动作再分支:同步全局 skills / 同步项目配置。
822
+ * 自升级只走 `dwy upgrade`。测试注入 selected / action 时跳过动作菜单。
823
+ *
824
+ * @param {object} [opts]
825
+ */
826
+ export async function runDwy(opts = {}) {
827
+ const skipMenu = !!(opts.selected || opts.selectedPlatforms || opts.action)
828
+ const action = opts.action != null
829
+ ? normalizeAction(opts.action)
830
+ : (skipMenu ? ACTION_SYNC : await promptAction())
831
+ if (action === null) {
832
+ console.log(chalk.yellow('\n已取消。'))
833
+ return
834
+ }
835
+ if (action === ACTION_INSTALL_SKILLS) {
836
+ await installSkills()
837
+ return
838
+ }
839
+ if (action === ACTION_UPGRADE) {
840
+ try {
841
+ await selfUpgrade()
842
+ } catch (error) {
843
+ console.error(error.message)
844
+ process.exitCode = 1
845
+ }
846
+ return
847
+ }
848
+ await syncAll(opts)
849
+ }
850
+
722
851
  export async function syncAll({
723
852
  sourceDir: sourceDirOverride,
724
853
  projectDir: projectDirOverride,
@@ -731,6 +860,8 @@ export async function syncAll({
731
860
  syncMode: syncModeOverride,
732
861
  // 全局 skill 写入的 home,测试注入假目录,禁止默认写真实 $HOME
733
862
  homeDir,
863
+ // 测试注入勾选方式;交互模式在同步范围之后再问
864
+ selectionStyle: selectionStyleOverride,
734
865
  } = {}) {
735
866
  const sourceDir = sourceDirOverride || await resolveSourceDir()
736
867
  if (!await fs.pathExists(sourceDir)) {
@@ -741,7 +872,7 @@ export async function syncAll({
741
872
 
742
873
  // 开头无条件自检全局外部 skill:未装 / 清单变更(cli 加了 skill 或改了 tag)则自动更新,
743
874
  // 已装且清单一致则零开销跳过。失败不阻塞 sync(离线/网络故障,下次再来)。
744
- // 这样用户跑一次 `dwy` 就把 skill 更新到位,无需另记 `dwy skills install`。
875
+ // 这样用户跑一次同步就把 skill 更新到位;强制重装走入口「刷新全局外部 skill」。
745
876
  console.log(chalk.gray('检查全局外部 skill 更新...'))
746
877
  await ensureSkillsInstalled()
747
878
 
@@ -791,9 +922,23 @@ export async function syncAll({
791
922
  return
792
923
  }
793
924
 
794
- const selected = selectedOverride || await interactiveSelect(scans, selectionDefaults, {
795
- categories: syncMode === SYNC_MODE_SKILLS ? SKILL_ONLY_CATEGORIES : undefined,
796
- })
925
+ const selectionStyle = selectionStyleOverride
926
+ ? normalizeSelectionStyle(selectionStyleOverride)
927
+ : (skipInteractive
928
+ ? normalizeSelectionStyle(syncState.selectionStyle)
929
+ : await promptSelectionStyle(normalizeSelectionStyle(syncState.selectionStyle)))
930
+ if (selectionStyle === null) {
931
+ console.log(chalk.yellow('\n已取消同步。'))
932
+ return
933
+ }
934
+
935
+ const selected = selectedOverride || (
936
+ selectionStyle === SELECTION_STYLE_PACKS
937
+ ? await interactiveSelectByPacks(scans, syncState, { skillsOnly: syncMode === SYNC_MODE_SKILLS })
938
+ : await interactiveSelect(scans, selectionDefaults, {
939
+ categories: syncMode === SYNC_MODE_SKILLS ? SKILL_ONLY_CATEGORIES : undefined,
940
+ })
941
+ )
797
942
  if (selected === null) {
798
943
  console.log(chalk.yellow('\n已取消同步。'))
799
944
  return
@@ -943,6 +1088,11 @@ export async function syncAll({
943
1088
  }
944
1089
  syncState.skillScope = skillScope
945
1090
  syncState.syncMode = syncMode
1091
+ syncState.selectionStyle = selectionStyle
1092
+ // 按条目同步不覆盖上次按包选择,方便下次再走按包
1093
+ if (selectionStyle === SELECTION_STYLE_PACKS) {
1094
+ syncState.packs = normalizePacks(selected.packs)
1095
+ }
946
1096
  await saveSyncState(projectDir, syncState)
947
1097
 
948
1098
  if (!syncedAnySupportedPlatform) {